From 5f9c0f96d7e70827dd0b0a25b6ed0d917c635ad5 Mon Sep 17 00:00:00 2001 From: shaohuzhang1 Date: Fri, 18 Sep 2026 14:29:24 +0800 Subject: [PATCH] feat: Prompt Generate --- .../ai-chat-node/component/PromptGenerate.vue | 126 ++++++------------ 1 file changed, 44 insertions(+), 82 deletions(-) diff --git a/ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerate.vue b/ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerate.vue index 8ab600944f2..858012b47f5 100644 --- a/ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerate.vue +++ b/ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerate.vue @@ -6,9 +6,8 @@ import type { ScrollbarInstance } from 'element-plus' import ApplicationApi from '@/api/admin/workspace/application/application' import type { ModelItem, ModelProviderItem, PromptGenerateMessage, PromptGeneratePayload } from '@/api/types' import { MsgError } from '@/utils/message' - +import { ConversationStream } from "@/conversation-panel/stream"; defineOptions({ name: 'AiChatNodePromptGenerate' }) - const props = defineProps<{ modelId: string disabled: boolean @@ -89,7 +88,6 @@ const inputValue = ref('') const applicationId = ref('') const activeModelId = ref('') const messages = ref([]) -const lastRequestMessages = ref([]) const scrollbarRef = useTemplateRef('scrollbarRef') const latestAnswer = computed(() => [...messages.value].reverse().find(({ role }) => role === 'ai')?.content ?? '') @@ -101,7 +99,6 @@ function resetData() { inputValue.value = '' loading.value = false messages.value = [] - lastRequestMessages.value = [] activeModelId.value = '' } @@ -120,72 +117,48 @@ function scrollToBottom() { nextTick(() => scrollbarRef.value?.setScrollTop(Number.MAX_SAFE_INTEGER)) } -/** 解析单个 SSE 事件,追加文本并将服务端错误交给请求流程处理。 */ -function appendStreamEvent(eventText: string, answer: PromptGenerateMessage) { - const data = eventText - .split(/\r?\n/) - .filter((line) => line.startsWith('data:')) - .map((line) => line.slice(5).trimStart()) - .join('\n') - if (!data || data === '[DONE]') return - - const chunk = JSON.parse(data) as { content?: string; error?: string } - if (chunk.error) throw new Error(chunk.error) - answer.content += chunk.content ?? '' - scrollToBottom() -} - -/** 按事件边界读取响应流,保留跨数据块的未完整事件。 */ -async function readStream(response: Response, answer: PromptGenerateMessage) { - if (!response.body) throw new Error('生成接口未返回可读取的数据流') +let conversationStream: ConversationStream | undefined = undefined; - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = '' - - while (true) { - const { done, value } = await reader.read() - buffer += decoder.decode(value, { stream: !done }) - const events = buffer.split(/\r?\n\r?\n/) - buffer = events.pop() ?? '' - events.forEach((eventText) => appendStreamEvent(eventText, answer)) - if (done) break - } - if (buffer.trim()) appendStreamEvent(buffer, answer) -} - -// 生成操作 -/** 提交输入并读取生成结果;重试时复用上次请求,避免重复追加用户消息。 */ -async function generatePrompt(regenerate = false) { +function generatePrompt(regenerate = false) { const content = inputValue.value.trim() if (loading.value || !applicationId.value || !activeModelId.value) return - if (regenerate ? !lastRequestMessages.value.length : !content) return - - const requestMessages: PromptGenerateMessage[] = regenerate - ? lastRequestMessages.value.map((message) => ({ ...message })) - : [...messages.value.map((message) => ({ ...message })), { content, role: 'user' }] - lastRequestMessages.value = requestMessages - const answer = reactive({ content: '', role: 'ai' }) - messages.value = [...requestMessages, answer] - if (!regenerate) inputValue.value = '' - loading.value = true - - const payload: PromptGeneratePayload = { messages: requestMessages, prompt: PROMPT_TEMPLATE } - try { - const response = await ApplicationApi.postPromptGenerate(applicationId.value, activeModelId.value, payload) - await readStream(response, answer) - } catch (error) { - messages.value = messages.value.filter((message) => message !== answer) - if (!(error instanceof Error) || error.name !== 'StreamRequestError') { - MsgError(error instanceof Error ? error.message : '提示词生成失败') - } - } finally { - loading.value = false + if (regenerate) { + messages.value.push({ content: 'Re generate', role: 'user' }) + }else{ + messages.value.push({ content: content, role: 'user' }) } + inputValue.value = '' + loading.value = true + const payload: PromptGeneratePayload = { messages: [...messages.value], prompt: PROMPT_TEMPLATE } + const answer = reactive({ content: '', role: 'ai' }) + messages.value.push(answer) + ApplicationApi.postPromptGenerate(applicationId.value, activeModelId.value, payload).then(response=>{ + conversationStream = new ConversationStream(response, + (chunk) => { + answer.content+=chunk.content + scrollToBottom() + }, (error) => { + if (error) { + messages.value = messages.value.filter((message) => message !== answer) + if (!(error instanceof Error) || error.name !== 'StreamRequestError') { + MsgError(error instanceof Error ? error.message : '提示词生成失败') + } + } + conversationStream = undefined + loading.value = false + }) + conversationStream.start() + }) } /** 停止生成的预留入口,当前尚未接入请求取消逻辑。 */ -function stopGenerate() {} +function stopGenerate() { + if (conversationStream) { + conversationStream.cancel() + loading.value=false + conversationStream = undefined + } +} /** Enter 提交主题,Shift+Enter 和输入法组合输入保留原生行为。 */ function handleKeydown(event: KeyboardEvent) { @@ -202,7 +175,9 @@ function replacePrompt() { } /** 重新生成按钮的预留入口,当前尚未调用重试流程。 */ -function handleReGenerate() {} +function handleReGenerate() { + generatePrompt(true) +} // 弹窗关闭时调用停止入口,会话数据在关闭动画结束后统一清理。 watch(visible, (value) => { @@ -224,7 +199,7 @@ watch(visible, (value) => {
{{ latestAnswer }}
-
+
替换 @@ -232,28 +207,15 @@ watch(visible, (value) => {
- + - +