fix(web): prompt editor
This commit is contained in:
39
web/src/components/Chat/PromptChatPanel.tsx
Normal file
39
web/src/components/Chat/PromptChatPanel.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { forwardRef, useImperativeHandle, useState } from 'react'
|
||||||
|
import ChatContent from './ChatContent'
|
||||||
|
import type { ChatItem } from './types'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface PromptChatPanelRef {
|
||||||
|
append: (item: ChatItem) => void
|
||||||
|
clear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromptChatPanelProps {
|
||||||
|
classNames?: string
|
||||||
|
contentClassNames?: string
|
||||||
|
empty: ReactNode
|
||||||
|
labelFormat: (item: ChatItem) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
const PromptChatPanel = forwardRef<PromptChatPanelRef, PromptChatPanelProps>((props, ref) => {
|
||||||
|
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
append: (item) => setChatList(prev => [...prev, item]),
|
||||||
|
clear: () => setChatList([]),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatContent
|
||||||
|
classNames={props.classNames}
|
||||||
|
contentClassNames={props.contentClassNames}
|
||||||
|
empty={props.empty}
|
||||||
|
data={chatList}
|
||||||
|
streamLoading={false}
|
||||||
|
labelPosition="top"
|
||||||
|
labelFormat={props.labelFormat}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PromptChatPanel
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:26:44
|
* @Date: 2026-02-03 16:26:44
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-20 13:53:05
|
* @Last Modified time: 2026-04-15 14:21:55
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* AI Prompt Assistant Modal
|
* AI Prompt Assistant Modal
|
||||||
@@ -20,10 +20,9 @@ import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
|
|||||||
import type { AiPromptModalRef, AiPromptVariableModalRef, AiPromptForm } from '../types'
|
import type { AiPromptModalRef, AiPromptVariableModalRef, AiPromptForm } from '../types'
|
||||||
import RbModal from '@/components/RbModal'
|
import RbModal from '@/components/RbModal'
|
||||||
import type { Model } from '@/views/ModelManagement/types'
|
import type { Model } from '@/views/ModelManagement/types'
|
||||||
import ChatContent from '@/components/Chat/ChatContent'
|
|
||||||
import Empty from '@/components/Empty'
|
import Empty from '@/components/Empty'
|
||||||
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
||||||
import type { ChatItem } from '@/components/Chat/types'
|
import PromptChatPanel, { type PromptChatPanelRef } from '@/components/Chat/PromptChatPanel'
|
||||||
import ModelSelect from '@/components/ModelSelect'
|
import ModelSelect from '@/components/ModelSelect'
|
||||||
import AiPromptVariableModal from './AiPromptVariableModal'
|
import AiPromptVariableModal from './AiPromptVariableModal'
|
||||||
import { type SSEMessage } from '@/utils/stream'
|
import { type SSEMessage } from '@/utils/stream'
|
||||||
@@ -55,12 +54,14 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [form] = Form.useForm<AiPromptForm>()
|
const [form] = Form.useForm<AiPromptForm>()
|
||||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
|
||||||
const [variables, setVariables] = useState<string[]>([])
|
const [variables, setVariables] = useState<string[]>([])
|
||||||
const [promptSession, setPromptSession] = useState<string | null>(null)
|
const [promptSession, setPromptSession] = useState<string | null>(null)
|
||||||
|
const [hasPrompt, setHasPrompt] = useState(false)
|
||||||
const aiPromptVariableModalRef = useRef<AiPromptVariableModalRef>(null)
|
const aiPromptVariableModalRef = useRef<AiPromptVariableModalRef>(null)
|
||||||
|
const chatPanelRef = useRef<PromptChatPanelRef>(null)
|
||||||
const editorRef = useRef<any>(null)
|
const editorRef = useRef<any>(null)
|
||||||
const currentPromptValueRef = useRef<string>('')
|
const currentPromptValueRef = useRef<string>('')
|
||||||
|
const isStreamingRef = useRef(false)
|
||||||
|
|
||||||
const values = Form.useWatch([], form)
|
const values = Form.useWatch([], form)
|
||||||
|
|
||||||
@@ -68,12 +69,13 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setChatList([])
|
chatPanelRef.current?.clear()
|
||||||
setVariables([])
|
setVariables([])
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
message: undefined,
|
message: undefined,
|
||||||
current_prompt: undefined,
|
current_prompt: undefined,
|
||||||
})
|
})
|
||||||
|
setHasPrompt(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Open modal and create new prompt session */
|
/** Open modal and create new prompt session */
|
||||||
@@ -102,9 +104,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
}
|
}
|
||||||
const messageContent = values.message
|
const messageContent = values.message
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setChatList(prev => {
|
chatPanelRef.current?.append({ role: 'user', content: messageContent })
|
||||||
return [...prev, { role: 'user', content: messageContent}]
|
|
||||||
})
|
|
||||||
form.setFieldsValue({ message: undefined, current_prompt: undefined })
|
form.setFieldsValue({ message: undefined, current_prompt: undefined })
|
||||||
|
|
||||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||||
@@ -114,6 +114,8 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
switch (item.event) {
|
switch (item.event) {
|
||||||
case 'start':
|
case 'start':
|
||||||
currentPromptValueRef.current = ''
|
currentPromptValueRef.current = ''
|
||||||
|
isStreamingRef.current = true
|
||||||
|
setHasPrompt(true)
|
||||||
if (editorRef.current?.clear) {
|
if (editorRef.current?.clear) {
|
||||||
editorRef.current.clear();
|
editorRef.current.clear();
|
||||||
}
|
}
|
||||||
@@ -123,15 +125,12 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
currentPromptValueRef.current += content;
|
currentPromptValueRef.current += content;
|
||||||
if (editorRef.current?.appendText) {
|
if (editorRef.current?.appendText) {
|
||||||
editorRef.current.appendText(content);
|
editorRef.current.appendText(content);
|
||||||
editorRef.current.scrollToBottom();
|
|
||||||
} else {
|
} else {
|
||||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (desc) {
|
if (desc) {
|
||||||
setChatList(prev => {
|
chatPanelRef.current?.append({ role: 'assistant', content: desc })
|
||||||
return [...prev, { role: 'assistant', content: desc }]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (variables) {
|
if (variables) {
|
||||||
setVariables(variables)
|
setVariables(variables)
|
||||||
@@ -139,6 +138,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
break;
|
break;
|
||||||
case 'end':
|
case 'end':
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
isStreamingRef.current = false
|
||||||
// Sync form value when stream ends
|
// Sync form value when stream ends
|
||||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||||
break
|
break
|
||||||
@@ -193,7 +193,6 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
setIsFocus(false)
|
setIsFocus(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
return (
|
return (
|
||||||
<RbModal
|
<RbModal
|
||||||
title={t(`${source}.AIPromptAssistant`)}
|
title={t(`${source}.AIPromptAssistant`)}
|
||||||
@@ -207,7 +206,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
body: 'rb:p-0! rb:border-t rb:border-t-[#EBEBEB]'
|
body: 'rb:p-0! rb:border-t rb:border-t-[#EBEBEB]'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form form={form} className="rb:mx-4!">
|
<Form form={form} className="rb:mx-4! rb:h-[calc(100vh-202px)]">
|
||||||
<div className="rb:grid rb:grid-cols-2">
|
<div className="rb:grid rb:grid-cols-2">
|
||||||
<div className="rb:border-r rb:border-r-[#EBEBEB] rb:pr-4 rb:pt-3 rb:pb-4">
|
<div className="rb:border-r rb:border-r-[#EBEBEB] rb:pr-4 rb:pt-3 rb:pb-4">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -220,13 +219,11 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<ChatContent
|
<PromptChatPanel
|
||||||
classNames="rb:h-105.5 rb:pb-[15px]!"
|
ref={chatPanelRef}
|
||||||
|
classNames="rb:h-[calc(100vh-330px)] rb:pb-[15px]!"
|
||||||
contentClassNames="rb:max-w-75!"
|
contentClassNames="rb:max-w-75!"
|
||||||
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
|
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
|
||||||
data={chatList || []}
|
|
||||||
streamLoading={false}
|
|
||||||
labelPosition="top"
|
|
||||||
labelFormat={(item) => item.role === 'user' ? t(`${source}.you`) : t(`${source}.ai`)}
|
labelFormat={(item) => item.role === 'user' ? t(`${source}.you`) : t(`${source}.ai`)}
|
||||||
/>
|
/>
|
||||||
<Flex align="center" gap={12} justify="space-between"
|
<Flex align="center" gap={12} justify="space-between"
|
||||||
@@ -288,16 +285,21 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Form.Item name="current_prompt" noStyle>
|
{hasPrompt
|
||||||
{values?.current_prompt
|
? <Form.Item name="current_prompt" noStyle>
|
||||||
? <Editor
|
<Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className="rb:h-119 rb:bg-white! rb:border-none! rb:p-0!"
|
height="rb:h-[calc(100vh-276px)]"
|
||||||
onChange={(value) => form.setFieldValue('current_prompt', value)}
|
className={clsx('rb:bg-white! rb:border-none! rb:p-0!')}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!isStreamingRef.current) {
|
||||||
|
form.setFieldValue('current_prompt', value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
: <Empty url={analysisEmptyIcon} title={t(`${source}.promptOptimizationEmpty`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-119 rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
|
</Form.Item>
|
||||||
}
|
: <Empty url={analysisEmptyIcon} title={t(`${source}.promptOptimizationEmpty`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-119 rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
|
||||||
</Form.Item>
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:25:17
|
* @Date: 2026-02-03 16:25:17
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-26 11:18:04
|
* @Last Modified time: 2026-04-15 14:00:07
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Rich text editor component using Lexical framework
|
* Rich text editor component using Lexical framework
|
||||||
* Provides text editing with insert, append, clear, and scroll capabilities
|
* Provides text editing with insert, append, clear, and scroll capabilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {forwardRef, useImperativeHandle } from 'react';
|
import {forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
@@ -50,7 +50,7 @@ interface LexicalEditorProps {
|
|||||||
/** Callback when content changes */
|
/** Callback when content changes */
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
/** Editor height in pixels */
|
/** Editor height in pixels */
|
||||||
height?: number;
|
height?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +73,42 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
|||||||
value,
|
value,
|
||||||
placeholder = "Please enter content...",
|
placeholder = "Please enter content...",
|
||||||
onChange,
|
onChange,
|
||||||
disabled
|
disabled,
|
||||||
|
height
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const pendingTextRef = useRef<string>('');
|
||||||
|
const rafRef = useRef<number | null>(null);
|
||||||
|
const isAppendingRef = useRef(false);
|
||||||
|
const scrollTopRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = scrollRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const onPointerDown = () => {
|
||||||
|
if (!isAppendingRef.current) scrollTopRef.current = el.scrollTop;
|
||||||
|
};
|
||||||
|
el.addEventListener('pointerdown', onPointerDown);
|
||||||
|
return () => el.removeEventListener('pointerdown', onPointerDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerUpdateListener(({ tags }) => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
if (tags.has('append-text')) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
} else {
|
||||||
|
scrollRef.current.scrollTop = scrollTopRef.current;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expose editor methods to parent component
|
* Expose editor methods to parent component
|
||||||
@@ -94,24 +127,33 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
appendText: (text: string) => {
|
appendText: (text: string) => {
|
||||||
editor.update(() => {
|
pendingTextRef.current += text;
|
||||||
const root = $getRoot();
|
if (rafRef.current !== null) return;
|
||||||
const lastChild = root.getLastChild();
|
rafRef.current = requestAnimationFrame(() => {
|
||||||
if (lastChild && $isParagraphNode(lastChild)) {
|
rafRef.current = null;
|
||||||
const lastTextNode = lastChild.getLastChild();
|
const batch = pendingTextRef.current;
|
||||||
if (lastTextNode && $isTextNode(lastTextNode)) {
|
pendingTextRef.current = '';
|
||||||
const currentText = lastTextNode.getTextContent();
|
if (scrollRef.current) scrollTopRef.current = scrollRef.current.scrollTop;
|
||||||
lastTextNode.setTextContent(currentText + text);
|
isAppendingRef.current = true;
|
||||||
|
editor.update(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
const lastChild = root.getLastChild();
|
||||||
|
if (lastChild && $isParagraphNode(lastChild)) {
|
||||||
|
const lastTextNode = lastChild.getLastChild();
|
||||||
|
if (lastTextNode && $isTextNode(lastTextNode)) {
|
||||||
|
lastTextNode.setTextContent(lastTextNode.getTextContent() + batch);
|
||||||
|
} else {
|
||||||
|
lastChild.append($createTextNode(batch));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const textNode = $createTextNode(text);
|
const paragraph = $createParagraphNode();
|
||||||
lastChild.append(textNode);
|
paragraph.append($createTextNode(batch));
|
||||||
|
root.append(paragraph);
|
||||||
}
|
}
|
||||||
} else {
|
}, {
|
||||||
const paragraph = $createParagraphNode();
|
tag: 'append-text',
|
||||||
const textNode = $createTextNode(text);
|
onUpdate: () => { isAppendingRef.current = false; }
|
||||||
paragraph.append(textNode);
|
});
|
||||||
root.append(paragraph);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
@@ -122,21 +164,16 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
|||||||
root.append(paragraph);
|
root.append(paragraph);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
scrollToBottom: () => {
|
scrollToBottom,
|
||||||
const editorElement = editor.getRootElement();
|
|
||||||
if (editorElement) {
|
|
||||||
editorElement.scrollTop = editorElement.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}), [editor]);
|
}), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div ref={scrollRef} style={{ position: 'relative' }} className={height ? `${height} rb:overflow-y-auto` : ''}>
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb-border rb:rounded-lg rb:overflow-auto",
|
"rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb-border rb:rounded-lg",
|
||||||
disabled && "rb:cursor-not-allowed rb:bg-[#F6F8FC] rb:text-[#5B6167]",
|
disabled && "rb:cursor-not-allowed rb:bg-[#F6F8FC] rb:text-[#5B6167]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 17:44:15
|
* @Date: 2026-02-03 17:44:15
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-27 15:14:58
|
* @Last Modified time: 2026-04-15 14:25:17
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Prompt Editor Component
|
* Prompt Editor Component
|
||||||
@@ -18,10 +18,9 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
|
|
||||||
import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
|
import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
|
||||||
import type { PromptVariableModalRef, AiPromptForm, HistoryItem, PromptSaveModalRef } from './types'
|
import type { PromptVariableModalRef, AiPromptForm, HistoryItem, PromptSaveModalRef } from './types'
|
||||||
import ChatContent from '@/components/Chat/ChatContent'
|
|
||||||
import Empty from '@/components/Empty'
|
import Empty from '@/components/Empty'
|
||||||
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
||||||
import type { ChatItem } from '@/components/Chat/types'
|
import PromptChatPanel, { type PromptChatPanelRef } from '@/components/Chat/PromptChatPanel'
|
||||||
import ModelSelect from '@/components/ModelSelect'
|
import ModelSelect from '@/components/ModelSelect'
|
||||||
import PromptVariableModal from './components/PromptVariableModal'
|
import PromptVariableModal from './components/PromptVariableModal'
|
||||||
import { type SSEMessage } from '@/utils/stream'
|
import { type SSEMessage } from '@/utils/stream'
|
||||||
@@ -39,13 +38,15 @@ const Prompt: FC = () => {
|
|||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [form] = Form.useForm<AiPromptForm>()
|
const [form] = Form.useForm<AiPromptForm>()
|
||||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
|
||||||
const [variables, setVariables] = useState<string[]>([])
|
const [variables, setVariables] = useState<string[]>([])
|
||||||
const [promptSession, setPromptSession] = useState<string | null>(null)
|
const [promptSession, setPromptSession] = useState<string | null>(null)
|
||||||
const aiPromptVariableModalRef = useRef<PromptVariableModalRef>(null)
|
const aiPromptVariableModalRef = useRef<PromptVariableModalRef>(null)
|
||||||
const promptSaveModalRef = useRef<PromptSaveModalRef>(null)
|
const promptSaveModalRef = useRef<PromptSaveModalRef>(null)
|
||||||
|
const chatPanelRef = useRef<PromptChatPanelRef>(null)
|
||||||
const editorRef = useRef<any>(null)
|
const editorRef = useRef<any>(null)
|
||||||
const currentPromptValueRef = useRef<string>(undefined)
|
const currentPromptValueRef = useRef<string>(undefined)
|
||||||
|
const isStreamingRef = useRef(false)
|
||||||
|
const [hasPrompt, setHasPrompt] = useState(false)
|
||||||
const values = Form.useWatch([], form)
|
const values = Form.useWatch([], form)
|
||||||
const [editVo, setEditVo] = useState<HistoryItem | null>(null)
|
const [editVo, setEditVo] = useState<HistoryItem | null>(null)
|
||||||
|
|
||||||
@@ -56,14 +57,14 @@ const Prompt: FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editVo?.id) {
|
if (editVo?.id) {
|
||||||
form.setFieldValue('current_prompt', editVo.prompt)
|
form.setFieldValue('current_prompt', editVo.prompt)
|
||||||
setChatList([])
|
setHasPrompt(true)
|
||||||
|
chatPanelRef.current?.clear()
|
||||||
}
|
}
|
||||||
updateSession()
|
updateSession()
|
||||||
}, [editVo])
|
}, [editVo])
|
||||||
|
|
||||||
/** Update session ID */
|
/** Update session ID */
|
||||||
const updateSession = () => {
|
const updateSession = () => {
|
||||||
console.log('updateSession')
|
|
||||||
createPromptSessions().then(res => {
|
createPromptSessions().then(res => {
|
||||||
const response = res as { id: string }
|
const response = res as { id: string }
|
||||||
setPromptSession(response.id)
|
setPromptSession(response.id)
|
||||||
@@ -83,9 +84,7 @@ const Prompt: FC = () => {
|
|||||||
}
|
}
|
||||||
const messageContent = values.message
|
const messageContent = values.message
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setChatList(prev => {
|
chatPanelRef.current?.append({ role: 'user', content: messageContent })
|
||||||
return [...prev, { role: 'user', content: messageContent}]
|
|
||||||
})
|
|
||||||
form.setFieldsValue({ message: undefined, current_prompt: undefined })
|
form.setFieldsValue({ message: undefined, current_prompt: undefined })
|
||||||
|
|
||||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||||
@@ -95,33 +94,35 @@ const Prompt: FC = () => {
|
|||||||
switch (item.event) {
|
switch (item.event) {
|
||||||
case 'start':
|
case 'start':
|
||||||
currentPromptValueRef.current = ''
|
currentPromptValueRef.current = ''
|
||||||
|
isStreamingRef.current = true
|
||||||
|
setHasPrompt(true)
|
||||||
if (editorRef.current?.clear) {
|
if (editorRef.current?.clear) {
|
||||||
editorRef.current.clear();
|
editorRef.current.clear();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'message':
|
case 'message':
|
||||||
if (typeof content === 'string') {
|
if (content) {
|
||||||
currentPromptValueRef.current += content;
|
currentPromptValueRef.current += content;
|
||||||
if (editorRef.current?.appendText) {
|
if (editorRef.current?.appendText) {
|
||||||
editorRef.current.appendText(content);
|
editorRef.current.appendText(content);
|
||||||
editorRef.current.scrollToBottom();
|
|
||||||
} else {
|
} else {
|
||||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (desc) {
|
if (desc) {
|
||||||
setChatList(prev => {
|
chatPanelRef.current?.append({ role: 'assistant', content: desc })
|
||||||
return [...prev, { role: 'assistant', content: desc }]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (variables) {
|
if (variables) {
|
||||||
setVariables(variables)
|
setVariables(variables)
|
||||||
}
|
}
|
||||||
|
console.log('currentPromptValueRef.current', currentPromptValueRef.current)
|
||||||
break;
|
break;
|
||||||
case 'end':
|
case 'end':
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
isStreamingRef.current = false
|
||||||
// Sync form values when stream ends
|
// Sync form values when stream ends
|
||||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||||
|
console.log('currentPromptValueRef.current', currentPromptValueRef.current)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -164,7 +165,8 @@ const Prompt: FC = () => {
|
|||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
form.setFieldValue('current_prompt', undefined)
|
form.setFieldValue('current_prompt', undefined)
|
||||||
currentPromptValueRef.current = undefined;
|
currentPromptValueRef.current = undefined;
|
||||||
setChatList([])
|
setHasPrompt(false)
|
||||||
|
chatPanelRef.current?.clear()
|
||||||
setEditVo(null)
|
setEditVo(null)
|
||||||
updateSession()
|
updateSession()
|
||||||
}
|
}
|
||||||
@@ -193,13 +195,11 @@ const Prompt: FC = () => {
|
|||||||
headerType="borderless"
|
headerType="borderless"
|
||||||
bodyClassName="rb:px-4! rb:pt-0! rb:pb-3!"
|
bodyClassName="rb:px-4! rb:pt-0! rb:pb-3!"
|
||||||
>
|
>
|
||||||
<ChatContent
|
<PromptChatPanel
|
||||||
|
ref={chatPanelRef}
|
||||||
classNames="rb:h-[calc(100vh-257px)] rb:mb-[12px]!"
|
classNames="rb:h-[calc(100vh-257px)] rb:mb-[12px]!"
|
||||||
contentClassNames="rb:max-w-75!"
|
contentClassNames="rb:max-w-75!"
|
||||||
empty={<Empty url={ConversationEmptyIcon} title={t(`prompt.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
|
empty={<Empty url={ConversationEmptyIcon} title={t(`prompt.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
|
||||||
data={chatList || []}
|
|
||||||
streamLoading={false}
|
|
||||||
labelPosition="top"
|
|
||||||
labelFormat={(item) => item.role === 'user' ? t(`prompt.you`) : t(`prompt.ai`)}
|
labelFormat={(item) => item.role === 'user' ? t(`prompt.you`) : t(`prompt.ai`)}
|
||||||
/>
|
/>
|
||||||
<Flex align="center" gap={12} justify="space-between"
|
<Flex align="center" gap={12} justify="space-between"
|
||||||
@@ -275,16 +275,21 @@ const Prompt: FC = () => {
|
|||||||
></Button>
|
></Button>
|
||||||
</Space>}
|
</Space>}
|
||||||
>
|
>
|
||||||
<Form.Item name="current_prompt" noStyle>
|
{hasPrompt
|
||||||
{values?.current_prompt
|
? <Form.Item name="current_prompt" noStyle>
|
||||||
? <Editor
|
<Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className="rb:h-[calc(100vh-193px)] rb:bg-white! rb:border-none! rb:p-0! rb:text-[#212332] rb:leading-5"
|
height="rb:h-[calc(100vh-193px)]"
|
||||||
onChange={(value) => form.setFieldValue('current_prompt', value)}
|
className="rb:bg-white! rb:border-none! rb:p-0! rb:text-[#212332] rb:leading-5"
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!isStreamingRef.current) {
|
||||||
|
form.setFieldValue('current_prompt', value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
: <Empty url={analysisEmptyIcon} title={t(`prompt.promptPlaceholder`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-[calc(100vh-193px)] rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
|
</Form.Item>
|
||||||
}
|
: <Empty url={analysisEmptyIcon} title={t(`prompt.promptPlaceholder`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-[calc(100vh-193px)] rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
|
||||||
</Form.Item>
|
}
|
||||||
</RbCard>
|
</RbCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user