From ed839618a7debc33836908aab1d602c61fef94d3 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 26 Jan 2024 18:06:04 +0800 Subject: [PATCH 1/5] feat: move anwser related code --- app/components/base/app-icon/index.tsx | 2 +- app/components/base/app-icon/style.module.css | 10 +- .../icons/line/arrows/chevron-down/data.json | 39 +++++++ .../icons/line/arrows/chevron-down/index.tsx | 16 +++ .../base/icons/public/data-set/data.json | 64 +++++++++++ .../base/icons/public/data-set/index.tsx | 16 +++ .../solid/general/check-circle/data.json | 38 ++++++ .../solid/general/check-circle/index.tsx | 16 +++ app/components/chat/answer/index.tsx | 65 ++++++++--- app/components/chat/thought/index.tsx | 61 ++++++++++ app/components/chat/thought/panel.tsx | 28 +++++ app/components/chat/thought/style.module.css | 7 ++ app/components/chat/thought/tool.tsx | 103 +++++++++++++++++ i18n/i18next-config.ts | 6 + i18n/lang/tools.en.ts | 103 +++++++++++++++++ i18n/lang/tools.zh.ts | 95 +++++++++++++++ types/app.ts | 1 + types/base.ts | 5 + types/tools.ts | 108 ++++++++++++++++++ 19 files changed, 768 insertions(+), 15 deletions(-) create mode 100644 app/components/base/icons/line/arrows/chevron-down/data.json create mode 100644 app/components/base/icons/line/arrows/chevron-down/index.tsx create mode 100644 app/components/base/icons/public/data-set/data.json create mode 100644 app/components/base/icons/public/data-set/index.tsx create mode 100644 app/components/base/icons/solid/general/check-circle/data.json create mode 100644 app/components/base/icons/solid/general/check-circle/index.tsx create mode 100644 app/components/chat/thought/index.tsx create mode 100644 app/components/chat/thought/panel.tsx create mode 100644 app/components/chat/thought/style.module.css create mode 100644 app/components/chat/thought/tool.tsx create mode 100644 i18n/lang/tools.en.ts create mode 100644 i18n/lang/tools.zh.ts create mode 100644 types/base.ts create mode 100644 types/tools.ts diff --git a/app/components/base/app-icon/index.tsx b/app/components/base/app-icon/index.tsx index b70991b..48e1608 100644 --- a/app/components/base/app-icon/index.tsx +++ b/app/components/base/app-icon/index.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames' import style from './style.module.css' export type AppIconProps = { - size?: 'tiny' | 'small' | 'medium' | 'large' + size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' rounded?: boolean icon?: string background?: string diff --git a/app/components/base/app-icon/style.module.css b/app/components/base/app-icon/style.module.css index 43098fd..f73ba60 100644 --- a/app/components/base/app-icon/style.module.css +++ b/app/components/base/app-icon/style.module.css @@ -1,15 +1,23 @@ .appIcon { @apply flex items-center justify-center relative w-9 h-9 text-lg bg-teal-100 rounded-lg grow-0 shrink-0; } + .appIcon.large { @apply w-10 h-10; } + .appIcon.small { @apply w-8 h-8; } + +.appIcon.xs { + @apply w-3 h-3 text-base; +} + .appIcon.tiny { @apply w-6 h-6 text-base; } + .appIcon.rounded { @apply rounded-full; -} +} \ No newline at end of file diff --git a/app/components/base/icons/line/arrows/chevron-down/data.json b/app/components/base/icons/line/arrows/chevron-down/data.json new file mode 100644 index 0000000..a57225d --- /dev/null +++ b/app/components/base/icons/line/arrows/chevron-down/data.json @@ -0,0 +1,39 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "12", + "height": "12", + "viewBox": "0 0 12 12", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "chevron-down" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon", + "d": "M3 4.5L6 7.5L9 4.5", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + } + ] + }, + "name": "ChevronDown" +} \ No newline at end of file diff --git a/app/components/base/icons/line/arrows/chevron-down/index.tsx b/app/components/base/icons/line/arrows/chevron-down/index.tsx new file mode 100644 index 0000000..3c70693 --- /dev/null +++ b/app/components/base/icons/line/arrows/chevron-down/index.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './data.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'ChevronDown' + +export default Icon diff --git a/app/components/base/icons/public/data-set/data.json b/app/components/base/icons/public/data-set/data.json new file mode 100644 index 0000000..55952fe --- /dev/null +++ b/app/components/base/icons/public/data-set/data.json @@ -0,0 +1,64 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "12", + "height": "12", + "viewBox": "0 0 12 12", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "clip-path": "url(#clip0_7847_32895)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M10.5 2.5C10.5 3.32843 8.48528 4 6 4C3.51472 4 1.5 3.32843 1.5 2.5M10.5 2.5C10.5 1.67157 8.48528 1 6 1C3.51472 1 1.5 1.67157 1.5 2.5M10.5 2.5V9.5C10.5 10.33 8.5 11 6 11C3.5 11 1.5 10.33 1.5 9.5V2.5M10.5 6C10.5 6.83 8.5 7.5 6 7.5C3.5 7.5 1.5 6.83 1.5 6", + "stroke": "#667085", + "stroke-width": "1.25", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "clip0_7847_32895" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "12", + "height": "12", + "fill": "white" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "DataSet" +} \ No newline at end of file diff --git a/app/components/base/icons/public/data-set/index.tsx b/app/components/base/icons/public/data-set/index.tsx new file mode 100644 index 0000000..cadbbdc --- /dev/null +++ b/app/components/base/icons/public/data-set/index.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './DataSet.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'DataSet' + +export default Icon diff --git a/app/components/base/icons/solid/general/check-circle/data.json b/app/components/base/icons/solid/general/check-circle/data.json new file mode 100644 index 0000000..1b567e8 --- /dev/null +++ b/app/components/base/icons/solid/general/check-circle/data.json @@ -0,0 +1,38 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "check-circle" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Solid", + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM11.4714 6.47136C11.7318 6.21101 11.7318 5.7889 11.4714 5.52855C11.2111 5.26821 10.7889 5.26821 10.5286 5.52855L7 9.05715L5.47141 7.52855C5.21106 7.2682 4.78895 7.2682 4.5286 7.52855C4.26825 7.7889 4.26825 8.21101 4.5286 8.47136L6.5286 10.4714C6.78895 10.7317 7.21106 10.7317 7.47141 10.4714L11.4714 6.47136Z", + "fill": "currentColor" + }, + "children": [] + } + ] + } + ] + }, + "name": "CheckCircle" +} \ No newline at end of file diff --git a/app/components/base/icons/solid/general/check-circle/index.tsx b/app/components/base/icons/solid/general/check-circle/index.tsx new file mode 100644 index 0000000..fe2cbfc --- /dev/null +++ b/app/components/base/icons/solid/general/check-circle/index.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './CheckCircle.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'CheckCircle' + +export default Icon diff --git a/app/components/chat/answer/index.tsx b/app/components/chat/answer/index.tsx index cc0c463..7489b6f 100644 --- a/app/components/chat/answer/index.tsx +++ b/app/components/chat/answer/index.tsx @@ -6,10 +6,13 @@ import { useTranslation } from 'react-i18next' import LoadingAnim from '../loading-anim' import type { FeedbackFunc, IChatItem } from '../type' import s from '../style.module.css' +import ImageGallery from '../../base/image-gallery' +import Thought from '../thought' import { randomString } from '@/utils/string' -import type { MessageRating } from '@/types/app' +import type { MessageRating, VisionFile } from '@/types/app' import Tooltip from '@/app/components/base/tooltip' import { Markdown } from '@/app/components/base/markdown' +import type { Emoji } from '@/types/tools' const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
} // The component needs to maintain its own state to control whether to display input component -const Answer: FC = ({ item, feedbackDisabled = false, onFeedback, isResponsing }) => { - const { id, content, feedback } = item +const Answer: FC = ({ + item, + feedbackDisabled = false, + onFeedback, + isResponsing, + allToolIcons, +}) => { + const { id, content, feedback, agent_thoughts } = item + const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0 + const { t } = useTranslation() /** @@ -121,6 +133,37 @@ const Answer: FC = ({ item, feedbackDisabled = false, onFeedback, ) } + const getImgs = (list?: VisionFile[]) => { + if (!list) + return [] + return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant') + } + + const agentModeAnswer = ( +
+ {agent_thoughts?.map((item, index) => ( +
+ {item.thought && ( + + )} + {/* {item.tool} */} + {/* perhaps not use tool */} + {!!item.tool && ( + + )} + + {getImgs(item.message_files).length > 0 && ( + item.url)} /> + )} +
+ ))} +
+ ) + return (
@@ -134,21 +177,17 @@ const Answer: FC = ({ item, feedbackDisabled = false, onFeedback,
- {item.isOpeningStatement && ( -
- -
{t('app.chat.openingStatementTitle')}
-
- )} - {(isResponsing && !content) + {(isResponsing && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content)) ? (
) - : ( - - )} + : (isAgentMode + ? agentModeAnswer + : ( + + ))}
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()} diff --git a/app/components/chat/thought/index.tsx b/app/components/chat/thought/index.tsx new file mode 100644 index 0000000..6a893b1 --- /dev/null +++ b/app/components/chat/thought/index.tsx @@ -0,0 +1,61 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import type { ThoughtItem, ToolInfoInThought } from '../type' +import Tool from './tool' +import type { Emoji } from '@/types/tools' + +export type IThoughtProps = { + thought: ThoughtItem + allToolIcons: Record + isFinished: boolean +} + +function getValue(value: string, isValueArray: boolean, index: number) { + if (isValueArray) { + try { + return JSON.parse(value)[index] + } + catch (e) { + } + } + return value +} + +const Thought: FC = ({ + thought, + allToolIcons, + isFinished, +}) => { + const [toolNames, isValueArray]: [string[], boolean] = (() => { + try { + if (Array.isArray(JSON.parse(thought.tool))) + return [JSON.parse(thought.tool), true] + } + catch (e) { + } + return [[thought.tool], false] + })() + + const toolThoughtList = toolNames.map((toolName, index) => { + return { + name: toolName, + input: getValue(thought.tool_input, isValueArray, index), + output: getValue(thought.observation, isValueArray, index), + isFinished, + } + }) + + return ( +
+ {toolThoughtList.map((item: ToolInfoInThought, index) => ( + + ))} +
+ ) +} +export default React.memo(Thought) diff --git a/app/components/chat/thought/panel.tsx b/app/components/chat/thought/panel.tsx new file mode 100644 index 0000000..2b0f1f9 --- /dev/null +++ b/app/components/chat/thought/panel.tsx @@ -0,0 +1,28 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' + +type Props = { + isRequest: boolean + toolName: string + content: string +} + +const Panel: FC = ({ + isRequest, + toolName, + content, +}) => { + const { t } = useTranslation() + + return ( +
+
+ {t(`tools.thought.${isRequest ? 'requestTitle' : 'responseTitle'}`)} {toolName} +
+
{content}
+
+ ) +} +export default React.memo(Panel) diff --git a/app/components/chat/thought/style.module.css b/app/components/chat/thought/style.module.css new file mode 100644 index 0000000..2b6aa26 --- /dev/null +++ b/app/components/chat/thought/style.module.css @@ -0,0 +1,7 @@ +.wrap { + background-color: rgba(255, 255, 255, 0.92); +} + +.wrapHoverEffect:hover{ + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.1); +} \ No newline at end of file diff --git a/app/components/chat/thought/tool.tsx b/app/components/chat/thought/tool.tsx new file mode 100644 index 0000000..d52728f --- /dev/null +++ b/app/components/chat/thought/tool.tsx @@ -0,0 +1,103 @@ +'use client' +import type { FC } from 'react' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import cn from 'classnames' +import type { ToolInfoInThought } from '../type' +import Panel from './panel' +import Loading02 from '@/app/components/base/icons/line/loading-02' +import ChevronDown from '@/app/components/base/icons/line/arrows/chevron-down' +import CheckCircle from '@/app/components/base/icons/solid/general/check-circle' +import DataSetIcon from '@/app/components/base/icons/public/data-set' +import type { Emoji } from '@/types/tools' +import AppIcon from '@/app/components/base/app-icon' + +type Props = { + payload: ToolInfoInThought + allToolIcons?: Record +} + +const getIcon = (toolName: string, allToolIcons: Record) => { + if (toolName.startsWith('dataset-')) + return + const icon = allToolIcons[toolName] + if (!icon) + return null + return ( + typeof icon === 'string' + ? ( +
+ ) + : ( + + )) +} + +const Tool: FC = ({ + payload, + allToolIcons = {}, +}) => { + const { t } = useTranslation() + const { name, input, isFinished, output } = payload + const toolName = name.startsWith('dataset-') ? t('dataset.knowledge') : name + const [isShowDetail, setIsShowDetail] = useState(false) + const icon = getIcon(toolName, allToolIcons) as any + return ( +
+
+
setIsShowDetail(!isShowDetail)} + > + {!isFinished && ( + + )} + {isFinished && !isShowDetail && ( + + )} + {isFinished && isShowDetail && ( + icon + )} + + {t(`tools.thought.${isFinished ? 'used' : 'using'}`)} + + + {toolName} + + +
+ {isShowDetail && ( +
+ + {output && ( + + )} +
+ )} +
+
+ ) +} +export default React.memo(Tool) diff --git a/i18n/i18next-config.ts b/i18n/i18next-config.ts index 13580b1..96de654 100644 --- a/i18n/i18next-config.ts +++ b/i18n/i18next-config.ts @@ -5,6 +5,8 @@ import commonEn from './lang/common.en' import commonZh from './lang/common.zh' import appEn from './lang/app.en' import appZh from './lang/app.zh' +import toolsEn from './lang/tools.en' +import toolsZh from './lang/tools.zh' import type { Locale } from '.' const resources = { @@ -12,12 +14,16 @@ const resources = { translation: { common: commonEn, app: appEn, + // tools + tools: toolsEn, }, }, 'zh-Hans': { translation: { common: commonZh, app: appZh, + // tools + tools: toolsZh, }, }, } diff --git a/i18n/lang/tools.en.ts b/i18n/lang/tools.en.ts new file mode 100644 index 0000000..7b8314f --- /dev/null +++ b/i18n/lang/tools.en.ts @@ -0,0 +1,103 @@ +const translation = { + title: 'Tools', + createCustomTool: 'Create Custom Tool', + type: { + all: 'All', + builtIn: 'Built-in', + custom: 'Custom', + }, + contribute: { + line1: 'I\'m interested in ', + line2: 'contributing tools to Dify.', + viewGuide: 'View the guide', + }, + author: 'By', + auth: { + unauthorized: 'To Authorize', + authorized: 'Authorized', + setup: 'Set up authorization to use', + setupModalTitle: 'Set Up Authorization', + setupModalTitleDescription: 'After configuring credentials, all members within the workspace can use this tool when orchestrating applications.', + }, + includeToolNum: '{{num}} tools included', + addTool: 'Add Tool', + createTool: { + title: 'Create Custom Tool', + editAction: 'Configure', + editTitle: 'Edit Custom Tool', + name: 'Name', + toolNamePlaceHolder: 'Enter the tool name', + schema: 'Schema', + schemaPlaceHolder: 'Enter your OpenAPI schema here', + viewSchemaSpec: 'View the OpenAPI-Swagger Specification', + importFromUrl: 'Import from URL', + importFromUrlPlaceHolder: 'https://...', + urlError: 'Please enter a valid URL', + examples: 'Examples', + exampleOptions: { + json: 'Weather(JSON)', + yaml: 'Pet Store(YAML)', + blankTemplate: 'Blank Template', + }, + availableTools: { + title: 'Available Tools', + name: 'Name', + description: 'Description', + method: 'Method', + path: 'Path', + action: 'Actions', + test: 'Test', + }, + authMethod: { + title: 'Authorization method', + type: 'Authorization type', + types: { + none: 'None', + api_key: 'API Key', + }, + key: 'Key', + value: 'Value', + }, + privacyPolicy: 'Privacy policy', + privacyPolicyPlaceholder: 'Please enter privacy policy', + }, + test: { + title: 'Test', + parametersValue: 'Parameters & Value', + parameters: 'Parameters', + value: 'Value', + testResult: 'Test Results', + testResultPlaceholder: 'Test result will show here', + }, + thought: { + using: 'Using', + used: 'Used', + requestTitle: 'Request to', + responseTitle: 'Response from', + }, + setBuiltInTools: { + info: 'Info', + setting: 'Setting', + toolDescription: 'Tool description', + parameters: 'parameters', + string: 'string', + number: 'number', + required: 'Required', + infoAndSetting: 'Info & Settings', + }, + noCustomTool: { + title: 'No custom tools!', + content: 'Add and manage your custom tools here for building AI apps.', + createTool: 'Create Tool', + }, + noSearchRes: { + title: 'Sorry, no results!', + content: 'We couldn\'t find any tools that match your search.', + reset: 'Reset Search', + }, + builtInPromptTitle: 'Prompt', + toolRemoved: 'Tool removed', + notAuthorized: 'Tool not authorized', +} + +export default translation diff --git a/i18n/lang/tools.zh.ts b/i18n/lang/tools.zh.ts new file mode 100644 index 0000000..40753dd --- /dev/null +++ b/i18n/lang/tools.zh.ts @@ -0,0 +1,95 @@ +const translation = { + title: '工具', + createCustomTool: '创建自定义工具', + type: { + all: '全部', + builtIn: '内置', + custom: '自定义', + }, + contribute: { + line1: '我有兴趣为 ', + line2: 'Dify 贡献工具。', + viewGuide: '查看指南', + }, + author: '作者', + auth: { + unauthorized: '去授权', + authorized: '已授权', + setup: '要使用请先授权', + setupModalTitle: '设置授权', + setupModalTitleDescription: '配置凭据后,工作区中的所有成员都可以在编排应用程序时使用此工具。', + }, + includeToolNum: '包含 {{num}} 个工具', + addTool: '添加工具', + createTool: { + title: '创建自定义工具', + editAction: '编辑', + editTitle: '编辑自定义工具', + name: '名称', + toolNamePlaceHolder: '输入工具名称', + schema: 'Schema', + schemaPlaceHolder: '在此处输入您的 OpenAPI schema', + viewSchemaSpec: '查看 OpenAPI-Swagger 规范', + importFromUrl: '从 URL 中导入', + importFromUrlPlaceHolder: 'https://...', + urlError: '请输入有效的 URL', + examples: '例子', + exampleOptions: { + json: '天气(JSON)', + yaml: '宠物商店(YAML)', + blankTemplate: '空白模版', + }, + availableTools: { + title: '可用工具', + name: '名称', + description: '描述', + method: '方法', + path: '路径', + action: '操作', + test: '测试', + }, + authMethod: { + title: '鉴权方法', + type: '鉴权类型', + types: { + none: '无', + api_key: 'API Key', + }, + key: '键', + value: '值', + }, + privacyPolicy: '隐私协议', + privacyPolicyPlaceholder: '请输入隐私协议', + }, + thought: { + using: '正在使用', + used: '已使用', + requestTitle: '请求来自', + responseTitle: '响应来自', + }, + setBuiltInTools: { + info: '信息', + setting: '设置', + toolDescription: '工具描述', + parameters: '参数', + string: '字符串', + number: '数字', + required: '必填', + infoAndSetting: '信息和设置', + }, + noCustomTool: { + title: '没有自定义工具!', + content: '在此统一添加和管理你的自定义工具,方便构建应用时使用。', + createTool: '创建工具', + }, + noSearchRes: { + title: '抱歉,没有结果!', + content: '我们找不到任何与您的搜索相匹配的工具。', + reset: '重置搜索', + }, + builtInPromptTitle: '提示词', + toolRemoved: '工具已被移除', + notAuthorized: '工具未授权', +} + +export default translation diff --git a/types/app.ts b/types/app.ts index 2efcb95..93d9691 100644 --- a/types/app.ts +++ b/types/app.ts @@ -133,4 +133,5 @@ export type VisionFile = { transfer_method: TransferMethod url: string upload_file_id: string + belongs_to?: string } diff --git a/types/base.ts b/types/base.ts new file mode 100644 index 0000000..35b63ea --- /dev/null +++ b/types/base.ts @@ -0,0 +1,5 @@ +export type TypeWithI18N = { + 'en_US': T + 'zh_Hans': T + [key: string]: T +} diff --git a/types/tools.ts b/types/tools.ts new file mode 100644 index 0000000..b97ee61 --- /dev/null +++ b/types/tools.ts @@ -0,0 +1,108 @@ +import type { TypeWithI18N } from './base' +export enum LOC { + tools = 'tools', + app = 'app', +} + +export enum AuthType { + none = 'none', + apiKey = 'api_key', +} + +export type Credential = { + 'auth_type': AuthType + 'api_key_header'?: string + 'api_key_value'?: string +} + +export enum CollectionType { + all = 'all', + builtIn = 'builtin', + custom = 'api', +} + +export type Emoji = { + background: string + content: string +} + +export type Collection = { + id: string + name: string + author: string + description: TypeWithI18N + icon: string | Emoji + label: TypeWithI18N + type: CollectionType + team_credentials: Record + is_team_authorization: boolean + allow_delete: boolean +} + +export type ToolParameter = { + name: string + label: TypeWithI18N + human_description: TypeWithI18N + type: string + required: boolean + default: string + options?: { + label: TypeWithI18N + value: string + }[] +} + +export type Tool = { + name: string + label: TypeWithI18N + description: any + parameters: ToolParameter[] +} + +export type ToolCredential = { + name: string + label: TypeWithI18N + help: TypeWithI18N + placeholder: TypeWithI18N + type: string + required: boolean + default: string + options?: { + label: TypeWithI18N + value: string + }[] +} + +export type CustomCollectionBackend = { + provider: string + original_provider?: string + credentials: Credential + icon: Emoji + schema_type: string + schema: string + privacy_policy: string + tools?: ParamItem[] +} + +export type ParamItem = { + name: string + label: TypeWithI18N + human_description: TypeWithI18N + type: string + required: boolean + default: string + min?: number + max?: number + options?: { + label: TypeWithI18N + value: string + }[] +} + +export type CustomParamSchema = { + operation_id: string // name + summary: string + server_url: string + method: string + parameters: ParamItem[] +} From 2019d8f3e3478027ad83736111de96403e527db9 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 29 Jan 2024 10:27:55 +0800 Subject: [PATCH 2/5] feat: change service code --- service/base.ts | 102 +++++++++++++++++++++++++++++++++-------------- service/index.ts | 11 +++-- 2 files changed, 81 insertions(+), 32 deletions(-) diff --git a/service/base.ts b/service/base.ts index 33ca199..16dd112 100644 --- a/service/base.ts +++ b/service/base.ts @@ -1,5 +1,7 @@ import { API_PREFIX } from '@/config' import Toast from '@/app/components/base/toast' +import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/chat/type' +import type { VisionFile } from '@/types/app' const TIME_OUT = 100000 @@ -21,20 +23,35 @@ const baseOptions = { } export type IOnDataMoreInfo = { - conversationId: string | undefined + conversationId?: string + taskId?: string messageId: string errorMessage?: string + errorCode?: string } export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void -export type IOnCompleted = () => void -export type IOnError = (msg: string) => void +export type IOnThought = (though: ThoughtItem) => void +export type IOnFile = (file: VisionFile) => void +export type IOnMessageEnd = (messageEnd: MessageEnd) => void +export type IOnMessageReplace = (messageReplace: MessageReplace) => void +export type IOnAnnotationReply = (messageReplace: AnnotationReply) => void +export type IOnCompleted = (hasError?: boolean) => void +export type IOnError = (msg: string, code?: string) => void type IOtherOptions = { + isPublicAPI?: boolean + bodyStringify?: boolean needAllResponseContent?: boolean + deleteContentType?: boolean onData?: IOnData // for stream + onThought?: IOnThought + onFile?: IOnFile + onMessageEnd?: IOnMessageEnd + onMessageReplace?: IOnMessageReplace onError?: IOnError onCompleted?: IOnCompleted // for stream + getAbortController?: (abortController: AbortController) => void } function unicodeToChar(text: string) { @@ -43,17 +60,18 @@ function unicodeToChar(text: string) { }) } -const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted) => { +const handleStream = (response: Response, onData: IOnData, onCompleted?: IOnCompleted, onThought?: IOnThought, onMessageEnd?: IOnMessageEnd, onMessageReplace?: IOnMessageReplace, onFile?: IOnFile) => { if (!response.ok) throw new Error('Network response was not ok') - const reader = response.body.getReader() + const reader = response.body?.getReader() const decoder = new TextDecoder('utf-8') let buffer = '' - let bufferObj: any + let bufferObj: Record let isFirstMessage = true function read() { - reader.read().then((result: any) => { + let hasError = false + reader?.read().then((result: any) => { if (result.done) { onCompleted && onCompleted() return @@ -62,27 +80,51 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted const lines = buffer.split('\n') try { lines.forEach((message) => { - if (!message || !message.startsWith('data: ')) - return - try { - bufferObj = JSON.parse(message.substring(6)) // remove data: and parse as json + if (message.startsWith('data: ')) { // check if it starts with data: + try { + bufferObj = JSON.parse(message.substring(6)) as Record// remove data: and parse as json + } + catch (e) { + // mute handle message cut off + onData('', isFirstMessage, { + conversationId: bufferObj?.conversation_id, + messageId: bufferObj?.message_id, + }) + return + } + if (bufferObj.status === 400 || !bufferObj.event) { + onData('', false, { + conversationId: undefined, + messageId: '', + errorMessage: bufferObj?.message, + errorCode: bufferObj?.code, + }) + hasError = true + onCompleted?.(true) + return + } + if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') { + // can not use format here. Because message is splited. + onData(unicodeToChar(bufferObj.answer), isFirstMessage, { + conversationId: bufferObj.conversation_id, + taskId: bufferObj.task_id, + messageId: bufferObj.id, + }) + isFirstMessage = false + } + else if (bufferObj.event === 'agent_thought') { + onThought?.(bufferObj as ThoughtItem) + } + else if (bufferObj.event === 'message_file') { + onFile?.(bufferObj as VisionFile) + } + else if (bufferObj.event === 'message_end') { + onMessageEnd?.(bufferObj as MessageEnd) + } + else if (bufferObj.event === 'message_replace') { + onMessageReplace?.(bufferObj as MessageReplace) + } } - catch (e) { - // mute handle message cut off - onData('', isFirstMessage, { - conversationId: bufferObj?.conversation_id, - messageId: bufferObj?.id, - }) - return - } - if (bufferObj.event !== 'message') - return - - onData(unicodeToChar(bufferObj.answer), isFirstMessage, { - conversationId: bufferObj.conversation_id, - messageId: bufferObj.id, - }) - isFirstMessage = false }) buffer = lines[lines.length - 1] } @@ -92,10 +134,12 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted messageId: '', errorMessage: `${e}`, }) + hasError = true + onCompleted?.(true) return } - - read() + if (!hasError) + read() }) } read() diff --git a/service/index.ts b/service/index.ts index 8028e0a..126dc07 100644 --- a/service/index.ts +++ b/service/index.ts @@ -1,18 +1,23 @@ -import type { IOnCompleted, IOnData, IOnError } from './base' +import type { IOnCompleted, IOnData, IOnError, IOnFile, IOnMessageEnd, IOnMessageReplace, IOnThought } from './base' import { get, post, ssePost } from './base' import type { Feedbacktype } from '@/types/app' -export const sendChatMessage = async (body: Record, { onData, onCompleted, onError }: { +export const sendChatMessage = async (body: Record, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }: { onData: IOnData onCompleted: IOnCompleted + onFile: IOnFile + onThought: IOnThought + onMessageEnd: IOnMessageEnd + onMessageReplace: IOnMessageReplace onError: IOnError + getAbortController?: (abortController: AbortController) => void }) => { return ssePost('chat-messages', { body: { ...body, response_mode: 'streaming', }, - }, { onData, onCompleted, onError }) + }, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }) } export const fetchConversations = async () => { From b62e34c6fa7c0b352ffcd6164ab543189ec8c492 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 29 Jan 2024 13:00:14 +0800 Subject: [PATCH 3/5] feat: support output agent info --- .../[conversationId]/name/route.ts | 20 ++ .../base/icons/public/data-set/index.tsx | 2 +- .../solid/general/check-circle/index.tsx | 2 +- app/components/index.tsx | 207 ++++++++++++++++-- hooks/use-conversation.ts | 4 +- package.json | 2 +- service/index.ts | 6 +- types/app.ts | 7 +- types/log.ts | 16 ++ 9 files changed, 239 insertions(+), 27 deletions(-) create mode 100644 app/api/conversations/[conversationId]/name/route.ts create mode 100644 types/log.ts diff --git a/app/api/conversations/[conversationId]/name/route.ts b/app/api/conversations/[conversationId]/name/route.ts new file mode 100644 index 0000000..0b7cffe --- /dev/null +++ b/app/api/conversations/[conversationId]/name/route.ts @@ -0,0 +1,20 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { client, getInfo } from '@/app/api/utils/common' + +export async function POST(request: NextRequest, { params }: { + params: { conversationId: string } +}) { + const body = await request.json() + const { + auto_generate, + name, + } = body + const { conversationId } = params + const { user } = getInfo(request) + + // auto generate name + const { data } = await client.renameConversation(conversationId, name, user, auto_generate) + console.log(conversationId, name, user, auto_generate) + return NextResponse.json(data) +} diff --git a/app/components/base/icons/public/data-set/index.tsx b/app/components/base/icons/public/data-set/index.tsx index cadbbdc..fa2acc3 100644 --- a/app/components/base/icons/public/data-set/index.tsx +++ b/app/components/base/icons/public/data-set/index.tsx @@ -2,7 +2,7 @@ // DON NOT EDIT IT MANUALLY import * as React from 'react' -import data from './DataSet.json' +import data from './data.json' import IconBase from '@/app/components/base/icons/IconBase' import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' diff --git a/app/components/base/icons/solid/general/check-circle/index.tsx b/app/components/base/icons/solid/general/check-circle/index.tsx index fe2cbfc..07a0ff1 100644 --- a/app/components/base/icons/solid/general/check-circle/index.tsx +++ b/app/components/base/icons/solid/general/check-circle/index.tsx @@ -2,7 +2,7 @@ // DON NOT EDIT IT MANUALLY import * as React from 'react' -import data from './CheckCircle.json' +import data from './data.json' import IconBase from '@/app/components/base/icons/IconBase' import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' diff --git a/app/components/index.tsx b/app/components/index.tsx index cc65547..9cc3f41 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -3,16 +3,16 @@ import type { FC } from 'react' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import produce from 'immer' +import produce, { setAutoFreeze } from 'immer' import { useBoolean, useGetState } from 'ahooks' import useConversation from '@/hooks/use-conversation' import Toast from '@/app/components/base/toast' import Sidebar from '@/app/components/sidebar' import ConfigSence from '@/app/components/config-scence' import Header from '@/app/components/header' -import { fetchAppParams, fetchChatList, fetchConversations, sendChatMessage, updateFeedback } from '@/service' +import { fetchAppParams, fetchChatList, fetchConversations, generationConversationName, sendChatMessage, updateFeedback } from '@/service' import type { ConversationItem, Feedbacktype, IChatItem, PromptConfig, VisionFile, VisionSettings } from '@/types/app' -import { TransferMethod } from '@/types/app' +import { Resolution, TransferMethod } from '@/types/app' import Chat from '@/app/components/chat' import { setLocaleOnClient } from '@/i18n/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -20,6 +20,7 @@ import Loading from '@/app/components/base/loading' import { replaceVarWithValues, userInputsFormToPromptVariables } from '@/utils/prompt' import AppUnavailable from '@/app/components/app-unavailable' import { API_KEY, APP_ID, APP_INFO, isShowPrompt, promptTemplate } from '@/config' +import type { Annotation as AnnotationType } from '@/types/log' const Main: FC = () => { const { t } = useTranslation() @@ -36,13 +37,26 @@ const Main: FC = () => { const [inited, setInited] = useState(false) // in mobile, show sidebar by click button const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) - const [visionConfig, setVisionConfig] = useState(undefined) + const [visionConfig, setVisionConfig] = useState({ + enabled: false, + number_limits: 2, + detail: Resolution.low, + transfer_methods: [TransferMethod.local_file], + }) useEffect(() => { if (APP_INFO?.title) document.title = `${APP_INFO.title} - Powered by Dify` }, [APP_INFO?.title]) + // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 + useEffect(() => { + setAutoFreeze(false) + return () => { + setAutoFreeze(true) + } + }, []) + /* * conversation info */ @@ -50,6 +64,7 @@ const Main: FC = () => { conversationList, setConversationList, currConversationId, + getCurrConversationId, setCurrConversationId, getConversationIdFromStorage, isNewConversation, @@ -244,6 +259,7 @@ const Main: FC = () => { }, []) const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) + const [abortController, setAbortController] = useState(null) const { notify } = Toast const logError = (message: string) => { notify({ type: 'error', message }) @@ -267,6 +283,36 @@ const Main: FC = () => { return true } + const [controlFocus, setControlFocus] = useState(0) + const [openingSuggestedQuestions, setOpeningSuggestedQuestions] = useState([]) + const [messageTaskId, setMessageTaskId] = useState('') + const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false) + const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true) + const [userQuery, setUserQuery] = useState('') + + const updateCurrentQA = ({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }: { + responseItem: IChatItem + questionId: string + placeholderAnswerId: string + questionItem: IChatItem + }) => { + // closesure new list is outdated. + const newListWithAnswer = produce( + getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), + (draft) => { + if (!draft.find(item => item.id === questionId)) + draft.push({ ...questionItem }) + + draft.push({ ...responseItem }) + }) + setChatList(newListWithAnswer) + } + const handleSend = async (message: string, files?: VisionFile[]) => { if (isResponsing) { notify({ type: 'info', message: t('app.errorMessage.waitForResponse') }) @@ -309,23 +355,145 @@ const Main: FC = () => { const newList = [...getChatList(), questionItem, placeholderAnswerItem] setChatList(newList) + let isAgentMode = false + // answer - const responseItem = { + const responseItem: IChatItem = { id: `${Date.now()}`, content: '', + agent_thoughts: [], + message_files: [], isAnswer: true, } + let hasSetResponseId = false + const prevTempNewConversationId = getCurrConversationId() || '-1' let tempNewConversationId = '' + setResponsingTrue() sendChatMessage(data, { - onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId }: any) => { - responseItem.content = responseItem.content + message - responseItem.id = messageId + getAbortController: (abortController) => { + setAbortController(abortController) + }, + onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { + if (!isAgentMode) { + responseItem.content = responseItem.content + message + } + else { + const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] + if (lastThought) + lastThought.thought = lastThought.thought + message // need immer setAutoFreeze + } + if (messageId && !hasSetResponseId) { + responseItem.id = messageId + hasSetResponseId = true + } + if (isFirstMessage && newConversationId) tempNewConversationId = newConversationId - // closesure new list is outdated. + setMessageTaskId(taskId) + // has switched to other conversation + if (prevTempNewConversationId !== getCurrConversationId()) { + setIsResponsingConCurrCon(false) + return + } + updateCurrentQA({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }) + }, + async onCompleted(hasError?: boolean) { + if (hasError) + return + + if (getConversationIdChangeBecauseOfNew()) { + const { data: allConversations }: any = await fetchConversations() + const newItem: any = await generationConversationName(allConversations[0].id) + + const newAllConversations = produce(allConversations, (draft: any) => { + draft[0].name = newItem.name + }) + setConversationList(newAllConversations as any) + } + setConversationIdChangeBecauseOfNew(false) + resetNewConversationInputs() + setChatNotStarted() + setCurrConversationId(tempNewConversationId, APP_ID, true) + setResponsingFalse() + }, + onFile(file) { + const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] + if (lastThought) + lastThought.message_files = [...(lastThought as any).message_files, { ...file }] + + updateCurrentQA({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }) + }, + onThought(thought) { + isAgentMode = true + const response = responseItem as any + if (thought.message_id && !hasSetResponseId) { + response.id = thought.message_id + hasSetResponseId = true + } + // responseItem.id = thought.message_id; + if (response.agent_thoughts.length === 0) { + response.agent_thoughts.push(thought) + } + else { + const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1] + // thought changed but still the same thought, so update. + if (lastThought.id === thought.id) { + thought.thought = lastThought.thought + thought.message_files = lastThought.message_files + responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought + } + else { + responseItem.agent_thoughts!.push(thought) + } + } + // has switched to other conversation + if (prevTempNewConversationId !== getCurrConversationId()) { + setIsResponsingConCurrCon(false) + return false + } + + updateCurrentQA({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }) + }, + onMessageEnd: (messageEnd) => { + if (messageEnd.metadata?.annotation_reply) { + responseItem.id = messageEnd.id + responseItem.annotation = ({ + id: messageEnd.metadata.annotation_reply.id, + authorName: messageEnd.metadata.annotation_reply.account.name, + } as AnnotationType) + const newListWithAnswer = produce( + getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), + (draft) => { + if (!draft.find(item => item.id === questionId)) + draft.push({ ...questionItem }) + + draft.push({ + ...responseItem, + }) + }) + setChatList(newListWithAnswer) + return + } + // not support show citation + // responseItem.citation = messageEnd.retriever_resources const newListWithAnswer = produce( getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), (draft) => { @@ -336,19 +504,16 @@ const Main: FC = () => { }) setChatList(newListWithAnswer) }, - async onCompleted() { - setResponsingFalse() - if (!tempNewConversationId) - return + onMessageReplace: (messageReplace) => { + setChatList(produce( + getChatList(), + (draft) => { + const current = draft.find(item => item.id === messageReplace.id) - if (getConversationIdChangeBecauseOfNew()) { - const { data: conversations }: any = await fetchConversations() - setConversationList(conversations as ConversationItem[]) - } - setConversationIdChangeBecauseOfNew(false) - resetNewConversationInputs() - setChatNotStarted() - setCurrConversationId(tempNewConversationId, APP_ID, true) + if (current) + current.content = messageReplace.answer + }, + )) }, onError() { setResponsingFalse() diff --git a/hooks/use-conversation.ts b/hooks/use-conversation.ts index e24e6da..6a9c4ad 100644 --- a/hooks/use-conversation.ts +++ b/hooks/use-conversation.ts @@ -1,5 +1,6 @@ import { useState } from 'react' import produce from 'immer' +import { useGetState } from 'ahooks' import type { ConversationItem } from '@/types/app' const storageConversationIdKey = 'conversationIdInfo' @@ -7,7 +8,7 @@ const storageConversationIdKey = 'conversationIdInfo' type ConversationInfoType = Omit function useConversation() { const [conversationList, setConversationList] = useState([]) - const [currConversationId, doSetCurrConversationId] = useState('-1') + const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState('-1') // when set conversation id, we do not have set appId const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { doSetCurrConversationId(id) @@ -50,6 +51,7 @@ function useConversation() { conversationList, setConversationList, currConversationId, + getCurrConversationId, setCurrConversationId, getConversationIdFromStorage, isNewConversation, diff --git a/package.json b/package.json index 809eef3..a8666e1 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "axios": "^1.3.5", "classnames": "^2.3.2", "copy-to-clipboard": "^3.3.3", - "dify-client": "^2.1.0", + "dify-client": "^2.2.0", "eslint": "8.36.0", "eslint-config-next": "13.4.0", "eventsource-parser": "^1.0.0", diff --git a/service/index.ts b/service/index.ts index 126dc07..9053515 100644 --- a/service/index.ts +++ b/service/index.ts @@ -21,7 +21,7 @@ export const sendChatMessage = async (body: Record, { onData, onCom } export const fetchConversations = async () => { - return get('conversations', { params: { limit: 20, first_id: '' } }) + return get('conversations', { params: { limit: 100, first_id: '' } }) } export const fetchChatList = async (conversationId: string) => { @@ -36,3 +36,7 @@ export const fetchAppParams = async () => { export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => { return post(url, { body }) } + +export const generationConversationName = async (id: string) => { + return post(`conversations/${id}/name`, { body: { auto_generate: true } }) +} diff --git a/types/app.ts b/types/app.ts index 93d9691..c7593cb 100644 --- a/types/app.ts +++ b/types/app.ts @@ -1,4 +1,6 @@ +import type { Annotation } from './log' import type { Locale } from '@/i18n' +import type { ThoughtItem } from '@/app/components/chat/type' export type PromptVariable = { key: string @@ -74,9 +76,12 @@ export type IChatItem = { * More information about this message */ more?: MessageMore - isIntroduction?: boolean + annotation?: Annotation useCurrentUserAvatar?: boolean isOpeningStatement?: boolean + suggestedQuestions?: string[] + log?: { role: string; text: string }[] + agent_thoughts?: ThoughtItem[] message_files?: VisionFile[] } diff --git a/types/log.ts b/types/log.ts new file mode 100644 index 0000000..cae5da4 --- /dev/null +++ b/types/log.ts @@ -0,0 +1,16 @@ +export type LogAnnotation = { + content: string + account: { + id: string + name: string + email: string + } + created_at: number +} + +export type Annotation = { + id: string + authorName: string + logAnnotation?: LogAnnotation + created_at?: number +} From 83695999eaa0a663cf33463f0a8265a36abefcea Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 29 Jan 2024 13:33:15 +0800 Subject: [PATCH 4/5] feat: conversation support show thought --- .../[conversationId]/name/route.ts | 1 - app/components/index.tsx | 6 ++++- utils/tools.ts | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 utils/tools.ts diff --git a/app/api/conversations/[conversationId]/name/route.ts b/app/api/conversations/[conversationId]/name/route.ts index 0b7cffe..d9dd327 100644 --- a/app/api/conversations/[conversationId]/name/route.ts +++ b/app/api/conversations/[conversationId]/name/route.ts @@ -15,6 +15,5 @@ export async function POST(request: NextRequest, { params }: { // auto generate name const { data } = await client.renameConversation(conversationId, name, user, auto_generate) - console.log(conversationId, name, user, auto_generate) return NextResponse.json(data) } diff --git a/app/components/index.tsx b/app/components/index.tsx index 9cc3f41..111e81d 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -21,6 +21,7 @@ import { replaceVarWithValues, userInputsFormToPromptVariables } from '@/utils/p import AppUnavailable from '@/app/components/app-unavailable' import { API_KEY, APP_ID, APP_INFO, isShowPrompt, promptTemplate } from '@/config' import type { Annotation as AnnotationType } from '@/types/log' +import { addFileInfos, sortAgentSorts } from '@/utils/tools' const Main: FC = () => { const { t } = useTranslation() @@ -130,13 +131,16 @@ const Main: FC = () => { id: `question-${item.id}`, content: item.query, isAnswer: false, - message_files: item.message_files, + message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], + }) newChatList.push({ id: item.id, content: item.answer, + agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files), feedback: item.feedback, isAnswer: true, + message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], }) }) setChatList(newChatList) diff --git a/utils/tools.ts b/utils/tools.ts new file mode 100644 index 0000000..a6ecb93 --- /dev/null +++ b/utils/tools.ts @@ -0,0 +1,26 @@ +import type { ThoughtItem } from '@/app/components/chat/type' +import type { VisionFile } from '@/types/app' + +export const sortAgentSorts = (list: ThoughtItem[]) => { + if (!list) + return list + if (list.some(item => item.position === undefined)) + return list + const temp = [...list] + temp.sort((a, b) => a.position - b.position) + return temp +} + +export const addFileInfos = (list: ThoughtItem[], messageFiles: VisionFile[]) => { + if (!list || !messageFiles) + return list + return list.map((item) => { + if (item.files && item.files?.length > 0) { + return { + ...item, + message_files: item.files.map(fileId => messageFiles.find(file => file.id === fileId)) as VisionFile[], + } + } + return item + }) +} From 25ba4ac4766c1f2d90cf0660781adc959a2ad879 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 29 Jan 2024 16:03:50 +0800 Subject: [PATCH 5/5] feat: show agent show --- service/base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/base.ts b/service/base.ts index 16dd112..bd37f3b 100644 --- a/service/base.ts +++ b/service/base.ts @@ -258,7 +258,7 @@ export const upload = (fetchOptions: any): Promise => { }) } -export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, onError }: IOtherOptions) => { +export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, onThought, onFile, onMessageEnd, onMessageReplace, onError }: IOtherOptions) => { const options = Object.assign({}, baseOptions, { method: 'POST', }, fetchOptions) @@ -290,7 +290,7 @@ export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, o onData?.(str, isFirstMessage, moreInfo) }, () => { onCompleted?.() - }) + }, onThought, onMessageEnd, onMessageReplace, onFile) }).catch((e) => { Toast.notify({ type: 'error', message: e }) onError?.(e)