| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- <template>
- <div class="ai-status">
- <span class="ai-status__dot"></span>
- <span class="ai-status__phase">{{ phaseText }}</span>
- <span class="ai-status__elapsed">{{ elapsed }}s</span>
- <template v-if="intent?.intent">
- <span class="ai-status__sep"></span>
- <a-tag class="ai-status__tag" :bordered="false">{{ shortIntent }}</a-tag>
- <a-tooltip v-if="intentDetail" placement="topLeft" :title="intentDetail">
- <span class="ai-status__hint">意图详情</span>
- </a-tooltip>
- </template>
- </div>
- </template>
- <script lang="ts" setup>
- /**
- * 流式状态条
- *
- * 让用户随时知道 AI 当前处于哪个阶段(识别意图 / 调用工具 / 生成回答),
- * 并展示意图识别结果与耗时 —— 避免长任务期间界面"看起来卡住了"。
- */
- import { computed, onBeforeUnmount, ref, watch } from 'vue';
- import { Tag, Tooltip } from 'ant-design-vue';
- import type { SseIntentData, ToolStep } from '../api/types';
- import { toolLabel } from '../utils/constants';
- const ATag = Tag;
- const ATooltip = Tooltip;
- const props = defineProps<{
- streaming: boolean;
- intent?: SseIntentData;
- steps?: ToolStep[];
- /** 是否已开始输出正文 */
- hasContent?: boolean;
- /** 模型是否正在输出思考过程 */
- thinkingActive?: boolean;
- }>();
- const elapsed = ref(0);
- let timer: ReturnType<typeof setInterval> | null = null;
- function startTimer() {
- elapsed.value = 0;
- stopTimer();
- timer = setInterval(() => (elapsed.value += 1), 1000);
- }
- function stopTimer() {
- if (timer) {
- clearInterval(timer);
- timer = null;
- }
- }
- watch(
- () => props.streaming,
- (val) => (val ? startTimer() : stopTimer()),
- { immediate: true },
- );
- onBeforeUnmount(stopTimer);
- const runningTool = computed(() => props.steps?.find((s) => s.status === 'running'));
- const phaseText = computed(() => {
- if (runningTool.value) return `正在执行:${toolLabel(runningTool.value.toolName)}`;
- if (props.hasContent) return '正在生成回答';
- if (props.thinkingActive) return '正在深度思考';
- if (props.steps?.length) return '正在整理结果';
- if (props.intent) return '正在规划分析步骤';
- return '正在理解问题';
- });
- const shortIntent = computed(() => {
- const raw = props.intent?.intent || '';
- return raw.replace(/^《|》$/g, '').slice(0, 12) || '意图';
- });
- const intentDetail = computed(() => {
- const parts: string[] = [];
- if (props.intent?.enhanced) parts.push(`增强问题:${props.intent.enhanced}`);
- if (props.intent?.entities?.length) parts.push(`关键实体:${props.intent.entities.join('、')}`);
- return parts.join('\n');
- });
- </script>
- <style lang="less" scoped>
- .ai-status {
- display: flex;
- align-items: center;
- gap: var(--ai-sp-2);
- margin-bottom: var(--ai-sp-2);
- font-size: 12px;
- color: var(--ai-text-2);
- &__dot {
- flex: 0 0 auto;
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: @primary-color;
- animation: ai-status-pulse 1.2s ease-in-out infinite;
- }
- &__phase {
- color: var(--ai-text);
- }
- &__elapsed {
- font-variant-numeric: tabular-nums;
- opacity: 0.75;
- }
- &__sep {
- width: 1px;
- height: 12px;
- background: var(--ai-border);
- }
- &__tag {
- margin: 0;
- font-size: 11px;
- line-height: 18px;
- color: @primary-color;
- background: var(--ai-primary-soft);
- }
- &__hint {
- font-size: 11px;
- color: var(--ai-text-2);
- border-bottom: 1px dashed var(--ai-border-strong);
- cursor: help;
- }
- }
- @keyframes ai-status-pulse {
- 0%,
- 100% {
- opacity: 1;
- transform: scale(1);
- }
- 50% {
- opacity: 0.35;
- transform: scale(0.82);
- }
- }
- </style>
|