Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 44 additions & 82 deletions ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,7 +88,6 @@ const inputValue = ref('')
const applicationId = ref('')
const activeModelId = ref('')
const messages = ref<PromptGenerateMessage[]>([])
const lastRequestMessages = ref<PromptGenerateMessage[]>([])
const scrollbarRef = useTemplateRef<ScrollbarInstance>('scrollbarRef')

const latestAnswer = computed(() => [...messages.value].reverse().find(({ role }) => role === 'ai')?.content ?? '')
Expand All @@ -101,7 +99,6 @@ function resetData() {
inputValue.value = ''
loading.value = false
messages.value = []
lastRequestMessages.value = []
activeModelId.value = ''
}

Expand All @@ -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<PromptGenerateMessage>({ 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<PromptGenerateMessage>({ 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) {
Expand All @@ -202,7 +175,9 @@ function replacePrompt() {
}

/** 重新生成按钮的预留入口,当前尚未调用重试流程。 */
function handleReGenerate() {}
function handleReGenerate() {
generatePrompt(true)
}

// 弹窗关闭时调用停止入口,会话数据在关闭动画结束后统一清理。
watch(visible, (value) => {
Expand All @@ -224,36 +199,23 @@ watch(visible, (value) => {
<el-scrollbar v-if="latestAnswer" ref="scrollbarRef" max-height="320">
<div class="whitespace-pre-wrap break-words">{{ latestAnswer }}</div>
</el-scrollbar>
<div v-if="!loading && lastRequestMessages.length" class="flex gap-3">
<div v-if="!loading" class="flex gap-3">
<!-- 替换:将最新结果回写到节点的系统提示词。 -->
<el-button type="primary" :disabled="!latestAnswer" @click="replacePrompt">替换</el-button>
<!-- 重新生成:预留按钮,重试逻辑待接入。 -->
<el-button class="ml-0!" :disabled="!activeModelId" @click="handleReGenerate">重新生成</el-button>
</div>

<div class="prompt-generate-input relative overflow-hidden rounded-2xl bg-white">
<el-input
v-model="inputValue"
:autosize="{ minRows: 3, maxRows: 6 }"
maxlength="100000"
placeholder="请输入提示词主题"
type="textarea"
resize="none"
@keydown="handleKeydown"
/>
<el-input v-model="inputValue" :autosize="{ minRows: 3, maxRows: 6 }" maxlength="100000" placeholder="请输入提示词主题"
type="textarea" resize="none" @keydown="handleKeydown" />
<!-- 停止生成:生成期间显示,请求取消逻辑待接入。 -->
<el-button v-if="loading" class="absolute right-3 bottom-3" circle type="primary" @click="stopGenerate">
<span class="h-3 w-3 rounded-sm bg-white" />
</el-button>
<!-- 发送:提交输入主题;缺少主题、模型或智能体时禁用。 -->
<el-button
v-else
class="absolute right-3 bottom-3"
circle
type="primary"
:disabled="!inputValue.trim() || !activeModelId || !applicationId"
@click="generatePrompt()"
>
<el-button v-else class="absolute right-3 bottom-3" circle type="primary"
:disabled="!inputValue.trim() || !activeModelId || !applicationId" @click="generatePrompt()">
<MkIcon :icon="Top" />
</el-button>
</div>
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/ai-chat-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ onMounted(() => {
</el-tooltip>
</div>
<!-- // TODO 生成 -->
<!-- <PromptGenerate
<PromptGenerate
:model-id="formData.model_id"
:provider-options="providerOptions"
:disabled="formData.model_id_type === 'reference' || !formData.model_id"
@replace="formData.system = $event"
/> -->
/>
</div>
</template>
<MdEditorMagnify
Expand Down
Loading