From 7708e26851dd9c8344d8355c005e33529d078a6f Mon Sep 17 00:00:00 2001 From: matevip Date: Mon, 4 May 2026 14:00:25 +0800 Subject: [PATCH] feat(backstage): avatar status ring, hero focus panel, brand-tone time dial --- .../backstage/BackstageFocusPanel.vue | 766 ++++++++++++++++++ .../src/composables/useBackstageAgent.ts | 102 +++ mateclaw-ui/src/i18n/locales/en-US.ts | 2 + mateclaw-ui/src/i18n/locales/zh-CN.ts | 2 + mateclaw-ui/src/views/Backstage.vue | 739 ++++++----------- 5 files changed, 1121 insertions(+), 490 deletions(-) create mode 100644 mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue create mode 100644 mateclaw-ui/src/composables/useBackstageAgent.ts diff --git a/mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue b/mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue new file mode 100644 index 00000000..735a3d0a --- /dev/null +++ b/mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue @@ -0,0 +1,766 @@ + + + + + diff --git a/mateclaw-ui/src/composables/useBackstageAgent.ts b/mateclaw-ui/src/composables/useBackstageAgent.ts new file mode 100644 index 00000000..ed1fe25c --- /dev/null +++ b/mateclaw-ui/src/composables/useBackstageAgent.ts @@ -0,0 +1,102 @@ +/** + * Shared display helpers for the Backstage page and its child components. + * Pure formatting and classification — no API calls, no shared state. + * + * The parent (Backstage.vue) and the focus panel both render agent cards + * with avatars, status rings, human sentences, and elapsed-time strings; + * keeping these in one place avoids drift between the two surfaces when + * a status string or ring rule changes. + */ +import { useI18n } from 'vue-i18n' +import type { BackstageRunCard, BackstageSubagentCard } from '@/api' + +type AnyRun = BackstageRunCard | BackstageSubagentCard + +export function useBackstageAgent() { + const { t } = useI18n() + + function avatarLetter(run: AnyRun): string { + const name = run.agentName || (run as any).username || (run as any).subagentId || '?' + return name.charAt(0).toUpperCase() + } + + /** + * Soft, low-saturation gradient seeded from agent name. Even when an icon + * is present we use this as the avatar surface — Apple's wallpaper-behind- + * icon trick, not a billboard. + */ + function avatarBgStyle(run: AnyRun) { + const seed = run.agentName || (run as any).conversationId || (run as any).subagentId || 'x' + let h = 0 + for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0 + const hue = Math.abs(h) % 360 + return { + background: `linear-gradient(140deg, hsl(${hue}, 55%, 92%), hsl(${(hue + 30) % 360}, 50%, 86%))`, + color: `hsl(${hue}, 45%, 28%)`, + } + } + + /** + * Status ring classification — four moods: + * - thinking: alive but no first token yet (and still young) — spinner arc + * - stuck: explicit stuckReason — slow, saturated breathing + * - orphan: nobody listening — faint, almost-still + * - healthy: streaming or settled — gentle 3s breath + * + * Subagent cards lack stuck/orphan/firstToken flags, so they fall through + * to healthy — which is what you'd want for a helper that's still around. + */ + function ringClass(run: AnyRun): Record { + const r = run as BackstageRunCard + if (r.stuckReason) return { 'ring-stuck': true } + if (r.orphan) return { 'ring-orphan': true } + if (r.firstTokenReceived === false && r.ageMs < 30_000) return { 'ring-thinking': true } + return { 'ring-healthy': true } + } + + function dotTitle(run: AnyRun): string { + const r = run as BackstageRunCard + if (r.stuckReason) return t('backstage.dotTitle.stuck') + if (r.orphan) return t('backstage.dotTitle.orphan') + return t('backstage.dotTitle.healthy') + } + + function humanSentence(run: BackstageRunCard): string { + if (run.stuckReason === 'tool_silent') return t('backstage.saying.toolSilent', { tool: run.runningToolName || t('backstage.aTool') }) + if (run.stuckReason === 'idle_silent') return t('backstage.saying.idleSilent') + if (run.stuckReason === 'hard_cap') return t('backstage.saying.hardCap') + if (run.currentPhase === 'awaiting_approval') return t('backstage.saying.awaitingApproval') + if (run.runningToolName) return t('backstage.saying.usingTool', { tool: run.runningToolName }) + if (run.currentPhase === 'executing_tool') return t('backstage.saying.usingSomething') + if (run.currentPhase === 'summarizing') return t('backstage.saying.wrappingUp') + if (run.currentPhase === 'planning') return t('backstage.saying.planning') + if (!run.firstTokenReceived) return t('backstage.saying.thinking') + return t('backstage.saying.replying') + } + + function formatAge(ms: number): string { + if (ms < 1500) return t('backstage.time.justNow') + const sec = Math.floor(ms / 1000) + if (sec < 60) return t('backstage.time.seconds', { n: sec }) + const min = Math.floor(sec / 60) + if (min < 60) return t('backstage.time.minutes', { n: min, s: sec % 60 }) + const hr = Math.floor(min / 60) + return t('backstage.time.hours', { n: hr, m: min % 60 }) + } + + function stuckCallout(run: BackstageRunCard): string { + if (run.stuckReason === 'tool_silent') return t('backstage.callout.toolSilent', { tool: run.runningToolName || t('backstage.aTool'), time: formatAge(run.msSinceLastEvent) }) + if (run.stuckReason === 'idle_silent') return t('backstage.callout.idleSilent', { time: formatAge(run.msSinceLastEvent) }) + return t('backstage.callout.hardCap', { time: formatAge(run.ageMs) }) + } + + return { + avatarLetter, + avatarBgStyle, + ringClass, + dotTitle, + humanSentence, + stuckCallout, + formatAge, + } +} diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index cabb44a8..fd685a52 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -373,6 +373,7 @@ export default { noOneListening: 'No one watching the chat right now.', peopleListening: '{n} listening', helpers: 'Helpers', + session: 'Session', }, actions: { live: 'Live', @@ -384,6 +385,7 @@ export default { stopHint: 'Ask it to wind down at the next checkpoint.', endIt: 'End it', endHint: 'Force it to stop now.', + close: 'Close', }, confirm: { stopTitle: 'Stop this agent?', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 90e151b4..daf87e14 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1553,6 +1553,7 @@ export default { noOneListening: '当前没有人在看这个对话。', peopleListening: '{n} 人在看', helpers: '帮手', + session: '会话', }, actions: { live: '实时', @@ -1564,6 +1565,7 @@ export default { stopHint: '让它在下个检查点停下来。', endIt: '强制结束', endHint: '立即强制停止。', + close: '关闭', }, confirm: { stopTitle: '让这个智能体停下吗?', diff --git a/mateclaw-ui/src/views/Backstage.vue b/mateclaw-ui/src/views/Backstage.vue index d9bdd4ea..606d3778 100644 --- a/mateclaw-ui/src/views/Backstage.vue +++ b/mateclaw-ui/src/views/Backstage.vue @@ -80,36 +80,31 @@ :class="cardClass(run)" @click="openDetail(run)" > - +
-
- - {{ avatarLetter(run) }} +
+
+ + {{ avatarLetter(run) }} +
{{ run.agentName || t('backstage.unknownAgent') }}
@{{ run.username }}
-
- -
{{ humanSentence(run) }}
- +
{{ formatAge(run.ageMs) }} - #{{ shortId(run.conversationId) }} - - {{ t('backstage.subagentsBadge', { n: run.subagentCount }) }} - {{ t('backstage.orphan') }} @@ -118,19 +113,36 @@
- +
- - +
+
+ + {{ avatarLetter(sub) }} +
+
+ +{{ childrenOf(run).length - 3 }} +
+
+
+ + +
@@ -138,80 +150,17 @@ - - -
-
-
- - {{ avatarLetter(detail) }} -
-
-
{{ detail.agentName || t('backstage.unknownAgent') }}
-
@{{ detail.username }}
-
- -
- -

{{ humanSentence(detail) }}

- -
- {{ stuckCallout(detail) }} -
- -
-
-
{{ t('backstage.detail.runningFor') }}
-
{{ formatAge(detail.ageMs) }}
-
-
-
{{ t('backstage.detail.lastHeard') }}
-
{{ formatAge(detail.msSinceLastEvent) }} {{ t('backstage.detail.ago') }}
-
-
-
{{ t('backstage.detail.audience') }}
-
- - {{ t('backstage.detail.noOneListening') }} - - {{ t('backstage.detail.peopleListening', { n: detail.subscriberCount }) }} -
-
-
- -
-
{{ t('backstage.detail.helpers') }}
-
-
- - {{ avatarLetter(sub) }} -
-
-
{{ sub.agentName || sub.subagentId }}
-
{{ sub.lastTool || sub.currentPhase || sub.status }} · {{ formatAge(sub.ageMs) }}
-
- -
-
- -
- - -
-
-
+ +