From 35a06c3cbe7651e9a1cbde2bae91a43fdb12f848 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 6 Jan 2026 15:03:40 +0800 Subject: [PATCH 1/2] feat(web): ai prompt api support stream --- web/src/api/prompt.ts | 5 +- .../components/AiPromptModal.tsx | 72 ++++++++++----- .../components/Editor/index.tsx | 91 +++++++++++++++++++ .../Editor/plugin/InitialValuePlugin.tsx | 25 +++++ .../Editor/plugin/InsertTextPlugin.tsx | 24 +++++ .../Editor/plugin/LineBreakPlugin.tsx | 24 +++++ web/src/views/Workflow/constant.ts | 3 +- 7 files changed, 218 insertions(+), 26 deletions(-) create mode 100644 web/src/views/ApplicationConfig/components/Editor/index.tsx create mode 100644 web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx create mode 100644 web/src/views/ApplicationConfig/components/Editor/plugin/InsertTextPlugin.tsx create mode 100644 web/src/views/ApplicationConfig/components/Editor/plugin/LineBreakPlugin.tsx diff --git a/web/src/api/prompt.ts b/web/src/api/prompt.ts index 77ea1271..526f50ac 100644 --- a/web/src/api/prompt.ts +++ b/web/src/api/prompt.ts @@ -1,5 +1,6 @@ import { request } from '@/utils/request' import type { AiPromptForm } from '@/views/ApplicationConfig/types' +import { handleSSE, type SSEMessage } from '@/utils/stream' export const createPromptSessions = () => { return request.post(`/prompt/sessions`) @@ -7,6 +8,6 @@ export const createPromptSessions = () => { export const getPrompt = (session_id: string) => { return request.get(`/prompt/sessions/${session_id}`) } -export const updatePromptMessages = (session_id: string, data: AiPromptForm) => { - return request.post(`/prompt/sessions/${session_id}/messages`, data) +export const updatePromptMessages = (session_id: string, data: AiPromptForm, onMessage?: (data: SSEMessage[]) => void) => { + return handleSSE(`/prompt/sessions/${session_id}/messages`, data, onMessage) } \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx index a85f5cf1..f52c0675 100644 --- a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx +++ b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx @@ -16,6 +16,8 @@ import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpt import type { ChatItem } from '@/components/Chat/types' import CustomSelect from '@/components/CustomSelect' import AiPromptVariableModal from './AiPromptVariableModal' +import { type SSEMessage } from '@/utils/stream' +import Editor from './Editor' interface AiPromptModalProps { refresh: (value: string) => void; @@ -35,7 +37,8 @@ const AiPromptModal = forwardRef(({ const [variables, setVariables] = useState([]) const [promptSession, setPromptSession] = useState(null) const aiPromptVariableModalRef = useRef(null) - const currentPromptRef = useRef(null) + const editorRef = useRef(null) + const currentPromptValueRef = useRef('') const values = Form.useWatch([], form) @@ -78,16 +81,45 @@ const AiPromptModal = forwardRef(({ setChatList(prev => { return [...prev, { role: 'user', content: messageContent}] }) - form.setFieldsValue({ message: undefined }) - updatePromptMessages(promptSession, values) - .then(res => { - const response = res as { prompt: string; desc: string; variables: string[] } - form.setFieldsValue({ current_prompt: response.prompt }) - setChatList(prev => { - return [...prev, { role: 'assistant', content: response.desc }] - }) - setVariables(response.variables) + form.setFieldsValue({ message: undefined, current_prompt: undefined }) + + const handleStreamMessage = (data: SSEMessage[]) => { + data.map(item => { + const { content, desc, variables } = item.data as { content: string; desc: string; variables: string[] }; + + switch (item.event) { + case 'start': + currentPromptValueRef.current = '' + break; + case 'message': + if (content) { + currentPromptValueRef.current += content; + form.setFieldsValue({ current_prompt: currentPromptValueRef.current }) + } + if (desc) { + setChatList(prev => { + return [...prev, { role: 'assistant', content: desc }] + }) + } + if (variables) { + setVariables(variables) + } + break; + case 'end': + setLoading(false) + break + } }) + }; + updatePromptMessages(promptSession, values, handleStreamMessage) + // .then(res => { + // const response = res as { prompt: string; desc: string; variables: string[] } + // form.setFieldsValue({ current_prompt: response.prompt }) + // setChatList(prev => { + // return [...prev, { role: 'assistant', content: response.desc }] + // }) + // setVariables(response.variables) + // }) .finally(() => { setLoading(false) }) @@ -101,18 +133,8 @@ const AiPromptModal = forwardRef(({ aiPromptVariableModalRef.current?.handleOpen() } const handleVariableApply = (value: string) => { - const textArea = currentPromptRef.current?.resizableTextArea?.textArea - if (textArea) { - const cursorPosition = textArea.selectionStart - const currentValue = values.current_prompt || '' - const newValue = currentValue.slice(0, cursorPosition) + value + currentValue.slice(cursorPosition) - form.setFieldValue('current_prompt', newValue) - - // 设置新的光标位置 - setTimeout(() => { - textArea.focus() - textArea.setSelectionRange(cursorPosition + value.length, cursorPosition + value.length) - }, 0) + if (editorRef.current?.insertText) { + editorRef.current.insertText(value) } else { form.setFieldValue('current_prompt', (values.current_prompt || '') + value) } @@ -191,7 +213,11 @@ const AiPromptModal = forwardRef(({ - + form.setFieldValue('current_prompt', value)} + />
diff --git a/web/src/views/ApplicationConfig/components/Editor/index.tsx b/web/src/views/ApplicationConfig/components/Editor/index.tsx new file mode 100644 index 00000000..d381e003 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Editor/index.tsx @@ -0,0 +1,91 @@ +import {forwardRef, useImperativeHandle } from 'react'; +import clsx from 'clsx'; +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { $getSelection } from 'lexical'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import InitialValuePlugin from './plugin/InitialValuePlugin' +import LineBreakPlugin from './plugin/LineBreakPlugin'; +import InsertTextPlugin from './plugin/InsertTextPlugin'; + +export interface EditorRef { + insertText: (text: string) => void; +} + +interface LexicalEditorProps { + className?: string; + placeholder?: string; + value?: string; + onChange?: (value: string) => void; + height?: number; +} + +const theme = { + paragraph: 'editor-paragraph', + text: { + bold: 'editor-text-bold', + italic: 'editor-text-italic', + }, +}; + +const EditorContent = forwardRef(({ + className = '', + value, + placeholder = "请输入内容...", + onChange, +}, ref) => { + const [editor] = useLexicalComposerContext(); + + useImperativeHandle(ref, () => ({ + insertText: (text: string) => { + editor.update(() => { + const selection = $getSelection(); + if (selection) { + selection.insertText(text); + } + }); + } + }), [editor]); + + return ( +
+ + } + placeholder={ +
+ {placeholder} +
+ } + ErrorBoundary={LexicalErrorBoundary} + /> + + + +
+ ); +}); + +const Editor = forwardRef((props, ref) => { + const initialConfig = { + namespace: 'Editor', + theme, + nodes: [], + onError: (error: Error) => { + console.error(error); + }, + }; + + return ( + + + + ); +}); + +export default Editor; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx new file mode 100644 index 00000000..b1054055 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx @@ -0,0 +1,25 @@ +import { type FC, useEffect } from 'react'; +import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; + +// 设置初始值的插件 +const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (value) { + editor.update(() => { + const root = $getRoot(); + root.clear(); + const paragraph = $createParagraphNode(); + const textNode = $createTextNode(value); + paragraph.append(textNode); + root.append(paragraph); + }); + } + }, [editor, value]); + + return null; +}; + +export default InitialValuePlugin \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Editor/plugin/InsertTextPlugin.tsx b/web/src/views/ApplicationConfig/components/Editor/plugin/InsertTextPlugin.tsx new file mode 100644 index 00000000..ca75c393 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Editor/plugin/InsertTextPlugin.tsx @@ -0,0 +1,24 @@ +import { forwardRef, useImperativeHandle } from 'react'; +import { $getSelection } from 'lexical'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import type { EditorRef } from '../index' + +// 插入文本的插件 +const InsertTextPlugin = forwardRef((_, ref) => { + const [editor] = useLexicalComposerContext(); + + useImperativeHandle(ref, () => ({ + insertText: (text: string) => { + editor.update(() => { + const selection = $getSelection(); + if (selection) { + selection.insertText(text); + } + }); + } + }), [editor]); + + return null; +}); + +export default InsertTextPlugin; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Editor/plugin/LineBreakPlugin.tsx b/web/src/views/ApplicationConfig/components/Editor/plugin/LineBreakPlugin.tsx new file mode 100644 index 00000000..63d1ffc4 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Editor/plugin/LineBreakPlugin.tsx @@ -0,0 +1,24 @@ +import { type FC, useEffect } from 'react'; +import { $getRoot } from 'lexical'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; + +// 处理换行的插件 +const LineBreakPlugin: FC<{ onChange?: (value: string) => void }> = ({ onChange }) => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const root = $getRoot(); + const textContent = root.getTextContent(); + // 将\n转换为实际换行 + const processedContent = textContent.replace(/\\n/g, '\n'); + onChange?.(processedContent); + }); + }); + }, [editor, onChange]); + + return null; +}; + +export default LineBreakPlugin; \ No newline at end of file diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index c398eb70..babc8614 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -394,7 +394,8 @@ export const nodeLibrary: NodeLibrary[] = [ defaultValue: {} }, retry: { - type: 'define', + type: 'switch', + defaultValue: false }, error_handle: { type: 'define', From 9d0622b6cc0b8631a397dcc866613fba89a6895e Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 6 Jan 2026 15:36:25 +0800 Subject: [PATCH 2/2] feat(web): user summary api update --- web/src/i18n/en.ts | 2 + web/src/i18n/zh.ts | 2 + .../UserMemoryDetail/components/AboutMe.tsx | 40 ++++++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index d186368b..630c6c7e 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1218,6 +1218,8 @@ export const en = { key_findings: 'Key Findings', behavior_pattern: 'Behavior Pattern', growth_trajectory: 'Growth Trajectory', + personality: 'Personality Traits', + core_values: 'Core Values', }, space: { createSpace: 'Create Space', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 028bd1df..b50ed1d8 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1299,6 +1299,8 @@ export const zh = { key_findings: '关键发现', behavior_pattern: '行为模式', growth_trajectory: '成长轨迹', + personality: '性格特点', + core_values: '核心价值观', }, space: { createSpace: '创建空间', diff --git a/web/src/views/UserMemoryDetail/components/AboutMe.tsx b/web/src/views/UserMemoryDetail/components/AboutMe.tsx index ba7e68fe..f2c94814 100644 --- a/web/src/views/UserMemoryDetail/components/AboutMe.tsx +++ b/web/src/views/UserMemoryDetail/components/AboutMe.tsx @@ -5,16 +5,25 @@ import { Skeleton } from 'antd'; import RbCard from '@/components/RbCard/Card' import Empty from '@/components/Empty'; +import RbAlert from '@/components/RbAlert'; import { getUserSummary, } from '@/api/memory' import type { AboutMeRef } from '../types' + +interface Data { + user_summary: string; + personality: string; + core_values: string; + one_sentence: string; + [key: string]: string; +} const AboutMe = forwardRef((_props, ref) => { const { t } = useTranslation() const { id } = useParams() const [loading, setLoading] = useState(false) - const [data, setData] = useState(null) + const [data, setData] = useState({} as Data) useEffect(() => { if (!id) return @@ -27,7 +36,7 @@ const AboutMe = forwardRef((_props, ref) => { setLoading(true) getUserSummary(id) .then((res) => { - setData((res as { summary?: string }).summary || null) + setData((res as Data) || null) }) .finally(() => { setLoading(false) @@ -44,10 +53,29 @@ const AboutMe = forwardRef((_props, ref) => { > {loading ? - : data - ?
- {data || '-'} -
+ : Object.keys(data).filter(key => data[key] !== null).length > 0 + ? <> + {data.user_summary && +
+ {data.user_summary} +
+ } + {data.personality && <> +
{t('userMemory.personality')}
+
+ {data.personality} +
+ } + {data.core_values && <> +
{t('userMemory.core_values')}
+
+ {data.core_values} +
+ } + {data.one_sentence && + {data.one_sentence} + } + : }