实现Agent对话,合格自动提交,不合格补充材料的能力

This commit is contained in:
wandering
2026-06-14 12:56:33 +08:00
parent ef9e1d771b
commit 01d4686ceb
68 changed files with 8914 additions and 1908 deletions
+199
View File
@@ -0,0 +1,199 @@
/**
* LLM 流式聊天气泡管理器
*
* 流式消息在处理时显示思考过程和逐字输出,结束后固化为持久消息。
*/
import { escapeHtml } from '../utils.js';
import { getChatMessages, scrollToBottom } from './renderer.js';
/**
* 当前活跃的 LLM 流式气泡状态
*/
let llmStreamState = {
wrapper: null,
bubble: null,
textContent: null,
accumulated: '',
reasoningContent: null,
reasoningAccumulated: '',
};
/**
* 处理 LLM 流式事件
*
* @param {Object} msg - SSE 传来的 llm_stream 事件对象
*/
export function handleLLMStream(msg) {
switch (msg.phase) {
case 'start':
_createLLMStreamBubble(msg.label || '正在分析中...');
break;
case 'reasoning':
_appendLLMStreamReasoning(msg.text || '');
break;
case 'chunk':
_appendLLMStreamChunk(msg.text || '');
break;
case 'end':
_closeLLMStreamBubble(msg.label || '分析完成');
break;
case 'error':
_errorLLMStreamBubble(msg.error || '分析失败');
break;
}
}
/**
* 创建 LLM 流式气泡(start 阶段)
*/
function _createLLMStreamBubble(label) {
const chatMessages = getChatMessages();
if (!chatMessages) return;
if (llmStreamState.wrapper) {
_closeLLMStreamBubbleSilent();
}
const wrapper = document.createElement('div');
wrapper.className = 'chat-message';
const bubble = document.createElement('div');
bubble.className = 'chat-bubble processing llm-stream-bubble';
const labelEl = document.createElement('div');
labelEl.className = 'llm-stream-label';
labelEl.textContent = label;
const reasoningSection = document.createElement('details');
reasoningSection.className = 'llm-reasoning-section';
reasoningSection.open = true;
const reasoningSummary = document.createElement('summary');
reasoningSummary.className = 'llm-reasoning-summary';
reasoningSummary.textContent = '思考过程(点击折叠)';
const reasoningText = document.createElement('div');
reasoningText.className = 'llm-reasoning-text';
reasoningText.textContent = '';
reasoningSection.appendChild(reasoningSummary);
reasoningSection.appendChild(reasoningText);
const textEl = document.createElement('div');
textEl.className = 'llm-stream-text';
textEl.textContent = '';
bubble.appendChild(labelEl);
bubble.appendChild(reasoningSection);
bubble.appendChild(textEl);
wrapper.appendChild(bubble);
chatMessages.appendChild(wrapper);
scrollToBottom();
llmStreamState = {
wrapper,
bubble,
textContent: textEl,
accumulated: '',
reasoningContent: reasoningText,
reasoningAccumulated: '',
};
}
/**
* 追加流式文本片段(chunk 阶段)
*/
function _appendLLMStreamChunk(text) {
if (!llmStreamState.textContent) return;
llmStreamState.accumulated += text;
llmStreamState.textContent.textContent = llmStreamState.accumulated;
scrollToBottom();
}
/**
* 追加思考过程片段(reasoning 阶段)
*/
function _appendLLMStreamReasoning(text) {
if (!llmStreamState.reasoningContent) return;
llmStreamState.reasoningAccumulated += text;
llmStreamState.reasoningContent.textContent = llmStreamState.reasoningAccumulated;
scrollToBottom();
}
/**
* 关闭流式气泡(end 阶段)— 气泡保留在聊天历史中作为持久消息
*/
function _closeLLMStreamBubble(label) {
if (!llmStreamState.bubble) return;
llmStreamState.bubble.classList.remove('processing');
llmStreamState.bubble.classList.add('done');
const labelEl = llmStreamState.bubble.querySelector('.llm-stream-label');
if (labelEl) {
labelEl.textContent = label;
}
const reasoningSection = llmStreamState.bubble.querySelector('.llm-reasoning-section');
if (reasoningSection) {
reasoningSection.open = false;
}
scrollToBottom();
llmStreamState = {
wrapper: null,
bubble: null,
textContent: null,
accumulated: '',
reasoningContent: null,
reasoningAccumulated: '',
};
}
/**
* 静默关闭流式气泡(不改变样式,用于 start 前清理)
*/
function _closeLLMStreamBubbleSilent() {
if (llmStreamState.wrapper) {
llmStreamState.wrapper.remove();
}
llmStreamState = {
wrapper: null,
bubble: null,
textContent: null,
accumulated: '',
reasoningContent: null,
reasoningAccumulated: '',
};
}
/**
* 错误状态(error 阶段)
*/
function _errorLLMStreamBubble(errorMsg) {
if (!llmStreamState.bubble) return;
llmStreamState.bubble.classList.remove('processing');
llmStreamState.bubble.classList.add('error');
const labelEl = llmStreamState.bubble.querySelector('.llm-stream-label');
if (labelEl) {
labelEl.textContent = '分析失败';
}
if (llmStreamState.textContent) {
llmStreamState.textContent.textContent = escapeHtml(errorMsg);
}
llmStreamState = {
wrapper: null,
bubble: null,
textContent: null,
accumulated: '',
reasoningContent: null,
reasoningAccumulated: '',
};
}