新增: 后台任务卡徽章+轮次投影(convRounds), 可恢复分区(parked), reattachToMain i18n 补键(#12)
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user