From 29ffd0d810e3cbe0e1967dfda9b90eb0e0fa8819 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 25 Dec 2025 16:18:26 +0800 Subject: [PATCH] feat(web): Agent add ai prompt --- web/src/api/prompt.ts | 12 ++ web/src/i18n/en.ts | 9 + web/src/i18n/zh.ts | 9 + web/src/views/ApplicationConfig/Agent.tsx | 49 +++-- .../components/AiPromptModal.tsx | 202 +++++++++++++++--- .../components/AiPromptVariableModal.tsx | 107 ++++++++++ .../ApplicationConfig/components/Chat.tsx | 2 +- web/src/views/ApplicationConfig/types.ts | 11 +- 8 files changed, 348 insertions(+), 53 deletions(-) create mode 100644 web/src/api/prompt.ts create mode 100644 web/src/views/ApplicationConfig/components/AiPromptVariableModal.tsx diff --git a/web/src/api/prompt.ts b/web/src/api/prompt.ts new file mode 100644 index 00000000..77ea1271 --- /dev/null +++ b/web/src/api/prompt.ts @@ -0,0 +1,12 @@ +import { request } from '@/utils/request' +import type { AiPromptForm } from '@/views/ApplicationConfig/types' + +export const createPromptSessions = () => { + return request.post(`/prompt/sessions`) +} +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) +} \ No newline at end of file diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 585a191a..eacf0437 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -990,6 +990,15 @@ export const en = { apiKeyDeleteContent: 'Once deleted, it cannot be recovered, and applications using this Key will not be able to access the API', currentValue: 'Current Value', qpsLimitUnit: 'times/second', + addVariable: 'Insert Variable', + defineVariableName: 'Custom Variable Name', + defineVariableNamePlaceholder: 'Enter variable name, e.g.: user_name', + defineVariableNameExtra: 'Format: Variable name will be automatically wrapped with {{}}', + you: 'You', + ai: 'AI Assistant', + promptChatPlaceholder: 'Describe the prompt you need, e.g.: I need a customer service assistant', + promptChatEmpty: 'No conversation content available', + promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.', }, userMemory: { userMemory: 'User Memory', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index be429b84..43388b55 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -505,6 +505,15 @@ export const zh = { apiKeyDeleteContent: '删除后将无法恢复,使用此Key的应用将无法访问 API', currentValue: '当前值', qpsLimitUnit: '次/秒', + addVariable: '插入变量', + defineVariableName: '自定义变量名', + defineVariableNamePlaceholder: '输入变量名,例如:user_name', + defineVariableNameExtra: '格式:变量名会自动添加{{}}包裹', + you: '你', + ai: 'AI 助手', + promptChatPlaceholder: '描述你需要的提示词,例如:我需要一个客服助手', + promptChatEmpty: '目前没有对话内容', + promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。', }, // 角色管理相关翻译 role: { diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index 2e032d84..d4206fbd 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -17,6 +17,7 @@ import type { KnowledgeConfig, Variable, MemoryConfig, + AiPromptModalRef } from './types' import type { Model } from '@/views/ModelManagement/types' import { getModelList } from '@/api/models'; @@ -27,19 +28,19 @@ import { getApplicationConfig } from '@/api/application' import { getKnowledgeBaseList } from '@/api/knowledgeBase' import { memoryConfigListUrl } from '@/api/memory' import CustomSelect from '@/components/CustomSelect' - - +import aiPrompt from '@/assets/images/application/aiPrompt.png' +import AiPromptModal from './components/AiPromptModal' const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => { return ( -
+
{desc}
) } const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode}> = ({title, className, children}) => { return ( -
+
{title} {children}
@@ -50,12 +51,12 @@ const SwitchWrapper: FC<{ title: string, desc: string, name: string }> = ({ titl return (
- + @@ -66,11 +67,11 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string, url: string const { t } = useTranslation(); return ( <> - + - + ) } @@ -336,15 +337,22 @@ const Agent = forwardRef((_props, ref) => { handleSave })) + const aiPromptModalRef = useRef(null) + const handlePrompt = () => { + aiPromptModalRef.current?.handleOpen() + } + const updatePrompt = (value: string) => { + form.setFieldValue('system_prompt', value) + } return ( <> {loading && } -
+
- + ((_props, ref) => { -
+
{t('application.debuggingAndPreview')} -
+
@@ -440,6 +452,11 @@ const Agent = forwardRef((_props, ref) => { ref={modelConfigModalRef} refresh={refresh} /> + ); }); diff --git a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx index 8bfa374c..8cb9d3de 100644 --- a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx +++ b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx @@ -1,43 +1,136 @@ -import { forwardRef, useImperativeHandle, useState } from 'react'; -import { Row, Col, Space, Button } from 'antd'; +import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; +import { Button, Form, Input, App, Row, Col } from 'antd'; import { useTranslation } from 'react-i18next'; +import clsx from 'clsx' +import copy from 'copy-to-clipboard'; -import type { AiPromptModalRef } from '../types' -// import { request } from '@/utils/request' +import { updatePromptMessages, createPromptSessions } from '@/api/prompt' +import { getModelListUrl } from '@/api/models' +import type { AiPromptModalRef, AiPromptVariableModalRef, AiPromptForm } from '../types' import RbModal from '@/components/RbModal' -import Markdown from '@/components/Markdown'; +import type { Model } from '@/views/ModelManagement/types' +import ChatContent from '@/components/Chat/ChatContent' +import Empty from '@/components/Empty' +import ChatSendIcon from '@/assets/images/application/chatSend.svg' +import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg' +import type { ChatItem } from '@/components/Chat/types' +import CustomSelect from '@/components/CustomSelect' +import AiPromptVariableModal from './AiPromptVariableModal' interface AiPromptModalProps { - refresh: () => void; + refresh: (value: string) => void; + defaultModel: Model | null; } const AiPromptModal = forwardRef(({ - // refresh + refresh, + defaultModel, }, ref) => { const { t } = useTranslation(); + const { message } = App.useApp() const [visible, setVisible] = useState(false); const [loading, setLoading] = useState(false) - const [content, setContent] = useState(''); + const [form] = Form.useForm() + const [chatList, setChatList] = useState([]) + const [variables, setVariables] = useState([]) + const [promptSession, setPromptSession] = useState(null) + const aiPromptVariableModalRef = useRef(null) + const currentPromptRef = useRef(null) + const values = Form.useWatch([], form) // 封装取消方法,添加关闭弹窗逻辑 const handleClose = () => { setVisible(false); setLoading(false) + setChatList([]) + setVariables([]) + form.setFieldsValue({ + message: undefined, + current_prompt: undefined, + }) }; const handleOpen = () => { - setVisible(true); + createPromptSessions() + .then(res => { + const response = res as { id: string } + setPromptSession(response.id) + + if (!values.model_id && defaultModel?.id) { + form.setFieldValue('model_id', defaultModel?.id) + } + setVisible(true); + }) }; - // 封装保存方法,添加提交逻辑 - // const handleSave = () => { - // } + const handleSend = () => { + if (!promptSession) return + if (!values.model_id) { + message.warning(t('common.selectPlaceholder', { title: t('application.model') })) + return + } + if (!values.message) { + message.warning(t('application.promptChatPlaceholder')) + return + } + const messageContent = values.message + setLoading(true) + 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) + }) + .finally(() => { + setLoading(false) + }) + } + const handleCopy = () => { + if (!values.current_prompt || values?.current_prompt?.trim() === '') return + copy(values.current_prompt) + message.success(t('common.copySuccess')) + } + const handleAdd = () => { + 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) + } else { + form.setFieldValue('current_prompt', (values.current_prompt || '') + value) + } + } + const handleApply = () => { + if (!values.current_prompt) { + return + } + refresh(values.current_prompt) + handleClose() + } // 暴露给父组件的方法 useImperativeHandle(ref, () => ({ handleOpen, })); + console.log(values) return ( (({ footer={null} width={1000} > - - -
{t('application.generatedPrompt')}
-
-
- +
+
+ + + + + } + data={chatList || []} + streamLoading={false} + labelPosition="top" + labelFormat={(item) => item.role === 'user' ? t('application.you') : t('application.ai')} + /> + +
+ + + +
- - -
{t('application.conversationOptimizationPrompt')}
-
-
- + +
+ + + + + + + + + + + +
+ +
- - - - - - - - - - +
+ + + ); }); diff --git a/web/src/views/ApplicationConfig/components/AiPromptVariableModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptVariableModal.tsx new file mode 100644 index 00000000..61847d9a --- /dev/null +++ b/web/src/views/ApplicationConfig/components/AiPromptVariableModal.tsx @@ -0,0 +1,107 @@ +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { Form, Input, App, Select, AutoComplete, type AutoCompleteProps } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { Application } from '@/views/ApplicationManagement/types' +import type { AiPromptVariableModalRef } from '../types' +import { createApiKey } from '@/api/apiKey'; +import RbModal from '@/components/RbModal' + +const FormItem = Form.Item; + +interface AiPromptVariableModalProps { + refresh: (value: string) => void; + variables: string[]; +} + +const AiPromptVariableModal = forwardRef(({ + refresh, + variables +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const [options, setOptions] = useState([]) + + useEffect(() => { + setOptions(variables.map(key => ({ + value: key, + label: `{{${key}}}` + }))) + }, [variables]) + const handleSearch = (value: string) => { + const filterKeys = variables?.filter(key => key.includes(value)) + + if (filterKeys.length) { + setOptions(filterKeys.map(key => ({ + value: key, + label: `{{${key}}}` + }))) + } else { + setOptions([{ + value: value, + label: `{{${value}}}` + }]) + } + } + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + const handleOpen = () => { + setVisible(true); + form.resetFields(); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + const variableName = form.getFieldValue('variableName') + + if (!variableName) return + + refresh(`{{${variableName}}}`) + handleClose() + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
+ + + +
+
+ ); +}); + +export default AiPromptVariableModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index ce4f94c9..6712724d 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -361,7 +361,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc data={chat.list || []} streamLoading={compareLoading} labelPosition="top" - labelFormat={(item) => item.role === 'user' ? 'You' : chat.label} + labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label} errorDesc={t('application.ReplyException')} /> diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index c085328b..894071e6 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -137,7 +137,7 @@ export interface ModelConfigModalData { [key: string]: string; } export interface AiPromptModalRef { - handleOpen: (application?: Config) => void; + handleOpen: () => void; } export interface KnowledgeModalRef { handleOpen: (config?: KnowledgeConfig[]) => void; @@ -203,4 +203,13 @@ export interface ApiKeyModalRef { } export interface ApiKeyConfigModalRef { handleOpen: (apiKey: ApiKey) => void; +} +export interface AiPromptVariableModalRef { + handleOpen: () => void; +} + +export interface AiPromptForm { + model_id?: string; + message?: string; + current_prompt?: string; } \ No newline at end of file