|
|
@@ -6,6 +6,9 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
import AppIcon from '@/components/common/AppIcon.vue'
|
|
|
import MessageFailureNotice from '@/components/messages/MessageFailureNotice.vue'
|
|
|
import { messageFailure } from '@/utils/messageFailure'
|
|
|
+import TaskResultNotice from '@/components/workspace/TaskResultNotice.vue'
|
|
|
+import MessageSystemEvent from '@/components/messages/MessageSystemEvent.vue'
|
|
|
+import { systemMessageNotice } from '@/utils/systemMessageNotice'
|
|
|
import AgentAvatar from '@/components/common/AgentAvatar.vue'
|
|
|
import EmptyState from '@/components/common/EmptyState.vue'
|
|
|
import TypingIndicator from '@/components/common/TypingIndicator.vue'
|
|
|
@@ -107,34 +110,6 @@ function avatarState(message: UiMessage): 'idle' | 'working' | 'waiting' | 'load
|
|
|
return 'idle'
|
|
|
}
|
|
|
|
|
|
-function delegationSummary(metadata?: Record<string, unknown>): string {
|
|
|
- const total = Number(metadata?.task_count ?? 0)
|
|
|
- const completed = Number(metadata?.completed_count ?? 0)
|
|
|
- const failed = Number(metadata?.failed_count ?? 0)
|
|
|
- const seconds = Number(metadata?.duration_seconds ?? 0)
|
|
|
- const duration = seconds > 0 ? ` · ${seconds < 60 ? `${Math.round(seconds)} 秒` : `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`}` : ''
|
|
|
- return `${total || completed + failed || 1} 个子任务 · ${completed} 已完成${failed ? ` · ${failed} 失败` : ''}${duration}`
|
|
|
-}
|
|
|
-
|
|
|
-function systemSummary(content: string): string {
|
|
|
- const model = content.match(/(?:changed to|切换为)\s*([^\s,。\]]+?)(?=\s+via\b|\s*$|\])/i)?.[1]
|
|
|
- if (model) return `模型已切换:${model}`
|
|
|
- return '系统信息'
|
|
|
-}
|
|
|
-
|
|
|
-function backgroundProcessSummary(metadata?: Record<string, unknown>): string {
|
|
|
- const exitCode = Number(metadata?.exit_code)
|
|
|
- const signal = typeof metadata?.signal === 'string' ? metadata.signal : ''
|
|
|
- if (exitCode === 0) return '后台子任务已完成'
|
|
|
- if (signal) return `后台子任务已终止 · ${signal}`
|
|
|
- return Number.isFinite(exitCode) ? `后台子任务失败 · 退出码 ${exitCode}` : '后台子任务已结束'
|
|
|
-}
|
|
|
-
|
|
|
-function isCompactionEvent(message: UiMessage): boolean {
|
|
|
- return message.timelineMetadata?.eventKind === 'compaction'
|
|
|
- || message.content.trim().toLocaleLowerCase().startsWith('[context compaction')
|
|
|
-}
|
|
|
-
|
|
|
function onScroll() {
|
|
|
const el = scroller.value
|
|
|
if (!el) return
|
|
|
@@ -265,7 +240,9 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
|
|
|
class="message"
|
|
|
:class="[`message--${message.role}`, {
|
|
|
'message--failed': message.status === 'failed',
|
|
|
- 'message--failure-notice': messageFailure(message)?.replacesContent,
|
|
|
+ 'message--failure-notice': messageFailure(message)?.replacesContent && !message.attachments?.length,
|
|
|
+ 'message--has-failure': !!messageFailure(message),
|
|
|
+ 'message--notice': !!systemMessageNotice(message),
|
|
|
'message--tool-only': message.role === 'assistant' && !message.content && !message.reasoning && Boolean(message.tools?.length),
|
|
|
'message--assistant-internal': message.role === 'assistant' && !message.content.trim(),
|
|
|
'message--assistant-anonymous': message.role === 'assistant' && !showAssistantIdentity,
|
|
|
@@ -274,30 +251,8 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
|
|
|
<AgentAvatar v-if="message.role !== 'user' && (message.role !== 'assistant' || showAssistantIdentity)" class="message__avatar" :name="message.author || message.profile || (message.role === 'assistant' ? '夭' : '系')" :avatar="message.profile ? agentAvatars[message.profile] || '' : ''" :size="27" :state="avatarState(message)" />
|
|
|
<div class="message__body">
|
|
|
<MessageFailureNotice v-if="messageFailure(message)?.replacesContent && !message.attachments?.length" :failure="messageFailure(message)!" />
|
|
|
- <template v-else-if="message.timelineKind === 'delegation-complete'">
|
|
|
- <details class="delegation-event">
|
|
|
- <summary><AppIcon name="groups" :size="14" /><span><strong>子任务已完成</strong><small>{{ delegationSummary(message.timelineMetadata) }}</small></span></summary>
|
|
|
- <MarkdownContent process-content v-if="message.content" :content="message.content" />
|
|
|
- </details>
|
|
|
- </template>
|
|
|
- <template v-else-if="message.timelineKind === 'background-process'">
|
|
|
- <details class="system-event background-process-event">
|
|
|
- <summary><AppIcon name="tools" :size="13" /><span>{{ backgroundProcessSummary(message.timelineMetadata) }}</span></summary>
|
|
|
- <MarkdownContent process-content v-if="message.content" :content="message.content" />
|
|
|
- </details>
|
|
|
- </template>
|
|
|
- <template v-else-if="message.timelineKind === 'system' && isCompactionEvent(message)">
|
|
|
- <details class="delegation-event compaction-event">
|
|
|
- <summary><AppIcon name="archive" :size="14" /><span><strong>上下文已压缩</strong><small>压缩摘要已归档 · 点击查看</small></span></summary>
|
|
|
- <MarkdownContent process-content v-if="message.content" :content="message.content" />
|
|
|
- </details>
|
|
|
- </template>
|
|
|
- <template v-else-if="message.timelineKind === 'system'">
|
|
|
- <details class="system-event">
|
|
|
- <summary><AppIcon name="settings" :size="13" /><span>{{ systemSummary(message.content) }}</span></summary>
|
|
|
- <MarkdownContent process-content v-if="message.content" :content="message.content" />
|
|
|
- </details>
|
|
|
- </template>
|
|
|
+ <TaskResultNotice v-else-if="message.role === 'system' && message.taskReference" :content="message.content" :task-reference="message.taskReference" />
|
|
|
+ <MessageSystemEvent v-else-if="systemMessageNotice(message)" :message="message" />
|
|
|
<template v-else>
|
|
|
<div v-if="message.role !== 'assistant' || showAssistantIdentity" class="message__meta">
|
|
|
<strong><AppIcon v-if="message.isRemoteAgent" class="message__remote-agent" name="globe" :size="12" />{{ message.role === 'user' ? '你' : message.author || message.profile || (message.role === 'assistant' ? '机器人' : '系统') }}</strong>
|
|
|
@@ -358,7 +313,6 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
|
|
|
<ToolTrace v-for="tool in message.tools" :key="tool.id" :tool="tool" />
|
|
|
</div>
|
|
|
|
|
|
- <MessageFailureNotice v-if="messageFailure(message)" :failure="messageFailure(message)!" />
|
|
|
|
|
|
<div v-if="hasConversationActions(message)" class="message__actions">
|
|
|
<time v-if="message.role === 'assistant' && !showAssistantIdentity && message.createdAt" class="message__action-time">{{ formatTime(message.createdAt) }}</time>
|
|
|
@@ -377,6 +331,7 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
|
|
|
</div>
|
|
|
</template>
|
|
|
</div>
|
|
|
+ <MessageFailureNotice v-if="messageFailure(message) && (!messageFailure(message)?.replacesContent || !!message.attachments?.length)" :failure="messageFailure(message)!" />
|
|
|
</article>
|
|
|
</template>
|
|
|
</template>
|
|
|
@@ -405,6 +360,12 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
+.message--has-failure { flex-wrap: wrap; }
|
|
|
+.message--has-failure > .failure-notice { flex: 0 0 100%; }
|
|
|
+.message.message--notice, .message.message--failure-notice { justify-content: center; }
|
|
|
+.message--notice .message__avatar, .message--failure-notice .message__avatar { display: none; }
|
|
|
+.message.message--notice .message__body, .message.message--failure-notice .message__body { width: 100%; max-width: 100%; margin: 0; padding: 0; border: 0; background: transparent; }
|
|
|
+
|
|
|
.timeline-frame { position: relative; display: flex; min-width: 0; min-height: 0; flex: 1; flex-direction: column; }
|
|
|
.timeline-header { display: flex; z-index: 6; min-height: 62px; align-items: center; gap: 13px; padding: 10px 18px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--canvas) 92%, transparent); backdrop-filter: blur(16px); }
|
|
|
.timeline-header--transparent { position: absolute; top: 0; right: 0; left: 0; min-height: 0; justify-content: flex-end; padding: 11px 14px; border: 0; background: transparent; backdrop-filter: none; pointer-events: none; }
|
|
|
@@ -430,9 +391,6 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
|
|
|
|
|
|
.message--user .message__meta { display: none; }
|
|
|
.message__delivery { display: flex; min-height: 18px; align-items: center; justify-content: flex-end; gap: 4px; margin: 5px 1px -4px; color: var(--text-muted); font-size: 10px; }.message__delivery--accepted { color: var(--success); }.message__delivery--failed, .message__delivery--unknown-receipt { color: var(--danger); }
|
|
|
-.message--system { justify-content: center; }.message--system .message__avatar, .message--system .message__meta { display: none; }.message--system .message__body { max-width: 82%; padding: 6px 10px; border: 1px solid var(--line); border-radius: 9px; color: var(--text-muted); text-align: center; }
|
|
|
-.delegation-event { min-width: 230px; max-width: 460px; text-align: left; }.delegation-event summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; }.delegation-event summary::-webkit-details-marker { display: none; }.delegation-event summary > span { display: flex; min-width: 0; flex-direction: column; gap: 2px; }.delegation-event strong { color: var(--text-secondary); font-size: 12px; font-weight: 650; }.delegation-event small { color: var(--text-muted); font-size: 10px; }.delegation-event :deep(.markdown) { max-height: 260px; margin-top: 9px; overflow: auto; color: var(--text-secondary); font-size: 11px; }
|
|
|
-.system-event { max-width: 460px; text-align: left; }.system-event summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; color: var(--text-muted); font-size: 11px; }.system-event summary::-webkit-details-marker { display: none; }.system-event :deep(.markdown) { max-height: 220px; margin-top: 8px; overflow: auto; color: var(--text-secondary); font-size: 11px; }
|
|
|
.message--tool-only { margin-block: 4px; }.message--tool-only .message__avatar, .message--tool-only .message__meta, .message--tool-only .message__actions { display: none; }.message--tool-only .message__body { max-width: 100%; }
|
|
|
.message--assistant-internal { margin-bottom: 6px; }.message--assistant-internal .message__reasoning { margin-bottom: 4px; padding-bottom: 4px; }.message--assistant-internal .message__tools { margin-top: 4px; }
|
|
|
.message--assistant-anonymous .message__body { max-width: min(680px, 100%); }
|
|
|
@@ -453,9 +411,6 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom, isFollowingBotto
|
|
|
.message-skeletons { width: min(760px, 100%); margin: 0 auto; padding: 35px 24px; }.message-skeletons div { display: flex; gap: 10px; margin: 0 0 25px; }.message-skeletons div.user { justify-content: flex-end; }.message-skeletons i { width: 27px; height: 27px; border-radius: 9px; background: var(--surface-hover); }.message-skeletons span { width: min(480px, 70%); height: 64px; border-radius: 12px; background: linear-gradient(90deg, var(--surface-soft), var(--surface-hover), var(--surface-soft)); background-size: 200% 100%; animation: shimmer 1.5s linear infinite; }.message-skeletons .user i { display: none; }.message-skeletons .user span { width: 44%; height: 46px; }
|
|
|
@keyframes shimmer { to { background-position: -200% 0; } }
|
|
|
|
|
|
-.message--failure-notice { justify-content: flex-start; }
|
|
|
-.message--failure-notice .message__avatar { display: none; }
|
|
|
-.message--failure-notice .message__body { width: min(460px, 100%); max-width: 100%; padding: 0; border: 0; background: transparent; text-align: left; }
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
.timeline-header { min-height: 54px; padding: 8px 12px; }.timeline-state { font-size: 0; }.timeline-state .status-dot { width: 7px; height: 7px; }
|