|
@@ -1,92 +1,100 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="chat-messages-container">
|
|
<div class="chat-messages-container">
|
|
|
<div class="chat-messages" ref="messagesRef">
|
|
<div class="chat-messages" ref="messagesRef">
|
|
|
- <div v-for="(message, index) in messages" :key="index" :class="['message-wrapper', message.type]">
|
|
|
|
|
|
|
+ <!-- 新对话缺省图 -->
|
|
|
|
|
+ <div v-if="messages.length === 0" class="welcome-empty">
|
|
|
|
|
+ <img :src="getCssUrl('--img-chat-welcome-img')" class="welcome-img" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(message, index) in messages" :key="index" :class="['message-wrapper', message.type]" @mouseleave="copiedMessageIndex = -1">
|
|
|
<!-- AI消息 -->
|
|
<!-- AI消息 -->
|
|
|
<div v-if="message.type === 'ai'" class="ai-message">
|
|
<div v-if="message.type === 'ai'" class="ai-message">
|
|
|
- <div class="message-agent">
|
|
|
|
|
|
|
+ <!-- <div class="message-agent">
|
|
|
<div class="agent-icon"></div>
|
|
<div class="agent-icon"></div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div> -->
|
|
|
<div class="message-body">
|
|
<div class="message-body">
|
|
|
<!-- 思考过程折叠卡片 -->
|
|
<!-- 思考过程折叠卡片 -->
|
|
|
<div v-if="message.thinkingSteps && message.thinkingSteps.length > 0" class="thinking-card">
|
|
<div v-if="message.thinkingSteps && message.thinkingSteps.length > 0" class="thinking-card">
|
|
|
- <div class="thinking-header" @click="toggleThinking(index)">
|
|
|
|
|
- <div class="thinking-header-left">
|
|
|
|
|
- <div class="thinking-icon"></div>
|
|
|
|
|
- <span class="thinking-title">思考过程</span>
|
|
|
|
|
- <span class="thinking-count">{{ getAgentCount(message.thinkingSteps) }} 个子任务</span>
|
|
|
|
|
- <span v-if="getDurationText(message)" class="thinking-duration">{{ getDurationText(message) }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div :class="['thinking-arrow', { expanded: expandedThinking[index] }]">
|
|
|
|
|
- <img :src="arrowIcon" width="12" height="12" />
|
|
|
|
|
|
|
+ <div class="thinking-duration-bar" @click="toggleThinkingCard(index)">
|
|
|
|
|
+ <span v-if="getDurationText(message)" class="thinking-duration">{{ getDurationText(message) }}</span>
|
|
|
|
|
+ <div :class="['thinking-arrow', { expanded: !collapsedThinking[index] }]">
|
|
|
|
|
+ <img :src="getCssUrl('--img-chat-arrow-icon')" width="12" height="12" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-show="expandedThinking[index]" class="thinking-body">
|
|
|
|
|
- <div v-for="(group, groupIdx) in groupStepsByAgent(message.thinkingSteps)" :key="groupIdx" class="agent-group">
|
|
|
|
|
- <div class="agent-group-header" @click="toggleAgent(index + '-' + group.agent)">
|
|
|
|
|
- <div class="agent-group-header-left">
|
|
|
|
|
- <div class="agent-group-icon"></div>
|
|
|
|
|
- <span class="agent-group-name">{{ group.agent }}</span>
|
|
|
|
|
- <span class="agent-group-count">{{ group.steps.length }} 步</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div :class="['thinking-arrow', { expanded: expandedAgents[index + '-' + group.agent] }]">
|
|
|
|
|
- <img :src="arrowIcon" width="10" height="10" />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div v-show="!collapsedThinking[index]">
|
|
|
|
|
+ <div class="thinking-header">
|
|
|
|
|
+ <div class="thinking-header-left">
|
|
|
|
|
+ <div class="thinking-icon"></div>
|
|
|
|
|
+ <span class="thinking-title">思考过程 持续了几秒</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-show="expandedAgents[index + '-' + group.agent]" class="agent-group-body">
|
|
|
|
|
- <div v-for="(step, stepIdx) in group.steps" :key="stepIdx" class="thinking-step">
|
|
|
|
|
- <!-- agent_start -->
|
|
|
|
|
- <div v-if="step.type === 'agent_start'" class="step-item step-agent-start">
|
|
|
|
|
- <div class="step-icon agent-icon-start"></div>
|
|
|
|
|
- <div class="step-content">
|
|
|
|
|
- <span class="step-desc">{{ step.message }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="thinking-body">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(group, groupIdx) in groupStepsByAgent(message.thinkingSteps)"
|
|
|
|
|
+ :key="groupIdx"
|
|
|
|
|
+ :class="['agent-group', { expanded: expandedAgents[index + '-' + group.agent] }]"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="agent-group-header" @click="toggleAgent(index + '-' + group.agent)">
|
|
|
|
|
+ <div class="agent-group-header-left">
|
|
|
|
|
+ <div class="agent-group-icon"></div>
|
|
|
|
|
+ <span class="agent-group-name">{{ group.agent }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <!-- tool_call -->
|
|
|
|
|
- <div v-else-if="step.type === 'tool_call'" class="step-item step-tool-call">
|
|
|
|
|
- <div class="step-icon tool-icon"></div>
|
|
|
|
|
- <div class="step-content">
|
|
|
|
|
- <span class="step-label">调用工具:</span>
|
|
|
|
|
- <span class="step-tool-name">{{ step.tool }}</span>
|
|
|
|
|
- <span v-if="step.message" class="step-desc">{{ step.message }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- 最新一条执行结果,内联显示 -->
|
|
|
|
|
+ <span v-if="getLatestStepMessage(group)" class="agent-group-latest">
|
|
|
|
|
+ {{ getLatestStepMessage(group) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <div :class="['thinking-arrow', 'agent-arrow', { expanded: expandedAgents[index + '-' + group.agent] }]">
|
|
|
|
|
+ <img :src="getCssUrl('--img-chat-arrow-icon')" width="10" height="10" />
|
|
|
</div>
|
|
</div>
|
|
|
- <!-- executing -->
|
|
|
|
|
- <div v-else-if="step.type === 'executing'" class="step-item step-executing">
|
|
|
|
|
- <div class="step-icon executing-icon"></div>
|
|
|
|
|
- <div class="step-content">
|
|
|
|
|
- <span class="step-desc">{{ step.message || '正在执行...' }}</span>
|
|
|
|
|
- <span v-if="step.tool" class="step-tool-name">{{ step.tool }}</span>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-show="expandedAgents[index + '-' + group.agent]" class="agent-group-body">
|
|
|
|
|
+ <div v-for="(step, stepIdx) in group.steps" :key="stepIdx" class="thinking-step">
|
|
|
|
|
+ <!-- agent_start -->
|
|
|
|
|
+ <div v-if="step.type === 'agent_start'" class="step-item step-agent-start">
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ <span class="step-desc">{{ step.message }}</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- <!-- updated_todo_list -->
|
|
|
|
|
- <div v-else-if="step.type === 'updated_todo_list'" class="step-item step-todo">
|
|
|
|
|
- <div class="step-icon todo-icon"></div>
|
|
|
|
|
- <div class="step-content">
|
|
|
|
|
- <div class="todo-list">
|
|
|
|
|
- <div v-for="(todo, todoIdx) in step.todos" :key="todoIdx" class="todo-item">
|
|
|
|
|
- <span :class="['todo-status', todo.status]">
|
|
|
|
|
- {{ todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '●' : '○' }}
|
|
|
|
|
- </span>
|
|
|
|
|
- <span class="todo-text">{{ todo.content }}</span>
|
|
|
|
|
|
|
+ <!-- tool_call -->
|
|
|
|
|
+ <div v-else-if="step.type === 'tool_call'" class="step-item step-tool-call">
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ <span class="step-label">调用工具:</span>
|
|
|
|
|
+ <span class="step-tool-name">{{ step.tool }}</span>
|
|
|
|
|
+ <span v-if="step.message" class="step-desc">{{ step.message }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- executing -->
|
|
|
|
|
+ <div v-else-if="step.type === 'executing'" class="step-item step-executing">
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ <span class="step-desc">{{ step.message || '正在执行...' }}</span>
|
|
|
|
|
+ <span v-if="step.tool" class="step-tool-name">{{ step.tool }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- updated_todo_list -->
|
|
|
|
|
+ <div v-else-if="step.type === 'updated_todo_list'" class="step-item step-todo">
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ <div class="todo-list">
|
|
|
|
|
+ <div v-for="(todo, todoIdx) in step.todos" :key="todoIdx" class="todo-item">
|
|
|
|
|
+ <span :class="['todo-status', todo.status]">
|
|
|
|
|
+ {{ todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '●' : '○' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="todo-text">{{ todo.content }}</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- <!-- tool_result -->
|
|
|
|
|
- <div v-else-if="step.type === 'tool_result'" class="step-item step-tool-result">
|
|
|
|
|
- <div class="step-icon result-icon"></div>
|
|
|
|
|
- <div class="step-content">
|
|
|
|
|
- <span class="step-label">工具结果:</span>
|
|
|
|
|
- <span class="step-tool-name">{{ step.tool }}</span>
|
|
|
|
|
- <span class="step-desc">{{ step.message }}</span>
|
|
|
|
|
|
|
+ <!-- tool_result -->
|
|
|
|
|
+ <div v-else-if="step.type === 'tool_result'" class="step-item step-tool-result">
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ <span class="step-label">工具结果:</span>
|
|
|
|
|
+ <span class="step-tool-name">{{ step.tool }}</span>
|
|
|
|
|
+ <span class="step-desc">{{ step.message }}</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- <!-- agent_done -->
|
|
|
|
|
- <div v-else-if="step.type === 'agent_done'" class="step-item step-agent-done">
|
|
|
|
|
- <div class="step-icon done-icon"></div>
|
|
|
|
|
- <div class="step-content">
|
|
|
|
|
- <span class="step-desc">{{ step.message }}</span>
|
|
|
|
|
|
|
+ <!-- agent_done -->
|
|
|
|
|
+ <div v-else-if="step.type === 'agent_done'" class="step-item step-agent-done">
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ <span class="step-desc">{{ step.message }}</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -95,7 +103,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div v-if="message.content" class="message-text" v-html="renderMarkdown(message.content)"></div>
|
|
|
|
|
|
|
+ <div v-if="message.content" class="message-text" v-html="renderMarkdown(message.content, tableIcons)"></div>
|
|
|
<div v-if="message.wordDownloadUrl" class="word-download-area">
|
|
<div v-if="message.wordDownloadUrl" class="word-download-area">
|
|
|
<a-button type="link" @click="emit('download-word', message.wordDownloadUrl)"> 下载报告文件 (.docx) </a-button>
|
|
<a-button type="link" @click="emit('download-word', message.wordDownloadUrl)"> 下载报告文件 (.docx) </a-button>
|
|
|
</div>
|
|
</div>
|
|
@@ -107,8 +115,8 @@
|
|
|
</div>
|
|
</div>
|
|
|
<!-- AI消息操作栏 -->
|
|
<!-- AI消息操作栏 -->
|
|
|
<div class="message-actions">
|
|
<div class="message-actions">
|
|
|
- <div class="action-btn" @click="copyMessageContent(message.content)" title="复制">
|
|
|
|
|
- <img :src="copyIcon" width="14" height="14" />
|
|
|
|
|
|
|
+ <div class="action-btn" @click="copyMessageContent(message.content, index)" :title="copiedMessageIndex === index ? '复制成功' : '复制'">
|
|
|
|
|
+ <img :src="copyIconUrl(index)" width="14" height="14" />
|
|
|
</div>
|
|
</div>
|
|
|
<span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
|
|
<span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
|
|
|
</div>
|
|
</div>
|
|
@@ -117,7 +125,7 @@
|
|
|
<!-- 用户消息 -->
|
|
<!-- 用户消息 -->
|
|
|
<div v-else class="user-message">
|
|
<div v-else class="user-message">
|
|
|
<div class="message-body">
|
|
<div class="message-body">
|
|
|
- <div class="message-text" v-html="renderMarkdown(message.content)"></div>
|
|
|
|
|
|
|
+ <div class="message-text" v-html="renderMarkdown(message.content, tableIcons)"></div>
|
|
|
<div v-if="message.attachedFile" class="attached-file-card" @click="emit('file-preview', message.attachedFile)">
|
|
<div v-if="message.attachedFile" class="attached-file-card" @click="emit('file-preview', message.attachedFile)">
|
|
|
<div class="file-icon-card"></div>
|
|
<div class="file-icon-card"></div>
|
|
|
<div class="file-info-card">
|
|
<div class="file-info-card">
|
|
@@ -128,8 +136,8 @@
|
|
|
</div>
|
|
</div>
|
|
|
<!-- 用户消息操作栏 -->
|
|
<!-- 用户消息操作栏 -->
|
|
|
<div class="message-actions">
|
|
<div class="message-actions">
|
|
|
- <div class="action-btn" @click="copyMessageContent(message.content)" title="复制">
|
|
|
|
|
- <img :src="copyIcon" width="14" height="14" />
|
|
|
|
|
|
|
+ <div class="action-btn" @click="copyMessageContent(message.content, index)" :title="copiedMessageIndex === index ? '复制成功' : '复制'">
|
|
|
|
|
+ <img :src="copyIconUrl(index)" width="14" height="14" />
|
|
|
</div>
|
|
</div>
|
|
|
<span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
|
|
<span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
|
|
|
</div>
|
|
</div>
|
|
@@ -148,7 +156,7 @@
|
|
|
<div class="table-preview-header">
|
|
<div class="table-preview-header">
|
|
|
<span class="table-preview-title">表格预览</span>
|
|
<span class="table-preview-title">表格预览</span>
|
|
|
<div class="table-preview-close" @click="closePreview">
|
|
<div class="table-preview-close" @click="closePreview">
|
|
|
- <img :src="closeIcon" width="16" height="16" />
|
|
|
|
|
|
|
+ <img :src="getCssUrl('--img-chat-close-icon-btn')" width="16" height="16" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="table-preview-body" v-html="previewTableHtml"></div>
|
|
<div class="table-preview-body" v-html="previewTableHtml"></div>
|
|
@@ -158,13 +166,28 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
- import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
|
|
|
|
|
|
+ import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue';
|
|
|
import dayjs from 'dayjs';
|
|
import dayjs from 'dayjs';
|
|
|
import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv } from './utils';
|
|
import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv } from './utils';
|
|
|
import type { Message, AttachedFile, ThinkingStep } from './types';
|
|
import type { Message, AttachedFile, ThinkingStep } from './types';
|
|
|
- import arrowIcon from '@/assets/images/ventAI/chatModalBlue/4-2.svg';
|
|
|
|
|
- import copyIcon from '@/assets/images/ventAI/chatModalBlue/4-3.svg';
|
|
|
|
|
- import closeIcon from '@/assets/images/ventAI/chatModalBlue/4-4.svg';
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const getCssUrl = (varName: string) => {
|
|
|
|
|
+ const val = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
|
|
|
|
|
+ return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const tableIcons = {
|
|
|
|
|
+ copy: getCssUrl('--img-chat-copy-icon'),
|
|
|
|
|
+ download: getCssUrl('--img-chat-download-icon-btn'),
|
|
|
|
|
+ preview: getCssUrl('--img-chat-preview-icon'),
|
|
|
|
|
+ copySuccess: getCssUrl('--img-chat-copy-success-icon'),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const copyIconUrl = computed(() => {
|
|
|
|
|
+ const icon = getCssUrl('--img-chat-copy-icon');
|
|
|
|
|
+ const successIcon = getCssUrl('--img-chat-copy-success-icon');
|
|
|
|
|
+ return (index: number) => (copiedMessageIndex.value === index ? successIcon : icon);
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
// 全局计时器 - 每秒刷新一次,用于实时显示思考耗时
|
|
// 全局计时器 - 每秒刷新一次,用于实时显示思考耗时
|
|
|
const nowTs = ref(Date.now());
|
|
const nowTs = ref(Date.now());
|
|
@@ -198,9 +221,10 @@
|
|
|
}>();
|
|
}>();
|
|
|
|
|
|
|
|
const messagesRef = ref<HTMLElement>();
|
|
const messagesRef = ref<HTMLElement>();
|
|
|
- const expandedThinking = reactive<Record<number, boolean>>({});
|
|
|
|
|
|
|
+ const collapsedThinking = reactive<Record<number, boolean>>({});
|
|
|
const expandedAgents = reactive<Record<string, boolean>>({});
|
|
const expandedAgents = reactive<Record<string, boolean>>({});
|
|
|
const isAtBottom = ref(true);
|
|
const isAtBottom = ref(true);
|
|
|
|
|
+ const copiedMessageIndex = ref(-1);
|
|
|
|
|
|
|
|
// 表格预览状态
|
|
// 表格预览状态
|
|
|
const previewTableHtml = ref('');
|
|
const previewTableHtml = ref('');
|
|
@@ -219,15 +243,36 @@
|
|
|
switch (action) {
|
|
switch (action) {
|
|
|
case 'copy': {
|
|
case 'copy': {
|
|
|
const markdown = tableHtmlToMarkdown(tableHtml);
|
|
const markdown = tableHtmlToMarkdown(tableHtml);
|
|
|
- navigator.clipboard.writeText(markdown).catch(() => {
|
|
|
|
|
- const ta = document.createElement('textarea');
|
|
|
|
|
- ta.value = markdown;
|
|
|
|
|
- ta.style.position = 'fixed';
|
|
|
|
|
- ta.style.opacity = '0';
|
|
|
|
|
- document.body.appendChild(ta);
|
|
|
|
|
- ta.select();
|
|
|
|
|
- document.execCommand('copy');
|
|
|
|
|
- document.body.removeChild(ta);
|
|
|
|
|
|
|
+ const doCopy = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(markdown);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ const ta = document.createElement('textarea');
|
|
|
|
|
+ ta.value = markdown;
|
|
|
|
|
+ ta.style.position = 'fixed';
|
|
|
|
|
+ ta.style.opacity = '0';
|
|
|
|
|
+ document.body.appendChild(ta);
|
|
|
|
|
+ ta.select();
|
|
|
|
|
+ document.execCommand('copy');
|
|
|
|
|
+ document.body.removeChild(ta);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ doCopy().then(() => {
|
|
|
|
|
+ const btn = target;
|
|
|
|
|
+ const img = btn.querySelector('img');
|
|
|
|
|
+ const successIcon = btn.dataset.iconSuccess;
|
|
|
|
|
+ const originalIcon = btn.dataset.icon;
|
|
|
|
|
+ if (img && successIcon) {
|
|
|
|
|
+ img.src = successIcon;
|
|
|
|
|
+ btn.title = '复制成功';
|
|
|
|
|
+ const reset = () => {
|
|
|
|
|
+ img.src = originalIcon!;
|
|
|
|
|
+ btn.title = '复制Markdown';
|
|
|
|
|
+ wrapper.removeEventListener('mouseleave', reset);
|
|
|
|
|
+ };
|
|
|
|
|
+ const wrapper = btn.closest('.markdown-table-wrapper')!;
|
|
|
|
|
+ wrapper.addEventListener('mouseleave', reset);
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
@@ -269,6 +314,14 @@
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const forceScrollToBottom = () => {
|
|
|
|
|
+ const el = messagesRef.value;
|
|
|
|
|
+ if (el) {
|
|
|
|
|
+ el.scrollTop = el.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+ isAtBottom.value = true;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
messagesRef.value?.addEventListener('click', handleTableAction);
|
|
messagesRef.value?.addEventListener('click', handleTableAction);
|
|
|
messagesRef.value?.addEventListener('scroll', checkScrollBottom);
|
|
messagesRef.value?.addEventListener('scroll', checkScrollBottom);
|
|
@@ -286,14 +339,26 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const toggleThinking = (index: number) => {
|
|
|
|
|
- expandedThinking[index] = !expandedThinking[index];
|
|
|
|
|
|
|
+ const toggleThinkingCard = (index: number) => {
|
|
|
|
|
+ collapsedThinking[index] = !collapsedThinking[index];
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const toggleAgent = (key: string) => {
|
|
const toggleAgent = (key: string) => {
|
|
|
expandedAgents[key] = !expandedAgents[key];
|
|
expandedAgents[key] = !expandedAgents[key];
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const getLatestStepMessage = (group: AgentGroup): string => {
|
|
|
|
|
+ if (group.steps.length === 0) return '';
|
|
|
|
|
+ const last = group.steps[group.steps.length - 1];
|
|
|
|
|
+ if (last.type === 'executing') return last.message || '正在执行...';
|
|
|
|
|
+ if (last.type === 'tool_call') return last.message || last.tool || '';
|
|
|
|
|
+ if (last.type === 'tool_result') return last.message || '';
|
|
|
|
|
+ if (last.type === 'agent_start') return last.message || '';
|
|
|
|
|
+ if (last.type === 'agent_done') return last.message || '';
|
|
|
|
|
+ if (last.type === 'updated_todo_list') return '任务列表已更新';
|
|
|
|
|
+ return '';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
interface AgentGroup {
|
|
interface AgentGroup {
|
|
|
agent: string;
|
|
agent: string;
|
|
|
steps: ThinkingStep[];
|
|
steps: ThinkingStep[];
|
|
@@ -324,10 +389,11 @@
|
|
|
return dayjs(createdAt).format('YYYY-MM-DD HH:mm:ss');
|
|
return dayjs(createdAt).format('YYYY-MM-DD HH:mm:ss');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const copyMessageContent = async (content: string) => {
|
|
|
|
|
|
|
+ const copyMessageContent = async (content: string, index: number) => {
|
|
|
if (!content) return;
|
|
if (!content) return;
|
|
|
try {
|
|
try {
|
|
|
await navigator.clipboard.writeText(content);
|
|
await navigator.clipboard.writeText(content);
|
|
|
|
|
+ copiedMessageIndex.value = index;
|
|
|
} catch {
|
|
} catch {
|
|
|
const textarea = document.createElement('textarea');
|
|
const textarea = document.createElement('textarea');
|
|
|
textarea.value = content;
|
|
textarea.value = content;
|
|
@@ -337,10 +403,11 @@
|
|
|
textarea.select();
|
|
textarea.select();
|
|
|
document.execCommand('copy');
|
|
document.execCommand('copy');
|
|
|
document.body.removeChild(textarea);
|
|
document.body.removeChild(textarea);
|
|
|
|
|
+ copiedMessageIndex.value = index;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- defineExpose({ messagesRef, isAtBottom });
|
|
|
|
|
|
|
+ defineExpose({ messagesRef, isAtBottom, forceScrollToBottom });
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
<style scoped lang="less">
|
|
@@ -356,9 +423,26 @@
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
padding: 20px;
|
|
padding: 20px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+
|
|
|
|
|
+ .welcome-empty {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+
|
|
|
|
|
+ .welcome-img {
|
|
|
|
|
+ max-width: 300px;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ object-fit: contain;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
.message-wrapper {
|
|
.message-wrapper {
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
|
|
+ // margin-bottom: 20px;
|
|
|
|
|
|
|
|
.ai-message {
|
|
.ai-message {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -385,16 +469,14 @@
|
|
|
/* 思考过程卡片 */
|
|
/* 思考过程卡片 */
|
|
|
.thinking-card {
|
|
.thinking-card {
|
|
|
margin-bottom: 12px;
|
|
margin-bottom: 12px;
|
|
|
- border: 1px solid rgba(63, 80, 106, 0.4);
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- background: rgba(10, 132, 255, 0.04);
|
|
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
- .thinking-header {
|
|
|
|
|
|
|
+ .thinking-duration-bar {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
- padding: 10px 14px;
|
|
|
|
|
|
|
+ padding: 8px 14px;
|
|
|
|
|
+ border-bottom: 1px solid rgba(63, 80, 106, 0.3);
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
user-select: none;
|
|
user-select: none;
|
|
|
transition: background 0.2s;
|
|
transition: background 0.2s;
|
|
@@ -403,6 +485,28 @@
|
|
|
background: rgba(10, 132, 255, 0.08);
|
|
background: rgba(10, 132, 255, 0.08);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .thinking-duration {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .thinking-arrow {
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ transition: transform 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &.expanded {
|
|
|
|
|
+ transform: rotate(180deg);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .thinking-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 10px 14px;
|
|
|
|
|
+
|
|
|
.thinking-header-left {
|
|
.thinking-header-left {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -412,17 +516,15 @@
|
|
|
.thinking-icon {
|
|
.thinking-icon {
|
|
|
width: 16px;
|
|
width: 16px;
|
|
|
height: 16px;
|
|
height: 16px;
|
|
|
- background-image: var(--img-chat-thinking-icon, none);
|
|
|
|
|
|
|
+ background-image: var(--img-chat-thinking-icon);
|
|
|
background-repeat: no-repeat;
|
|
background-repeat: no-repeat;
|
|
|
background-size: 100% 100%;
|
|
background-size: 100% 100%;
|
|
|
- /* fallback: 用一个 brain/lightbulb emoji 风格的 SVG */
|
|
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8 1.5a4.5 4.5 0 0 1 3.5 7.35V11a1 1 0 0 1-1 1H5.5a1 1 0 0 1-1-1v-2.15A4.5 4.5 0 0 1 8 1.5Z' stroke='%230a84ff' stroke-width='1.2'/%3E%3Cpath d='M6 13.5h4M6.5 15h3' stroke='%230a84ff' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.thinking-title {
|
|
.thinking-title {
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
- color: #0a84ff;
|
|
|
|
|
|
|
+ color: #8b949e;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.thinking-count {
|
|
.thinking-count {
|
|
@@ -432,36 +534,21 @@
|
|
|
padding: 1px 6px;
|
|
padding: 1px 6px;
|
|
|
border-radius: 10px;
|
|
border-radius: 10px;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .thinking-duration {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #8b949e;
|
|
|
|
|
- margin-left: 4px;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .thinking-arrow {
|
|
|
|
|
- color: #8b949e;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- transition: transform 0.2s;
|
|
|
|
|
-
|
|
|
|
|
- &.expanded {
|
|
|
|
|
- transform: rotate(180deg);
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.thinking-body {
|
|
.thinking-body {
|
|
|
- border-top: 1px solid rgba(63, 80, 106, 0.3);
|
|
|
|
|
padding: 8px 14px;
|
|
padding: 8px 14px;
|
|
|
- max-height: 400px;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
.agent-group {
|
|
.agent-group {
|
|
|
margin-bottom: 8px;
|
|
margin-bottom: 8px;
|
|
|
- border: 1px solid rgba(63, 80, 106, 0.25);
|
|
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
border-radius: 6px;
|
|
border-radius: 6px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
+ transition: border-color 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &.expanded {
|
|
|
|
|
+ border-color: rgba(63, 80, 106, 0.25);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
&:last-child {
|
|
&:last-child {
|
|
|
margin-bottom: 0;
|
|
margin-bottom: 0;
|
|
@@ -474,7 +561,6 @@
|
|
|
padding: 8px 12px;
|
|
padding: 8px 12px;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
user-select: none;
|
|
user-select: none;
|
|
|
- background: rgba(10, 132, 255, 0.06);
|
|
|
|
|
transition: background 0.2s;
|
|
transition: background 0.2s;
|
|
|
|
|
|
|
|
&:hover {
|
|
&:hover {
|
|
@@ -490,7 +576,7 @@
|
|
|
.agent-group-icon {
|
|
.agent-group-icon {
|
|
|
width: 14px;
|
|
width: 14px;
|
|
|
height: 14px;
|
|
height: 14px;
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none'%3E%3Ccircle cx='7' cy='5' r='2.5' stroke='%230a84ff' stroke-width='1'/%3E%3Cpath d='M2.5 12.5c0-2.49 2.01-4.5 4.5-4.5s4.5 2.01 4.5 4.5' stroke='%230a84ff' stroke-width='1' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
|
|
|
|
|
+ background-image: var(--img-chat-agent-icon);
|
|
|
background-repeat: no-repeat;
|
|
background-repeat: no-repeat;
|
|
|
background-size: 100% 100%;
|
|
background-size: 100% 100%;
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
@@ -499,20 +585,37 @@
|
|
|
.agent-group-name {
|
|
.agent-group-name {
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
- color: #0a84ff;
|
|
|
|
|
|
|
+ color: #fff;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .agent-group-count {
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
|
|
+ .agent-group-latest {
|
|
|
|
|
+ font-size: 12px;
|
|
|
color: #8b949e;
|
|
color: #8b949e;
|
|
|
- background: rgba(10, 132, 255, 0.08);
|
|
|
|
|
- padding: 1px 5px;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .agent-arrow {
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ transition: transform 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &.expanded {
|
|
|
|
|
+ transform: rotate(180deg);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.agent-group-body {
|
|
.agent-group-body {
|
|
|
padding: 4px 12px;
|
|
padding: 4px 12px;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -520,7 +623,6 @@
|
|
|
.step-item {
|
|
.step-item {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
align-items: flex-start;
|
|
|
- gap: 8px;
|
|
|
|
|
padding: 6px 0;
|
|
padding: 6px 0;
|
|
|
border-bottom: 1px solid rgba(63, 80, 106, 0.15);
|
|
border-bottom: 1px solid rgba(63, 80, 106, 0.15);
|
|
|
|
|
|
|
@@ -528,15 +630,6 @@
|
|
|
border-bottom: none;
|
|
border-bottom: none;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .step-icon {
|
|
|
|
|
- width: 18px;
|
|
|
|
|
- height: 18px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- margin-top: 1px;
|
|
|
|
|
- background-repeat: no-repeat;
|
|
|
|
|
- background-size: 100% 100%;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
.step-content {
|
|
.step-content {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
@@ -567,25 +660,8 @@
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* agent_start */
|
|
|
|
|
- .step-agent-start .step-icon {
|
|
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='6' r='3' stroke='%230a84ff' stroke-width='1.2'/%3E%3Cpath d='M3 16c0-3.31 2.69-6 6-6s6 2.69 6 6' stroke='%230a84ff' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* tool_call */
|
|
|
|
|
- .step-tool-call .step-icon {
|
|
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Cpath d='M10.5 2.5l5 5-8 8H2.5v-5.5l8-8Z' stroke='%23d29922' stroke-width='1.2' stroke-linejoin='round'/%3E%3Cpath d='M7 7l4 4' stroke='%23d29922' stroke-width='1.2'/%3E%3C/svg%3E");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* executing */
|
|
|
|
|
- .step-executing .step-icon {
|
|
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='9' r='7' stroke='%230a84ff' stroke-width='1.2' stroke-dasharray='4 2'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 9 9' to='360 9 9' dur='2s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/svg%3E");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/* updated_todo_list */
|
|
/* updated_todo_list */
|
|
|
- .step-todo .step-icon {
|
|
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Crect x='2' y='2' width='14' height='14' rx='2' stroke='%233fb950' stroke-width='1.2'/%3E%3Cpath d='M5 9l3 3 5-6' stroke='%233fb950' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
|
|
|
-
|
|
|
|
|
|
|
+ .step-todo {
|
|
|
.todo-list {
|
|
.todo-list {
|
|
|
.todo-item {
|
|
.todo-item {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -617,16 +693,6 @@
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- /* tool_result */
|
|
|
|
|
- .step-tool-result .step-icon {
|
|
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='9' r='7' stroke='%233fb950' stroke-width='1.2'/%3E%3Cpath d='M6 9l2 2 4-4' stroke='%233fb950' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* agent_done */
|
|
|
|
|
- .step-agent-done .step-icon {
|
|
|
|
|
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='9' r='7' stroke='%233fb950' stroke-width='1.2'/%3E%3Cpath d='M5.5 9l2.5 2.5 4.5-5' stroke='%233fb950' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -919,7 +985,7 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.ai-message .message-actions {
|
|
.ai-message .message-actions {
|
|
|
- padding: 4px 0 0 50px;
|
|
|
|
|
|
|
+ padding: 4px 0 0 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.user-message .message-actions {
|
|
.user-message .message-actions {
|