新增: 后台任务卡徽章+轮次投影(convRounds), 可恢复分区(parked), reattachToMain i18n 补键(#12)

This commit is contained in:
lxy
2026-10-06 12:13:33 +08:00
parent 70d6e98f82
commit 04db8c0d3b
11 changed files with 250 additions and 32 deletions
+145 -5
View File
@@ -151,6 +151,30 @@
<span class="ai-task-progress-fill" :style="{ width: taskProgressPct + '%' }"></span>
</span>
</div>
<!-- #14 桌面任务卡:后台在途任务(多会话并发生成中,点击切回) -->
<div v-if="runningTasks.length || recoverableTasks.length" class="ai-running-tasks-inline">
<button type="button" class="ai-running-tasks-badge ai-tool-badge" :class="{ 'is-active': runningTasksExpanded }" @click="togglePanel('running')" :title="$t('aiChat.runningTasks')">
<span class="ai-running-tasks-pulse"></span>
{{ runningTasks.length }}
</button>
<Transition name="ai-pop">
<div v-if="runningTasksExpanded" class="ai-running-tasks-list">
<button v-for="task in runningTasks" :key="task.id" type="button" class="ai-running-task-item" @click="switchToRunning(task.id)">
<span class="ai-running-task-dot"></span>
<span class="ai-running-task-title">{{ task.title }}</span>
<span class="ai-running-task-round">{{ t('aiChat.runningRounds', { n: task.rounds }) }}</span>
</button>
<template v-if="recoverableTasks.length">
<div class="ai-running-tasks-sep"></div>
<button v-for="task in recoverableTasks" :key="task.id" type="button" class="ai-running-task-item" @click="switchToRunning(task.id)">
<span class="ai-running-task-dot ai-running-task-dot--parked"></span>
<span class="ai-running-task-title">{{ task.title }}</span>
<span class="ai-running-task-round ai-running-task-round--parked">{{ t('aiChat.recoverableTask') }}</span>
</button>
</template>
</div>
</Transition>
</div>
<!-- 目标计数 -->
<div v-if="goals.length" class="ai-goals-inline">
<button type="button" class="ai-goals-inline-badge ai-tool-badge" :class="{ 'is-active': goalsExpanded }" @click="togglePanel('goals')" :title="$t('aiChat.viewGoals')">
@@ -258,6 +282,8 @@ import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { aiApi } from '../../api/ai'
import { pendingTaskProgress } from '../../composables/ai/useAiPendingState'
import { convRounds } from '../../composables/ai/aiShared'
import { switchConversation } from '../../composables/ai/useAiConversations'
import type { AiMessage, ModelConfig, GoalEntry } from '../../api/types'
defineProps<{
@@ -414,6 +440,36 @@ const taskProgress = computed(() => {
const agenticProgressVisible = computed(() =>
store.state.streaming && store.state.agentRound > 0,
)
// ── #14 桌面任务卡:后台在途任务列表(多会话并发,切走后可见可切回) ──
// 数据源=convRounds(轮次追踪,事件写)× store.isGenerating(状态,同源 convStates),
// 排除当前会话(自身进度已有 agentic 实时进度条,不重复)。响应式:convRounds 是
// aiShared 模块级 reactive Map,computed 内遍历即建依赖。
const runningTasksExpanded = ref(false)
const runningTasks = computed(() => {
const items: { id: string; title: string; rounds: number }[] = []
for (const [id, rounds] of convRounds) {
if (id === store.state.activeConversationId) continue
if (!store.isGenerating(id)) continue
const conv = store.state.conversations.find(c => c.id === id)
items.push({ id, title: conv?.title || t('aiChat.untitledConversation'), rounds })
}
return items
})
// V49 可恢复入口(断点续跑读侧):conversations 中 loop_state='parked' 的会话(重启中断待续),
// 独立分区展示在任务面板下方,点击切回(继续与否由用户在会话内决定,不做自动续跑)
const recoverableTasks = computed(() => {
return store.state.conversations
.filter(c => c.loop_state === 'parked' && c.id !== store.state.activeConversationId)
.slice(0, 10)
.map(c => ({ id: c.id, title: c.title || t('aiChat.untitledConversation') }))
})
async function switchToRunning(id: string) {
runningTasksExpanded.value = false
if (id !== store.state.activeConversationId) {
await switchConversation(id)
}
}
const taskProgressText = computed(() => {
const p = taskProgress.value
if (!p) return ''
@@ -531,11 +587,11 @@ interface ContextInfo {
}
const contextExpanded = ref(false)
/** 弹出面板互斥:goals/history/summary/context 四个 badge 面板,点一个收起其他,再点当前切换。 */
function togglePanel(panel: 'goals' | 'history' | 'summary' | 'context') {
const refs = { goals: goalsExpanded, history: historyExpanded, summary: summaryExpanded, context: contextExpanded }
/** 弹出面板互斥:goals/history/summary/context/running 五个 badge 面板,点一个收起其他,再点当前切换。 */
function togglePanel(panel: 'goals' | 'history' | 'summary' | 'context' | 'running') {
const refs = { goals: goalsExpanded, history: historyExpanded, summary: summaryExpanded, context: contextExpanded, running: runningTasksExpanded }
const willOpen = !refs[panel].value
goalsExpanded.value = historyExpanded.value = summaryExpanded.value = contextExpanded.value = false
goalsExpanded.value = historyExpanded.value = summaryExpanded.value = contextExpanded.value = runningTasksExpanded.value = false
refs[panel].value = willOpen
}
@@ -754,7 +810,7 @@ onBeforeUnmount(() => {
background: transparent;
color: var(--df-text-dim);
cursor: pointer;
transition: all 0.15s;
transition: background 0.15s, color 0.15s;
}
.ai-btn-icon:hover {
background: var(--df-bg-card);
@@ -954,6 +1010,90 @@ onBeforeUnmount(() => {
display: inline-flex;
align-items: center;
}
/* ── #14 桌面任务卡:后台在途任务徽章+面板 ── */
.ai-running-tasks-inline {
position: relative;
display: inline-flex;
align-items: center;
}
.ai-running-tasks-badge {
display: inline-flex;
align-items: center;
gap: 4px;
}
.ai-running-tasks-pulse {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--df-success, #34c77b);
animation: ai-running-pulse 2s ease-in-out infinite;
}
@keyframes ai-running-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.ai-running-tasks-list {
position: absolute;
left: auto; right: 0;
top: 100%;
z-index: 100;
min-width: 260px;
max-width: 400px;
max-height: 300px;
overflow-y: auto;
background: var(--df-bg);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-sm);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 4px 0;
}
.ai-running-task-item {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 5px 10px;
border: none;
background: none;
cursor: pointer;
text-align: left;
font-size: 12px;
color: var(--df-text);
}
.ai-running-task-item:hover {
background: var(--df-hover, rgba(128,128,128,0.08));
}
.ai-running-task-dot {
flex-shrink: 0;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--df-success, #34c77b);
}
.ai-running-task-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-running-task-round {
flex-shrink: 0;
font-size: 11px;
color: var(--df-text-secondary, #888);
}
/* V49 可恢复分区:琥珀点+「可恢复」徽标,与进行中(绿)区分 */
.ai-running-tasks-sep {
height: 1px;
margin: 4px 8px;
background: var(--df-border);
}
.ai-running-task-dot--parked {
background: var(--df-warning, #e6a23c);
}
.ai-running-task-round--parked {
color: var(--df-warning, #e6a23c);
}
.ai-history-inline-list {
position: absolute;
left: auto; right: 0;