|
@@ -13,21 +13,34 @@
|
|
|
<span class="di-header__title">数据解读</span>
|
|
<span class="di-header__title">数据解读</span>
|
|
|
<span class="di-header__close" @click="$emit('close')">✕</span>
|
|
<span class="di-header__close" @click="$emit('close')">✕</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="di-body">
|
|
|
|
|
|
|
+ <div ref="bodyRef" class="di-body">
|
|
|
<div v-if="showProcess" class="di-process">
|
|
<div v-if="showProcess" class="di-process">
|
|
|
- <div v-if="thinking" class="di-process__row di-process__thinking"><a-spin size="small" /><span>AI 正在思考...</span></div>
|
|
|
|
|
- <div v-if="todos.length > 0" class="di-process__todos">
|
|
|
|
|
- <div v-for="(todo, i) in todos" :key="i" class="di-todo-item" :class="`di-todo-item--${todo.status}`">
|
|
|
|
|
- <span class="di-todo-item__icon">
|
|
|
|
|
- <span v-if="todo.status === 'completed'" class="di-todo-check">✓</span>
|
|
|
|
|
- <a-spin v-else-if="todo.status === 'in_progress'" size="small" />
|
|
|
|
|
- <span v-else class="di-todo-dot"></span>
|
|
|
|
|
- </span>
|
|
|
|
|
- <span class="di-todo-item__text">{{ todo.content }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div v-if="processCollapsed" class="di-process__summary" @click="processCollapsed = false">
|
|
|
|
|
+ <span class="di-process__summary-icon">▸</span>
|
|
|
|
|
+ <span>查看处理过程</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div v-if="!props.loading" class="di-process__summary" @click="processCollapsed = true">
|
|
|
|
|
+ <span class="di-process__summary-icon">▾</span>
|
|
|
|
|
+ <span>收起处理过程</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="thinking" class="di-process__row di-process__thinking"><a-spin size="small" /><span>AI 正在思考...</span></div>
|
|
|
|
|
+ <div v-if="todos.length > 0" class="di-process__todos">
|
|
|
|
|
+ <div v-for="(todo, i) in todos" :key="i" class="di-todo-item" :class="`di-todo-item--${todo.status}`">
|
|
|
|
|
+ <span class="di-todo-item__icon">
|
|
|
|
|
+ <span v-if="todo.status === 'completed'" class="di-todo-check">✓</span>
|
|
|
|
|
+ <span v-else-if="todo.status === 'in_progress'" class="di-todo-spinner"></span>
|
|
|
|
|
+ <span v-else class="di-todo-dot"></span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="di-todo-item__text">{{ todo.content }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="props.latestMessage && loading" class="di-process__row di-process__message">
|
|
|
|
|
+ <span class="di-process__dot"></span><span>{{ props.latestMessage }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-if="loading && !hasContent && !hasProcess" class="di-loading"><a-spin tip="正在分析数据..." /></div>
|
|
|
|
|
|
|
+ <div v-if="loading && !hasContent" class="di-loading"><a-spin tip="正在分析数据..." /></div>
|
|
|
<div v-if="hasContent" class="di-markdown" v-html="renderedText"></div>
|
|
<div v-if="hasContent" class="di-markdown" v-html="renderedText"></div>
|
|
|
<div v-else-if="error && !loading" class="di-error">数据解读失败:{{ error }}</div>
|
|
<div v-else-if="error && !loading" class="di-error">数据解读失败:{{ error }}</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -36,7 +49,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
<script lang="ts" setup>
|
|
|
- import { computed } from 'vue';
|
|
|
|
|
|
|
+ import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
|
|
import { marked } from 'marked';
|
|
import { marked } from 'marked';
|
|
|
import type { TodoItem, ToolCallRecord } from '../types';
|
|
import type { TodoItem, ToolCallRecord } from '../types';
|
|
|
|
|
|
|
@@ -49,13 +62,66 @@
|
|
|
todos: TodoItem[];
|
|
todos: TodoItem[];
|
|
|
executingTools: string[];
|
|
executingTools: string[];
|
|
|
toolCalls: ToolCallRecord[];
|
|
toolCalls: ToolCallRecord[];
|
|
|
|
|
+ latestMessage: string;
|
|
|
}>();
|
|
}>();
|
|
|
|
|
|
|
|
defineEmits<{ close: [] }>();
|
|
defineEmits<{ close: [] }>();
|
|
|
|
|
|
|
|
|
|
+ const bodyRef = ref<HTMLDivElement>();
|
|
|
|
|
+ const isNearBottom = ref(true);
|
|
|
|
|
+
|
|
|
|
|
+ function checkNearBottom() {
|
|
|
|
|
+ const el = bodyRef.value;
|
|
|
|
|
+ if (!el) return;
|
|
|
|
|
+ isNearBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function scrollToBottom() {
|
|
|
|
|
+ const el = bodyRef.value;
|
|
|
|
|
+ if (!el) return;
|
|
|
|
|
+ el.scrollTop = el.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let scrollListenerAttached = false;
|
|
|
|
|
+
|
|
|
|
|
+ onMounted(() => {
|
|
|
|
|
+ const el = bodyRef.value;
|
|
|
|
|
+ if (el) {
|
|
|
|
|
+ el.addEventListener('scroll', checkNearBottom, { passive: true });
|
|
|
|
|
+ scrollListenerAttached = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ onBeforeUnmount(() => {
|
|
|
|
|
+ if (scrollListenerAttached && bodyRef.value) {
|
|
|
|
|
+ bodyRef.value.removeEventListener('scroll', checkNearBottom);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
const hasContent = computed(() => !!props.streamingText);
|
|
const hasContent = computed(() => !!props.streamingText);
|
|
|
- const hasProcess = computed(() => props.thinking || props.todos.length > 0 || props.executingTools.length > 0 || props.toolCalls.length > 0);
|
|
|
|
|
|
|
+ const hasProcess = computed(
|
|
|
|
|
+ () => props.thinking || props.todos.length > 0 || props.executingTools.length > 0 || props.toolCalls.length > 0 || !!props.latestMessage
|
|
|
|
|
+ );
|
|
|
const showProcess = computed(() => hasProcess.value);
|
|
const showProcess = computed(() => hasProcess.value);
|
|
|
|
|
+ const processCollapsed = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => props.loading,
|
|
|
|
|
+ (val, oldVal) => {
|
|
|
|
|
+ if (oldVal && !val && hasProcess.value) processCollapsed.value = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => [props.streamingText, props.latestMessage, props.todos],
|
|
|
|
|
+ () => {
|
|
|
|
|
+ if (isNearBottom.value) {
|
|
|
|
|
+ nextTick(scrollToBottom);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
const renderedText = computed(() => {
|
|
const renderedText = computed(() => {
|
|
|
if (!props.streamingText) return '';
|
|
if (!props.streamingText) return '';
|
|
|
return marked.parse(props.streamingText.replace(/<br\s*\/?>/gi, ' \n')) as string;
|
|
return marked.parse(props.streamingText.replace(/<br\s*\/?>/gi, ' \n')) as string;
|
|
@@ -84,7 +150,7 @@
|
|
|
border: 1px solid var(--vent-modal-border) !important;
|
|
border: 1px solid var(--vent-modal-border) !important;
|
|
|
-webkit-box-shadow: 0px 0px 10px var(--vent-modal-box-shadow) inset !important;
|
|
-webkit-box-shadow: 0px 0px 10px var(--vent-modal-box-shadow) inset !important;
|
|
|
box-shadow: 0px 0px 10px var(--vent-modal-box-shadow) inset !important;
|
|
box-shadow: 0px 0px 10px var(--vent-modal-box-shadow) inset !important;
|
|
|
- backdrop-filter: blur(10px);
|
|
|
|
|
|
|
+ backdrop-filter: blur(30px);
|
|
|
}
|
|
}
|
|
|
.ant-modal-body {
|
|
.ant-modal-body {
|
|
|
padding: 0;
|
|
padding: 0;
|
|
@@ -153,6 +219,42 @@
|
|
|
color: #c8a853;
|
|
color: #c8a853;
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
}
|
|
}
|
|
|
|
|
+ .di-process__message {
|
|
|
|
|
+ color: #8899aa;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .di-process__dot {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+ height: 6px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #4da6ff;
|
|
|
|
|
+ animation: di-pulse 1.4s ease-in-out infinite;
|
|
|
|
|
+ }
|
|
|
|
|
+ @keyframes di-pulse {
|
|
|
|
|
+ 0%,
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ opacity: 0.3;
|
|
|
|
|
+ }
|
|
|
|
|
+ 50% {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .di-process__summary {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ padding: 6px 0;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #8899aa;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: #4da6ff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .di-process__summary-icon {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
.di-process__todos {
|
|
.di-process__todos {
|
|
|
margin: 6px 0;
|
|
margin: 6px 0;
|
|
|
}
|
|
}
|
|
@@ -193,9 +295,22 @@
|
|
|
background: #4a5568;
|
|
background: #4a5568;
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
}
|
|
}
|
|
|
|
|
+ .di-todo-spinner {
|
|
|
|
|
+ width: 12px;
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+ border: 2px solid rgba(77, 166, 255, 0.25);
|
|
|
|
|
+ border-top-color: #4da6ff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: di-spin 0.8s linear infinite;
|
|
|
|
|
+ }
|
|
|
|
|
+ @keyframes di-spin {
|
|
|
|
|
+ to {
|
|
|
|
|
+ transform: rotate(360deg);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
.di-markdown {
|
|
.di-markdown {
|
|
|
line-height: 1.8;
|
|
line-height: 1.8;
|
|
|
- color: #c8d6e5;
|
|
|
|
|
|
|
+ color: #fff;
|
|
|
word-break: break-word;
|
|
word-break: break-word;
|
|
|
:deep(h1),
|
|
:deep(h1),
|
|
|
:deep(h2),
|
|
:deep(h2),
|
|
@@ -245,7 +360,7 @@
|
|
|
}
|
|
}
|
|
|
th {
|
|
th {
|
|
|
background: rgba(30, 144, 255, 0.1);
|
|
background: rgba(30, 144, 255, 0.1);
|
|
|
- color: #4da6ff;
|
|
|
|
|
|
|
+ color: #000;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|