重构: 前端God文件拆分(StreamRenderer+EmptyState+MentionPopover+EnrichmentPanel)
- useStreamRenderer.ts: 流式Markdown块级渲染提取为composable(220行) - EmptyState.vue: 空状态独立组件(无provider引导+示例问题) - MentionPopover.vue: @实体联想浮层独立组件 - EnrichmentPanel.vue: @项目enrichment预览面板独立组件 - MessageList.vue: 1386→1148行, ChatInput.vue: 1155→1007行 - AiChat进度条: completedTools计数器(AiToolCallCompleted+1,AiAgentRound重置) - i18n: 进度条文案加已完成工具数(中英文)
This commit is contained in:
+26
-174
@@ -29,31 +29,17 @@
|
||||
@drop="onDrop"
|
||||
></textarea>
|
||||
|
||||
<!-- UX-10 §1.4: @ 实体引用浮层(@ 触发,选中插入 [类型:名] 标记,架构复用技能 popover) -->
|
||||
<div v-if="mentionOpen" class="ai-skill-popover ai-mention-popover">
|
||||
<div v-if="!mentionItems.length" class="ai-skill-empty">
|
||||
{{ mentionTotal ? $t('aiChat.mentionNoMatch') : $t('aiChat.mentionEmpty') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<template v-for="g in mentionGroups" :key="g.key">
|
||||
<div v-if="g.items.length" class="ai-mention-group">{{ g.label }}</div>
|
||||
<div
|
||||
v-for="item in g.items"
|
||||
:key="item.uid"
|
||||
class="ai-skill-item"
|
||||
:class="{ 'ai-skill-item--active': mentionFlatIndex(item.uid) === mentionIndex }"
|
||||
@click="selectMention(item)"
|
||||
@mouseenter="mentionIndex = mentionFlatIndex(item.uid)"
|
||||
>
|
||||
<div class="ai-skill-item-row">
|
||||
<span class="ai-mention-item-type" :class="'ai-mention-item-type--' + item.type">{{ item.typeLabel }}</span>
|
||||
<span class="ai-mention-item-name">{{ item.name }}</span>
|
||||
<span class="ai-mention-item-desc">{{ item.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- UX-10 §1.4: @ 实体引用浮层(提取至 MentionPopover.vue,选中插入 [类型:名] 标记) -->
|
||||
<MentionPopover
|
||||
v-if="mentionOpen"
|
||||
:mention-items="mentionItems"
|
||||
:mention-groups="mentionGroups"
|
||||
:mention-total="mentionTotal"
|
||||
:mention-index="mentionIndex"
|
||||
:mention-flat-index="mentionFlatIndex"
|
||||
@select="selectMention"
|
||||
@update:mention-index="mentionIndex = $event"
|
||||
/>
|
||||
<!-- L2 状态机停止按钮三态(批2 1c):conv_state 派生 generating/stopping/error 三态,
|
||||
idle(或 conv_state 未收到回退旧 streaming=false)显发送按钮。双轨过渡不强制全替旧 bool。 -->
|
||||
<button
|
||||
@@ -90,26 +76,15 @@
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- ⑥.4 Phase 4: @项目展开摘要(输入区 enrichment 预览) -->
|
||||
<div v-if="hasEnrichment" class="ai-enrichment-bar">
|
||||
<button
|
||||
class="ai-enrichment-toggle"
|
||||
:class="{ 'ai-enrichment-toggle--expanded': enrichmentExpanded }"
|
||||
@click="enrichmentExpanded = !enrichmentExpanded"
|
||||
:title="enrichmentExpanded ? $t('aiChat.enrichmentCollapse') : $t('aiChat.enrichmentExpand')"
|
||||
:aria-label="enrichmentExpanded ? $t('aiChat.enrichmentCollapse') : $t('aiChat.enrichmentExpand')"
|
||||
>
|
||||
<svg class="ai-enrichment-chevron" width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
|
||||
<path d="M2 3l3 4 3-4" />
|
||||
</svg>
|
||||
<span class="ai-enrichment-badge">{{ enrichmentBadgeText }}</span>
|
||||
</button>
|
||||
<button class="ai-enrichment-dismiss" @click="dismissEnrichment" title="取消关联">×</button>
|
||||
<div v-if="enrichmentExpanded" class="ai-enrichment-panel">
|
||||
<div class="ai-enrichment-label">{{ $t('aiChat.enrichmentLabel') }}</div>
|
||||
<pre class="ai-enrichment-body"><code>{{ enrichmentDetailText }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ⑥.4 Phase 4: @项目展开摘要(提取至 EnrichmentPanel.vue) -->
|
||||
<EnrichmentPanel
|
||||
:has-enrichment="hasEnrichment"
|
||||
:enrichment-badge-text="enrichmentBadgeText"
|
||||
:enrichment-detail-text="enrichmentDetailText"
|
||||
:enrichment-expanded="enrichmentExpanded"
|
||||
@dismiss="dismissEnrichment"
|
||||
@update:enrichment-expanded="enrichmentExpanded = $event"
|
||||
/>
|
||||
<div class="ai-input-hint">{{ $t('aiChat.inputHint') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -130,7 +105,10 @@ import { useProjectStore } from '../../stores/project'
|
||||
import { getConvState } from '../../composables/ai/useAiEvents'
|
||||
import SkillMention from './SkillMention.vue'
|
||||
import ImageInput from './ImageInput.vue'
|
||||
import MentionPopover from './MentionPopover.vue'
|
||||
import EnrichmentPanel from './EnrichmentPanel.vue'
|
||||
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord, MentionSpan } from '../../api/types'
|
||||
import type { MentionItem } from './MentionItem'
|
||||
|
||||
const props = defineProps<{
|
||||
/** UX-09: 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
|
||||
@@ -354,16 +332,7 @@ function clearSkill() {
|
||||
// 联想源只读 projectsStore.projects / projectsStore.tasks(前端 store 现有数据,不新增 IPC)。
|
||||
// 触发:输入框光标前最近的 @ 后无空白/换行 → 激活;Esc/光标移开/选中插入 → 关闭。
|
||||
// Enter 不冲突:popover 开时 Enter 插入选中(覆盖发送),关时 Enter 发送(原逻辑)。
|
||||
interface MentionItem {
|
||||
type: 'project' | 'task' | 'idea'
|
||||
/** 扁平后唯一 key(用于 key/导航扁平索引定位) */
|
||||
uid: string
|
||||
/** 实体原始 id(供后端解析,目前仅插入 [类型:名] 文本,id 未透传) */
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
typeLabel: string
|
||||
}
|
||||
// MentionItem 接口已抽至 ./MentionItem.ts(MentionPopover 子组件共用)。
|
||||
|
||||
const mentionOpen = ref(false)
|
||||
const mentionIndex = ref(0)
|
||||
@@ -962,44 +931,7 @@ defineExpose({
|
||||
}
|
||||
.ai-skill-empty { padding: 8px 12px; color: var(--df-text-dim); opacity: 0.7; font-size: 12px; }
|
||||
|
||||
/* ── UX-10 §1.4: @ 实体引用浮层(复用 .ai-skill-popover 容器,仅新增组标题/类型标签) ── */
|
||||
.ai-mention-popover { /* 同 .ai-skill-popover 定位,这里仅为语义占位,样式继承 */ }
|
||||
.ai-mention-group {
|
||||
padding: 4px 10px 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--df-text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
user-select: none;
|
||||
}
|
||||
.ai-mention-item-name { color: var(--df-text); font-weight: 500; flex-shrink: 0; }
|
||||
.ai-mention-item-desc {
|
||||
color: var(--df-text-dim);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 类型标签小色块(项目/任务区分) */
|
||||
.ai-mention-item-type {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 5px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
background: var(--df-bg-hover);
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
.ai-mention-item-type--project {
|
||||
background: color-mix(in srgb, var(--df-accent) 16%, transparent);
|
||||
color: var(--df-accent);
|
||||
}
|
||||
.ai-mention-item-type--task {
|
||||
background: color-mix(in srgb, var(--df-success) 16%, transparent);
|
||||
color: var(--df-success);
|
||||
}
|
||||
/* ── UX-10 §1.4: @ 实体引用浮层样式已随 MentionPopover.vue 提取 ── */
|
||||
|
||||
.ai-input {
|
||||
flex: 1;
|
||||
@@ -1071,85 +1003,5 @@ defineExpose({
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ── ⑥.4 Phase 4: @项目展开摘要(输入区 enrichment 预览) ── */
|
||||
.ai-enrichment-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
border: 0.5px solid color-mix(in srgb, var(--df-accent) 30%, transparent);
|
||||
border-radius: var(--df-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ai-enrichment-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--df-accent) 10%, transparent);
|
||||
color: var(--df-text);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.ai-enrichment-toggle:hover {
|
||||
background: color-mix(in srgb, var(--df-accent) 18%, transparent);
|
||||
}
|
||||
.ai-enrichment-toggle--expanded {
|
||||
border-bottom: 0.5px solid color-mix(in srgb, var(--df-accent) 20%, transparent);
|
||||
}
|
||||
.ai-enrichment-chevron {
|
||||
flex-shrink: 0;
|
||||
color: var(--df-text-dim);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.ai-enrichment-toggle--expanded .ai-enrichment-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.ai-enrichment-dismiss {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.ai-enrichment-dismiss:hover {
|
||||
color: var(--df-danger);
|
||||
}
|
||||
.ai-enrichment-badge {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--df-accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
.ai-enrichment-panel {
|
||||
padding: 6px 8px;
|
||||
background: var(--df-bg);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ai-enrichment-label {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ai-enrichment-body {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
color: var(--df-text);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: var(--df-font-mono, ui-monospace, monospace);
|
||||
}
|
||||
/* ── ⑥.4 Phase 4: @项目展开摘要样式已随 EnrichmentPanel.vue 提取 ── */
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user