|
@@ -563,8 +563,14 @@
|
|
|
:style="{ width: rightMenuWidth + 'px' }"
|
|
|
>
|
|
|
<div v-if="inferenceModel">
|
|
|
- <div style="height: 91vh;overflow: auto;" class="file-list" v-html="formatted_data">
|
|
|
-
|
|
|
+ <div style="height: 91vh;overflow: auto;" class="file-list">
|
|
|
+ <!-- 步骤显示区域 -->
|
|
|
+ <div id="stepsContainer" v-show="showSteps" class="steps-container">
|
|
|
+ <h3 class="steps-title">📋 处理步骤</h3>
|
|
|
+ <div id="stepsList"></div>
|
|
|
+ </div>
|
|
|
+ <!-- 结果显示区域 -->
|
|
|
+ <div id="resultContent" v-html="formatted_data"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<!-- 修改收缩按钮的位置和样式 -->
|
|
@@ -871,8 +877,122 @@ const enableKnowledgeSearch = ref(false);
|
|
|
const knowledgeResults = ref([]);
|
|
|
const knowledgeTotal = ref(0);
|
|
|
const isKnowledgeLoading = ref(false);
|
|
|
-const formatted_data = ref('')
|
|
|
+const formatted_data = ref('');
|
|
|
+const showSteps = ref(false);
|
|
|
+const steps = ref([]);
|
|
|
+
|
|
|
+// SSE消息处理函数
|
|
|
+const handleSSEMessage = (event, data) => {
|
|
|
+ // 立即处理每个事件
|
|
|
+ switch (event) {
|
|
|
+ case 'html_start':
|
|
|
+ formatted_data.value = '';
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'html_chunk':
|
|
|
+ if (data && typeof data.html === 'string') {
|
|
|
+ // 直接追加新内容到现有内容
|
|
|
+ formatted_data.value += data.html;
|
|
|
+ // 确保滚动到最新内容
|
|
|
+ nextTick(() => {
|
|
|
+ const resultContent = document.getElementById('resultContent');
|
|
|
+ if (resultContent) {
|
|
|
+ resultContent.scrollTop = resultContent.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'html_end':
|
|
|
+ // 完成渲染,可以在这里添加完成的标记
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'init_steps':
|
|
|
+ showSteps.value = true;
|
|
|
+ // 立即初始化步骤
|
|
|
+ initializeSteps(data.steps);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'step_update':
|
|
|
+ // 立即更新步骤状态
|
|
|
+ updateStepElement(data.step_id, data.status, data.details, data.progress);
|
|
|
+ break;
|
|
|
+ case 'final_summary':
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 'stream_end':
|
|
|
+ showSteps.value = false;
|
|
|
+ /* // 立即初始化步骤
|
|
|
+ initializeSteps(data.steps); */
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'error':
|
|
|
+ console.error('SSE错误:', data.message);
|
|
|
+ /* ElMessage.error(data.message || '处理过程中出现错误'); */
|
|
|
+ break;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 初始化步骤
|
|
|
+const initializeSteps = (stepsData) => {
|
|
|
+ const stepsList = document.getElementById('stepsList');
|
|
|
+ if (!stepsList) return;
|
|
|
+
|
|
|
+ stepsList.innerHTML = '';
|
|
|
+ steps.value = stepsData;
|
|
|
+
|
|
|
+ stepsData.forEach(step => {
|
|
|
+ stepsList.appendChild(createStepElement(step));
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 创建步骤元素
|
|
|
+const createStepElement = (step) => {
|
|
|
+ const stepEl = document.createElement('div');
|
|
|
+ stepEl.className = `step ${step.status}`;
|
|
|
+ stepEl.id = `step-${step.step_id}`;
|
|
|
+
|
|
|
+ stepEl.innerHTML = `
|
|
|
+ <div class="step-header">
|
|
|
+ <div class="step-icon">${getStepIcon(step.status)}</div>
|
|
|
+ <div class="step-name">${step.name || step.step_id}</div>
|
|
|
+ </div>
|
|
|
+ <div class="step-details">${step.details || ''}</div>
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div class="progress-fill" style="width: ${step.progress || 0}%"></div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ return stepEl;
|
|
|
+};
|
|
|
+
|
|
|
+// 更新步骤元素
|
|
|
+const updateStepElement = (stepId, status, details, progress) => {
|
|
|
+ const stepEl = document.getElementById(`step-${stepId}`);
|
|
|
+ if (!stepEl) return;
|
|
|
+
|
|
|
+ stepEl.className = `step ${status}`;
|
|
|
+ stepEl.querySelector('.step-icon').textContent = getStepIcon(status);
|
|
|
+
|
|
|
+ if (details) {
|
|
|
+ stepEl.querySelector('.step-details').textContent = details;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (progress !== undefined) {
|
|
|
+ stepEl.querySelector('.progress-fill').style.width = `${progress}%`;
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
+// 获取步骤图标
|
|
|
+const getStepIcon = (status) => {
|
|
|
+ switch (status) {
|
|
|
+ case 'pending': return '⏳';
|
|
|
+ case 'running': return '🔄';
|
|
|
+ case 'completed': return '✅';
|
|
|
+ case 'error': return '❌';
|
|
|
+ default: return '⏳';
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
// 智能体
|
|
|
const inferenceModel = ref(false);
|
|
@@ -1423,39 +1543,110 @@ const sendMessage = async () => {
|
|
|
};
|
|
|
|
|
|
// 发送初始请求获取SSE URL
|
|
|
- const response = await fetch(`${import.meta.env.VITE_BASE_AI_API}/api/agents/database/`, {
|
|
|
- method: 'POST',
|
|
|
- headers: {
|
|
|
- 'Authorization': `JWT ${token}`,
|
|
|
- 'Content-Type': 'application/json'
|
|
|
- },
|
|
|
- body: JSON.stringify(requestData)
|
|
|
- });
|
|
|
+ let reader = null;
|
|
|
+ try {
|
|
|
+ // 只发送SSE请求
|
|
|
+ const sseResponse = await fetch(`${import.meta.env.VITE_BASE_AI_API}/api/agents/database/streaming/`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `JWT ${token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify(requestData)
|
|
|
+ });
|
|
|
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error('请求失败');
|
|
|
- }
|
|
|
+ if (!sseResponse.ok) {
|
|
|
+ throw new Error('SSE请求失败');
|
|
|
+ }
|
|
|
|
|
|
- // 获取SSE URL
|
|
|
- const responseData = await response.json();
|
|
|
- console.log(responseData)//.result.summary
|
|
|
- // 移除临时消息
|
|
|
- messages.value = messages.value.filter(
|
|
|
- (msg) => msg.id !== tempAiMessage.id
|
|
|
- );
|
|
|
- formatted_data.value = responseData.result.html
|
|
|
- const messageContent = responseData.result.summary
|
|
|
- // 创建新的AI消息
|
|
|
- const aiMessage = reactive({
|
|
|
- id: Date.now() + 2,
|
|
|
- role: "assistant",
|
|
|
- content: messageContent || '',
|
|
|
- displayContent: "",
|
|
|
- audioData: null,
|
|
|
- });
|
|
|
- messages.value.push(aiMessage);
|
|
|
- // 显示打字效果
|
|
|
- await typeMessage(aiMessage);
|
|
|
+ // 处理SSE响应
|
|
|
+
|
|
|
+ reader = sseResponse.body.getReader();
|
|
|
+ const decoder = new TextDecoder();
|
|
|
+ let buffer = '';
|
|
|
+ let currentEvent = '';
|
|
|
+ let streamContent = '';
|
|
|
+
|
|
|
+ // 读取流数据
|
|
|
+ while (true) {
|
|
|
+ const { done, value } = await reader.read();
|
|
|
+ if (done) break;
|
|
|
+
|
|
|
+ buffer += decoder.decode(value, { stream: true });
|
|
|
+ const lines = buffer.split('\n');
|
|
|
+
|
|
|
+ // 保留最后一行(可能不完整)
|
|
|
+ buffer = lines.pop() || '';
|
|
|
+
|
|
|
+ for (const line of lines) {
|
|
|
+ if (line.startsWith('event:')) {
|
|
|
+ currentEvent = line.substring(6).trim();
|
|
|
+ } else if (line.startsWith('data:')) {
|
|
|
+ try {
|
|
|
+ const dataStr = line.substring(5).trim();
|
|
|
+ if (dataStr) {
|
|
|
+ const data = JSON.parse(dataStr);
|
|
|
+ // 立即处理每条消息
|
|
|
+
|
|
|
+ handleSSEMessage(currentEvent || 'message', data);
|
|
|
+ if(currentEvent === 'final_summary'){
|
|
|
+ // 移除临时消息
|
|
|
+ messages.value = messages.value.filter(
|
|
|
+ (msg) => msg.id !== tempAiMessage.id
|
|
|
+ );
|
|
|
+
|
|
|
+ // 创建新的AI消息
|
|
|
+ const aiMessage = reactive({
|
|
|
+ id: Date.now() + 2,
|
|
|
+ role: "assistant",
|
|
|
+ content: data.summary,
|
|
|
+ displayContent: "",
|
|
|
+ audioData: null,
|
|
|
+ });
|
|
|
+ isTyping.value = true;
|
|
|
+ messages.value.push(aiMessage);
|
|
|
+ // 显示打字效果
|
|
|
+ try {
|
|
|
+ await typeMessage(aiMessage);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('打字效果出错:', error);
|
|
|
+ // 如果打字效果失败,直接显示完整内容
|
|
|
+ aiMessage.displayContent = aiMessage.content;
|
|
|
+ } finally {
|
|
|
+ isTyping.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Failed to parse SSE data:', line, e);
|
|
|
+ }
|
|
|
+ } else if (line === '') {
|
|
|
+ // 空行表示事件结束,重置当前事件
|
|
|
+ currentEvent = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 设置初始的formatted_data(如果流式数据还没开始更新)
|
|
|
+ /* if (!streamContent) {
|
|
|
+ formatted_data.value = responseData.result.html;
|
|
|
+ } */
|
|
|
+ } catch (error) {
|
|
|
+ console.error('请求或处理过程出错:', error);
|
|
|
+ ElMessage.error('请求失败,请稍后重试');
|
|
|
+ throw error;
|
|
|
+ } finally {
|
|
|
+ // 确保在结束时关闭reader
|
|
|
+ if (reader) {
|
|
|
+ try {
|
|
|
+ await reader.cancel();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('关闭reader时出错:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
|
|
@@ -5671,6 +5862,71 @@ button,
|
|
|
background: #e0f7fa;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
+ .step-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .step-icon {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* .step.pending .step-icon {
|
|
|
+ background: #e5e7eb;
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+
|
|
|
+ .step.running .step-icon {
|
|
|
+ background: #3b82f6;
|
|
|
+ color: white;
|
|
|
+ } */
|
|
|
+
|
|
|
+ /* .step.completed .step-icon {
|
|
|
+
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .step.error .step-icon {
|
|
|
+
|
|
|
+ color: white;
|
|
|
+ } */
|
|
|
+
|
|
|
+ .step-name {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .step-details {
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-left: 36px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* .progress-bar {
|
|
|
+ width: 100%;
|
|
|
+ height: 4px;
|
|
|
+ background: #e5e7eb;
|
|
|
+ border-radius: 2px;
|
|
|
+ margin-top: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-left: 36px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: #10b981;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+ width: 0%;
|
|
|
+ } */
|
|
|
|
|
|
</style>
|
|
|
|