Version: 0.9.47.dev.260427
后端: 1. execute 节点继续拆职责——超大 execute.go 下沉为 node/execute 子包,按决策流、动作路由、上下文锚点、工具执行、状态快照、工具展示与参数解析拆分;顶层 execute.go 收敛为桥接导出,降低单文件编排/业务/模型/工具逻辑混写 2. 节点公共能力继续沉到 shared——抽出 LLM 纠错回灌、完整上下文调试日志、thinking 开关、统一上下文压缩、可见 assistant 文本持久化等 node_* 公共件,减少 execute 独占实现并为其他节点复用铺路 3. speak 文本整理能力独立收口——新增 speak_text 辅助文件,补齐正文归一化的独立承载,继续收缩 execute 主文件体积 前端: 4. NewAgent 时间线接入 business_card 业务卡片协议——schedule_agent.ts 新增 task_query / task_record 卡片载荷类型与 business_card kind;AssistantPanel 增加业务卡片事件存储、时间线恢复、块渲染分支与 BusinessCardRenderer 接入,同时保留 interrupt / status / tool / reasoning 多块并存 5. 新增任务查询卡片与任务记录卡片组件,并补充 DesignDemo 设计预览页与路由,前端可先行验证 business_card 的视觉与交互落点 文档: 6. 新增 newagent business card 前后端对接说明,明确 timeline kind、payload 结构、卡片分类、前后端发射/渲染约束
This commit is contained in:
@@ -16,6 +16,45 @@ export interface TimelineConfirmPayload {
|
||||
summary: string
|
||||
}
|
||||
|
||||
export interface TaskQueryCardTaskItem {
|
||||
id: number
|
||||
title: string
|
||||
priority_group?: number
|
||||
priority_label?: string
|
||||
deadline_at?: string
|
||||
is_completed?: boolean
|
||||
}
|
||||
|
||||
export interface TaskQueryCardData {
|
||||
query_summary?: string
|
||||
result_count: number
|
||||
shown_count: number
|
||||
has_more?: boolean
|
||||
tasks: TaskQueryCardTaskItem[]
|
||||
}
|
||||
|
||||
export interface TaskRecordCardData {
|
||||
id?: number
|
||||
title: string
|
||||
priority_group?: number
|
||||
priority_label?: string
|
||||
deadline_at?: string
|
||||
urgency_threshold_at?: string
|
||||
status?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export type BusinessCardType = 'task_query' | 'task_record'
|
||||
export type TaskRecordSource = 'quick_note' | 'create_task'
|
||||
|
||||
export interface TimelineBusinessCardPayload {
|
||||
card_type: BusinessCardType
|
||||
title?: string
|
||||
summary?: string
|
||||
source?: TaskRecordSource
|
||||
data: TaskQueryCardData | TaskRecordCardData
|
||||
}
|
||||
|
||||
export interface TimelineEvent {
|
||||
id: number
|
||||
seq: number
|
||||
@@ -28,6 +67,7 @@ export interface TimelineEvent {
|
||||
| 'schedule_completed'
|
||||
| 'interrupt'
|
||||
| 'status'
|
||||
| 'business_card'
|
||||
role?: 'user' | 'assistant'
|
||||
content?: string
|
||||
payload?: {
|
||||
@@ -37,6 +77,7 @@ export interface TimelineEvent {
|
||||
display_mode?: 'card'
|
||||
tool?: TimelineToolPayload
|
||||
confirm?: TimelineConfirmPayload
|
||||
business_card?: TimelineBusinessCardPayload
|
||||
}
|
||||
tokens_consumed?: number
|
||||
created_at: string
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { TimelineBusinessCardPayload, TaskQueryCardData, TaskRecordCardData } from '@/api/schedule_agent'
|
||||
import TaskQueryResultCard from './TaskQueryResultCard.vue'
|
||||
import TaskRecordCard from './TaskRecordCard.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
payload: TimelineBusinessCardPayload
|
||||
}>()
|
||||
|
||||
const isTaskQuery = computed(() => props.payload.card_type === 'task_query')
|
||||
const isTaskRecord = computed(() => props.payload.card_type === 'task_record')
|
||||
|
||||
const queryData = computed(() => props.payload.data as TaskQueryCardData)
|
||||
const recordData = computed(() => props.payload.data as TaskRecordCardData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="business-card-renderer">
|
||||
<TaskQueryResultCard
|
||||
v-if="isTaskQuery"
|
||||
:data="queryData"
|
||||
:title="payload.title"
|
||||
:summary="payload.summary"
|
||||
/>
|
||||
|
||||
<TaskRecordCard
|
||||
v-else-if="isTaskRecord"
|
||||
:data="recordData"
|
||||
:source="payload.source"
|
||||
:title="payload.title"
|
||||
:summary="payload.summary"
|
||||
/>
|
||||
|
||||
<div v-else class="unknown-card">
|
||||
<p>未知业务卡片类型: {{ payload.card_type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.business-card-renderer {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: card-appear 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes card-appear {
|
||||
0% { opacity: 0; transform: scale(0.95) translateY(10px); }
|
||||
100% { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
.unknown-card {
|
||||
padding: 16px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 12px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
230
frontend/src/components/assistant/cards/TaskQueryResultCard.vue
Normal file
230
frontend/src/components/assistant/cards/TaskQueryResultCard.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<script setup lang="ts">
|
||||
import type { TaskQueryCardData } from '@/api/schedule_agent'
|
||||
|
||||
const props = defineProps<{
|
||||
data: TaskQueryCardData
|
||||
title?: string
|
||||
summary?: string
|
||||
}>()
|
||||
|
||||
// 对齐首页象限体系
|
||||
const quadMeta: any = {
|
||||
1: { title: '重要且紧急', tone: 'danger', color: '#ef4444' },
|
||||
2: { title: '重要不紧急', tone: 'primary', color: '#3b82f6' },
|
||||
3: { title: '简单不重要', tone: 'warning', color: '#f59e0b' },
|
||||
4: { title: '不简单不重要', tone: 'slate', color: '#64748b' }
|
||||
}
|
||||
|
||||
const getBgStyle = (group: number = 2) => {
|
||||
const bgMap: any = {
|
||||
1: 'linear-gradient(180deg, #fff1f2 0%, #fff7f7 100%)',
|
||||
2: 'linear-gradient(180deg, #eef7ff 0%, #f7fbff 100%)',
|
||||
3: 'linear-gradient(180deg, #fff8df 0%, #fffdf1 100%)',
|
||||
4: 'linear-gradient(180deg, #f2f5fb 0%, #f8fafc 100%)'
|
||||
}
|
||||
return bgMap[group] || bgMap[2]
|
||||
}
|
||||
|
||||
const getTextColor = (group: number = 2) => {
|
||||
return quadMeta[group]?.color || '#3b82f6'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="business-card query-results" :style="{ background: getBgStyle(props.data.tasks[0]?.priority_group) }">
|
||||
<header class="card-header">
|
||||
<div class="header-left">
|
||||
<p class="eyebrow">{{ summary || '查询结果' }}</p>
|
||||
<h3>{{ title || '为您找到以下任务' }}</h3>
|
||||
</div>
|
||||
<div class="count-badge" v-if="data.result_count > 0">
|
||||
{{ data.result_count }} 项
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card-content">
|
||||
<div v-if="data.tasks && data.tasks.length > 0" class="task-items">
|
||||
<div v-for="task in data.tasks" :key="task.id" class="task-item">
|
||||
<div class="item-check">
|
||||
<div class="check-circle" :style="{ borderColor: getTextColor(task.priority_group) }"></div>
|
||||
</div>
|
||||
<div class="item-body">
|
||||
<div class="item-title">{{ task.title }}</div>
|
||||
<div class="item-meta">
|
||||
<span
|
||||
class="q-pill"
|
||||
v-if="task.priority_group"
|
||||
:style="{ color: getTextColor(task.priority_group), background: getTextColor(task.priority_group) + '10' }"
|
||||
>
|
||||
Q{{ task.priority_group }} {{ quadMeta[task.priority_group]?.title }}
|
||||
</span>
|
||||
<span v-if="task.deadline_at" class="time-pill">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{{ task.deadline_at }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M10 10l4 4m0-4l-4 4"/></svg>
|
||||
</div>
|
||||
<p>暂无符合条目</p>
|
||||
</div>
|
||||
|
||||
<button v-if="data.has_more" class="btn-more">查看完整列表</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.business-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 28px;
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.business-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24px 24px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: rgba(30, 41, 59, 0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 850;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 100px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
|
||||
}
|
||||
|
||||
.task-items {
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0,0,0,0.04);
|
||||
border-radius: 18px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.check-circle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #122033;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.q-pill {
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.time-pill {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-more {
|
||||
width: calc(100% - 32px);
|
||||
margin: 16px 16px 20px;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-more:hover {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
180
frontend/src/components/assistant/cards/TaskRecordCard.vue
Normal file
180
frontend/src/components/assistant/cards/TaskRecordCard.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import type { TaskRecordCardData, TaskRecordSource } from '@/api/schedule_agent'
|
||||
|
||||
const props = defineProps<{
|
||||
data: TaskRecordCardData
|
||||
source?: TaskRecordSource
|
||||
title?: string
|
||||
summary?: string
|
||||
}>()
|
||||
|
||||
// 对齐首页象限体系
|
||||
const quadMeta: any = {
|
||||
1: { title: '重要且紧急', tone: 'danger', color: '#ef4444' },
|
||||
2: { title: '重要不紧急', tone: 'primary', color: '#3b82f6' },
|
||||
3: { title: '简单不重要', tone: 'warning', color: '#f59e0b' },
|
||||
4: { title: '不简单不重要', tone: 'slate', color: '#64748b' }
|
||||
}
|
||||
|
||||
const getBgStyle = (group: number = 2) => {
|
||||
const bgMap: any = {
|
||||
1: 'linear-gradient(180deg, #fff1f2 0%, #fff7f7 100%)',
|
||||
2: 'linear-gradient(180deg, #eef7ff 0%, #f7fbff 100%)',
|
||||
3: 'linear-gradient(180deg, #fff8df 0%, #fffdf1 100%)',
|
||||
4: 'linear-gradient(180deg, #f2f5fb 0%, #f8fafc 100%)'
|
||||
}
|
||||
return bgMap[group] || bgMap[2]
|
||||
}
|
||||
|
||||
const getTextColor = (group: number = 2) => {
|
||||
return quadMeta[group]?.color || '#3b82f6'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="business-card creation-receipt" :style="{ background: getBgStyle(props.data.priority_group) }">
|
||||
<div class="receipt-inner">
|
||||
<div class="receipt-header">
|
||||
<div class="success-ring" :style="{ background: getTextColor(props.data.priority_group) + '20', color: getTextColor(props.data.priority_group) }">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
</div>
|
||||
<div class="success-msg">
|
||||
<strong>{{ title || (source === 'quick_note' ? '已帮您记下' : '任务已创建') }}</strong>
|
||||
<span v-if="data.priority_group">归类至:{{ quadMeta[data.priority_group].title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-info-card">
|
||||
<div class="task-title">{{ data.title }}</div>
|
||||
<div class="task-footer">
|
||||
<span class="task-id" v-if="data.id">ID: {{ data.id }}</span>
|
||||
<span class="task-time" v-if="data.created_at || data.deadline_at">
|
||||
{{ data.deadline_at ? '截止:' + data.deadline_at : '刚刚创建' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="receipt-actions">
|
||||
<button class="btn-outline">修改详情</button>
|
||||
<button class="btn-fill" :style="{ background: getTextColor(props.data.priority_group) }">打开查看</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.business-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 28px;
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.receipt-inner {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.receipt-header {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.success-ring {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.success-msg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.success-msg strong {
|
||||
font-size: 15px;
|
||||
font-weight: 850;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.success-msg span {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-info-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0,0,0,0.03);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.01);
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.task-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.receipt-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
height: 42px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.btn-fill {
|
||||
height: 42px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-fill:hover {
|
||||
filter: brightness(1.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</style>
|
||||
@@ -32,6 +32,8 @@ import ScheduleResultCard from '@/components/assistant/ScheduleResultCard.vue'
|
||||
import ScheduleFineTuneModal from '@/components/assistant/ScheduleFineTuneModal.vue'
|
||||
import { formatConversationTime, formatMessageTime } from '@/utils/date'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
import BusinessCardRenderer from '@/components/assistant/cards/BusinessCardRenderer.vue'
|
||||
import type { TimelineBusinessCardPayload } from '@/api/schedule_agent'
|
||||
|
||||
interface StreamDeltaPayload {
|
||||
content?: string
|
||||
@@ -147,12 +149,13 @@ interface DisplayMessage {
|
||||
|
||||
interface DisplayAssistantBlock {
|
||||
id: string
|
||||
type: 'tool' | 'status' | 'reasoning' | 'content' | 'content_indicator' | 'schedule_card'
|
||||
type: 'tool' | 'status' | 'reasoning' | 'content' | 'content_indicator' | 'schedule_card' | 'business_card'
|
||||
seq: number
|
||||
text?: string
|
||||
event?: ToolTraceEvent
|
||||
statusEvent?: StatusTraceEvent
|
||||
schedulePreview?: SchedulePreviewData
|
||||
businessCard?: TimelineBusinessCardPayload
|
||||
/** 所属的源消息 ID,用于状态查询 */
|
||||
sourceId?: string
|
||||
/** 所属的源消息引用,用于渲染辅助信息 */
|
||||
@@ -228,12 +231,13 @@ const toolTraceExpandedMap = reactive<Record<string, boolean>>({})
|
||||
const assistantReasoningSeqMap = reactive<Record<string, number>>({})
|
||||
const assistantContentBlocksMap = reactive<Record<string, AssistantContentBlock[]>>({})
|
||||
const assistantReasoningBlocksMap = reactive<Record<string, AssistantContentBlock[]>>({})
|
||||
const assistantTimelineLastKindMap = reactive<Record<string, 'content' | 'tool' | 'status' | 'reasoning' | 'other'>>({})
|
||||
const assistantTimelineLastKindMap = reactive<Record<string, 'content' | 'tool' | 'status' | 'reasoning' | 'business_card' | 'other'>>({})
|
||||
const conversationContextStatsMap = reactive<Record<string, ConversationContextStats | null>>({})
|
||||
const conversationContextStatsLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const conversationContextStatsReadyMap = reactive<Record<string, boolean>>({})
|
||||
const conversationListItemRevealMap = reactive<Record<string, boolean>>({})
|
||||
const scheduleResultMap = reactive<Record<string, SchedulePreviewData>>({})
|
||||
const businessCardEventsMap = reactive<Record<string, TimelineBusinessCardPayload[]>>({})
|
||||
const isFineTuneModalVisible = ref(false)
|
||||
const fineTuneLoading = ref(false)
|
||||
const activeFineTuneData = ref<SchedulePreviewData | null>(null)
|
||||
@@ -484,6 +488,7 @@ function clearToolTraceState(messageId: string) {
|
||||
delete assistantContentBlocksMap[messageId]
|
||||
delete assistantTimelineLastKindMap[messageId]
|
||||
delete scheduleResultMap[messageId]
|
||||
delete businessCardEventsMap[messageId]
|
||||
for (const key of Object.keys(toolTraceExpandedMap)) {
|
||||
if (key.startsWith(`${messageId}:tool:`)) {
|
||||
delete toolTraceExpandedMap[key]
|
||||
@@ -711,6 +716,29 @@ function appendAssistantReasoningChunk(messageId: string, chunk: string) {
|
||||
assistantTimelineLastKindMap[messageId] = 'reasoning'
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加业务卡片事件
|
||||
*/
|
||||
function appendBusinessCardEvent(messageId: string, payload: TimelineBusinessCardPayload, seq?: number) {
|
||||
if (!businessCardEventsMap[messageId]) {
|
||||
businessCardEventsMap[messageId] = []
|
||||
}
|
||||
|
||||
// 如果上一个阶段是推理,则结束并折叠它
|
||||
if (assistantTimelineLastKindMap[messageId] === 'reasoning') {
|
||||
finishCurrentReasoningBlock(messageId)
|
||||
}
|
||||
|
||||
const eventSeq = seq || nextAssistantTimelineSeq()
|
||||
businessCardEventsMap[messageId].push({
|
||||
...payload,
|
||||
// 借用 payload 存储 seq,便于 getDisplayAssistantBlocks 排序
|
||||
_seq: eventSeq
|
||||
} as any)
|
||||
|
||||
assistantTimelineLastKindMap[messageId] = 'business_card'
|
||||
}
|
||||
|
||||
function mapToolEventState(rawStatus?: string): ToolTraceState {
|
||||
const normalized = `${rawStatus || ''}`.trim().toLowerCase()
|
||||
if (normalized === 'start' || normalized === 'calling' || normalized === 'called') {
|
||||
@@ -1284,7 +1312,6 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
const statusEvents = (statusTraceEventsMap[source.id] || []).slice().sort((left, right) => left.seq - right.seq)
|
||||
for (const statusEvent of statusEvents) {
|
||||
blocks.push({
|
||||
@@ -1321,6 +1348,18 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
})
|
||||
}
|
||||
|
||||
const businessCards = businessCardEventsMap[source.id] || []
|
||||
for (const card of businessCards) {
|
||||
blocks.push({
|
||||
id: `${source.id}:card:${(card as any)._seq}`,
|
||||
type: 'business_card',
|
||||
seq: (card as any)._seq,
|
||||
businessCard: card,
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
const contentBlocks = assistantContentBlocksMap[source.id] || []
|
||||
if (contentBlocks.length > 0) {
|
||||
hasContentBlock = true
|
||||
@@ -1803,18 +1842,16 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
|
||||
// 在刷新恢复场景下,我们只需设置状态即可。
|
||||
}
|
||||
break
|
||||
case 'business_card':
|
||||
if (event.payload?.business_card) {
|
||||
appendBusinessCardEvent(mid, event.payload.business_card)
|
||||
}
|
||||
break
|
||||
|
||||
case 'schedule_completed':
|
||||
// 1. 标记该消息需要排程卡片。
|
||||
// 2. 改造点:不在此处立即进行 getSchedulePreview 的异步拉取,
|
||||
// 避免后端还未完成落库、或者并发过高导致的 'schedule plan preview not found' 404 捕获。
|
||||
// 3. 这里先存入占位标志,真正的拉取推迟到用户“点击卡片”时。
|
||||
scheduleResultMap[mid] = {
|
||||
summary: '智能编排方案已就绪',
|
||||
conversation_id: conversationId,
|
||||
hybrid_entries: [],
|
||||
is_placeholder: true, // 内部临时标记
|
||||
} as any
|
||||
case 'business_card':
|
||||
if (event.payload?.business_card) {
|
||||
appendBusinessCardEvent(mid, event.payload.business_card, event.seq)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2255,20 +2292,11 @@ function handleStreamExtraEvent(extra: StreamExtraPayload | undefined, assistant
|
||||
`${extra.stage || ''}`,
|
||||
)
|
||||
}
|
||||
scheduleScrollMessagesToBottom(true)
|
||||
}
|
||||
|
||||
if (extra.kind === 'schedule_completed') {
|
||||
// 1. 每当“排程卡片”这种重量级里程碑出现时,刷新统计信息,让用户感知到上下文变动。
|
||||
void loadConversationContextStats(selectedConversationId.value, true)
|
||||
|
||||
// 2. 收到编排完成事件,仅在前端打上占位标记,展示展示卡片。
|
||||
// 不再并发执行异步 fetch,防止后端落库延迟导致的 NotFound。
|
||||
scheduleResultMap[assistantMessage.id] = {
|
||||
summary: '智能编排方案已就绪',
|
||||
conversation_id: selectedConversationId.value,
|
||||
hybrid_entries: [],
|
||||
is_placeholder: true,
|
||||
} as any
|
||||
if (extra.kind === 'business_card' && extra.business_card) {
|
||||
appendBusinessCardEvent(assistantMessage.id, extra.business_card)
|
||||
scheduleScrollMessagesToBottom(true)
|
||||
}
|
||||
}
|
||||
@@ -2798,16 +2826,7 @@ onBeforeUnmount(() => {
|
||||
<div v-else class="chat-message__assistant-flow">
|
||||
<TransitionGroup name="inner-fade">
|
||||
<div v-for="block in getDisplayAssistantBlocks(dm)" :key="block.id">
|
||||
<div v-if="block.type === 'tool'" class="chat-message__tool-list">
|
||||
<article
|
||||
class="chat-message__tool-item"
|
||||
:class="{
|
||||
'chat-message__tool-item--called': block.event?.state === 'called',
|
||||
'chat-message__tool-item--completed': block.event?.state === 'completed',
|
||||
'chat-message__tool-item--create': block.event?.state === 'create',
|
||||
'chat-message__tool-item--blocked': block.event?.state === 'blocked',
|
||||
}"
|
||||
>
|
||||
<article v-if="block.type === 'tool'" class="chat-message__tool">
|
||||
<button
|
||||
type="button"
|
||||
class="chat-message__tool-head"
|
||||
@@ -2831,105 +2850,108 @@ onBeforeUnmount(() => {
|
||||
{{ block.event.detail }}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-else-if="block.type === 'status'" class="chat-message__status-line">
|
||||
<span class="chat-message__status-icon" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2v4" />
|
||||
<path d="M12 18v4" />
|
||||
<path d="M4.93 4.93l2.83 2.83" />
|
||||
<path d="M16.24 16.24l2.83 2.83" />
|
||||
<path d="M2 12h4" />
|
||||
<path d="M18 12h4" />
|
||||
<path d="M4.93 19.07l2.83-2.83" />
|
||||
<path d="M16.24 7.76l2.83-2.83" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="chat-message__status-text">{{ block.statusEvent?.summary }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="block.type === 'reasoning'" class="chat-message__reasoning">
|
||||
<div class="chat-message__reasoning-head">
|
||||
<div class="chat-message__reasoning-title">
|
||||
<span class="chat-message__reasoning-icon">
|
||||
<svg
|
||||
class="chat-message__reasoning-icon-svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M8.00195 6.64454C8.75029 6.64454 9.35735 7.25169 9.35742 8.00001C9.35742 8.74838 8.75033 9.35548 8.00195 9.35548C7.2537 9.35533 6.64746 8.74829 6.64746 8.00001C6.64753 7.25178 7.25374 6.64468 8.00195 6.64454Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.97168 1.29981C11.5854 0.718916 13.271 0.642197 14.3145 1.68555C15.3578 2.72902 15.2811 4.41466 14.7002 6.02833C14.4708 6.66561 14.1505 7.32937 13.75 8.00001C14.1505 8.67062 14.4708 9.33444 14.7002 9.97169C15.2811 11.5854 15.3579 13.271 14.3145 14.3145C13.271 15.3579 11.5854 15.2811 9.97168 14.7002C9.33443 14.4708 8.67062 14.1505 8 13.75C7.32936 14.1505 6.66561 14.4708 6.02832 14.7002C4.41464 15.2811 2.72902 15.3578 1.68555 14.3145C0.642186 13.271 0.718901 11.5854 1.29981 9.97169C1.52918 9.33454 1.84868 8.67049 2.24902 8.00001C1.84869 7.32953 1.52918 6.66544 1.29981 6.02833C0.718882 4.41459 0.6421 2.729 1.68555 1.68555C2.729 0.642112 4.41459 0.718887 6.02832 1.29981C6.66544 1.52918 7.32953 1.8487 8 2.24903C8.67048 1.84869 9.33454 1.52919 9.97168 1.29981ZM12.9404 9.2129C12.4391 9.893 11.8616 10.5681 11.2148 11.2149C10.5681 11.8616 9.89299 12.4391 9.21289 12.9404C9.62535 13.1579 10.0271 13.338 10.4121 13.4766C11.9146 14.0174 12.9173 13.8738 13.3955 13.3955C13.8737 12.9173 14.0174 11.9146 13.4766 10.4121C13.338 10.0271 13.1579 9.62535 12.9404 9.2129ZM3.05859 9.2129C2.84124 9.62523 2.662 10.0272 2.52344 10.4121C1.98255 11.9146 2.1263 12.9172 2.60449 13.3955C3.08281 13.8737 4.08548 14.0174 5.58789 13.4766C5.97267 13.338 6.37392 13.1577 6.78613 12.9404C6.10627 12.4393 5.43171 11.8614 4.78516 11.2149C4.13826 10.5679 3.55995 9.89313 3.05859 9.2129ZM7.99902 3.792C7.23182 4.31419 6.45309 4.95512 5.7041 5.70411C4.95512 6.45309 4.31418 7.23184 3.79199 7.99903C4.31434 8.76666 4.95474 9.54653 5.7041 10.2959C6.45312 11.0449 7.23274 11.6848 8 12.207C8.76728 11.6848 9.54686 11.0449 10.2959 10.2959C11.0449 9.54686 11.6848 8.76729 12.207 8.00001C11.6848 7.23275 11.0449 6.45312 10.2959 5.70411C9.54653 4.95475 8.76665 4.31434 7.99902 3.792ZM5.58789 2.52344C4.08536 1.98255 3.08275 2.12625 2.60449 2.6045C2.12624 3.08275 1.98255 4.08536 2.52344 5.5879C2.66192 5.97253 2.84143 6.37409 3.05859 6.78614C3.55986 6.10611 4.13843 5.43189 4.78516 4.78516C5.4319 4.13843 6.10609 3.55987 6.78613 3.0586C6.37408 2.84144 5.97252 2.66192 5.58789 2.52344ZM13.3955 2.6045C12.9172 2.12631 11.9146 1.98257 10.4121 2.52344C10.0272 2.66201 9.62522 2.84125 9.21289 3.0586C9.89313 3.55996 10.5679 4.13827 11.2148 4.78516C11.8614 5.43172 12.4392 6.10627 12.9404 6.78614C13.1577 6.37393 13.338 5.97267 13.4766 5.5879C14.0174 4.08549 13.8736 3.08281 13.3955 2.6045Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="chat-message__reasoning-status">{{ getReasoningStatusLabel(block) }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-message__reasoning-toggle"
|
||||
:aria-label="isReasoningCollapsed(block.id) ? '展开深度思考' : '折叠深度思考'"
|
||||
@click="toggleReasoningCollapse(block.id)"
|
||||
>
|
||||
<span class="chat-message__reasoning-chevron">
|
||||
<svg
|
||||
class="chat-message__reasoning-chevron-icon"
|
||||
:class="{ 'chat-message__reasoning-chevron-icon--expanded': !isReasoningCollapsed(block.id) }"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5.5 2.15137L5.92383 2.57617L8.65137 5.30273C8.90706 5.55843 9.13382 5.78438 9.29785 5.98828C9.46883 6.20088 9.61756 6.44405 9.66602 6.75C9.69222 6.91565 9.69222 7.08435 9.66602 7.25C9.61756 7.55595 9.46883 7.79912 9.29785 8.01172C9.13382 8.21561 8.90706 8.44157 8.65137 8.69727L5.92383 11.4238L5.5 11.8486L4.65137 11L5.07617 10.5762L7.80273 7.84863C8.07732 7.57405 8.24849 7.40124 8.3623 7.25977C8.46904 7.12709 8.47813 7.07728 8.48047 7.0625C8.48703 7.02105 8.48703 6.97895 8.48047 6.9375C8.47813 6.92272 8.46904 6.87291 8.3623 6.74023C8.24848 6.59876 8.07732 6.42595 7.80273 6.15137L5.07617 3.42383L4.65137 3L5.5 2.15137Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<div v-else-if="block.type === 'status'" class="chat-message__status-line">
|
||||
<span class="chat-message__status-icon" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2v4" />
|
||||
<path d="M12 18v4" />
|
||||
<path d="M4.93 4.93l2.83 2.83" />
|
||||
<path d="M16.24 16.24l2.83 2.83" />
|
||||
<path d="M2 12h4" />
|
||||
<path d="M18 12h4" />
|
||||
<path d="M4.93 19.07l2.83-2.83" />
|
||||
<path d="M16.24 7.76l2.83-2.83" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="chat-message__status-text">{{ block.statusEvent?.summary }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isReasoningCollapsed(block.id) === false" class="chat-message__reasoning-body">
|
||||
<div
|
||||
v-if="block.text"
|
||||
class="chat-message__markdown chat-message__markdown--reasoning"
|
||||
v-html="renderMessageMarkdown(block.text || '')"
|
||||
/>
|
||||
<div v-else class="chat-message__streaming chat-message__streaming--reasoning">
|
||||
<div class="thinking-indicator">
|
||||
<span class="thinking-indicator__text">正在思考</span>
|
||||
<div v-else-if="block.type === 'reasoning'" class="chat-message__reasoning">
|
||||
<div class="chat-message__reasoning-head">
|
||||
<div class="chat-message__reasoning-title">
|
||||
<span class="chat-message__reasoning-icon">
|
||||
<svg
|
||||
class="chat-message__reasoning-icon-svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M8.00195 6.64454C8.75029 6.64454 9.35735 7.25169 9.35742 8.00001C9.35742 8.74838 8.75033 9.35548 8.00195 9.35548C7.2537 9.35533 6.64746 8.74829 6.64746 8.00001C6.64753 7.25178 7.25374 6.64468 8.00195 6.64454Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.97168 1.29981C11.5854 0.718916 13.271 0.642197 14.3145 1.68555C15.3578 2.72902 15.2811 4.41466 14.7002 6.02833C14.4708 6.66561 14.1505 7.32937 13.75 8.00001C14.1505 8.67062 14.4708 9.33444 14.7002 9.97169C15.2811 11.5854 15.3579 13.271 14.3145 14.3145C13.271 15.3579 11.5854 15.2811 9.97168 14.7002C9.33443 14.4708 8.67062 14.1505 8 13.75C7.32936 14.1505 6.66561 14.4708 6.02832 14.7002C4.41464 15.2811 2.72902 15.3578 1.68555 14.3145C0.642186 13.271 0.718901 11.5854 1.29981 9.97169C1.52918 9.33454 1.84868 8.67049 2.24902 8.00001C1.84869 7.32953 1.52918 6.66544 1.29981 6.02833C0.718882 4.41459 0.6421 2.729 1.68555 1.68555C2.729 0.642112 4.41459 0.718887 6.02832 1.29981C6.66544 1.52918 7.32953 1.8487 8 2.24903C8.67048 1.84869 9.33454 1.52919 9.97168 1.29981ZM12.9404 9.2129C12.4391 9.893 11.8616 10.5681 11.2148 11.2149C10.5681 11.8616 9.89299 12.4391 9.21289 12.9404C9.62535 13.1579 10.0271 13.338 10.4121 13.4766C11.9146 14.0174 12.9173 13.8738 13.3955 13.3955C13.8737 12.9173 14.0174 11.9146 13.4766 10.4121C13.338 10.0271 13.1579 9.62535 12.9404 9.2129ZM3.05859 9.2129C2.84124 9.62523 2.662 10.0272 2.52344 10.4121C1.98255 11.9146 2.1263 12.9172 2.60449 13.3955C3.08281 13.8737 4.08548 14.0174 5.58789 13.4766C5.97267 13.338 6.37392 13.1577 6.78613 12.9404C6.10627 12.4393 5.43171 11.8614 4.78516 11.2149C4.13826 10.5679 3.55995 9.89313 3.05859 9.2129ZM7.99902 3.792C7.23182 4.31419 6.45309 4.95512 5.7041 5.70411C4.95512 6.45309 4.31418 7.23184 3.79199 7.99903C4.31434 8.76666 4.95474 9.54653 5.7041 10.2959C6.45312 11.0449 7.23274 11.6848 8 12.207C8.76728 11.6848 9.54686 11.0449 10.2959 10.2959C11.0449 9.54686 11.6848 8.76729 12.207 8.00001C11.6848 7.23275 11.0449 6.45312 10.2959 5.70411C9.54653 4.95475 8.76665 4.31434 7.99902 3.792ZM5.58789 2.52344C4.08536 1.98255 3.08275 2.12625 2.60449 2.6045C2.12624 3.08275 1.98255 4.08536 2.52344 5.5879C2.66192 5.97253 2.84143 6.37409 3.05859 6.78614C3.55986 6.10611 4.13843 5.43189 4.78516 4.78516C5.4319 4.13843 6.10609 3.55987 6.78613 3.0586C6.37408 2.84144 5.97252 2.66192 5.58789 2.52344ZM13.3955 2.6045C12.9172 2.12631 11.9146 1.98257 10.4121 2.52344C10.0272 2.66201 9.62522 2.84125 9.21289 3.0586C9.89313 3.55996 10.5679 4.13827 11.2148 4.78516C11.8614 5.43172 12.4392 6.10627 12.9404 6.78614C13.1577 6.37393 13.338 5.97267 13.4766 5.5879C14.0174 4.08549 13.8736 3.08281 13.3955 2.6045Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="chat-message__reasoning-status">{{ getReasoningStatusLabel(block) }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-message__reasoning-toggle"
|
||||
:aria-label="isReasoningCollapsed(block.id) ? '展开深度思考' : '折叠深度思考'"
|
||||
@click="toggleReasoningCollapse(block.id)"
|
||||
>
|
||||
<span class="chat-message__reasoning-chevron">
|
||||
<svg
|
||||
class="chat-message__reasoning-chevron-icon"
|
||||
:class="{ 'chat-message__reasoning-chevron-icon--expanded': !isReasoningCollapsed(block.id) }"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5.5 2.15137L5.92383 2.57617L8.65137 5.30273C8.90706 5.55843 9.13382 5.78438 9.29785 5.98828C9.46883 6.20088 9.61756 6.44405 9.66602 6.75C9.69222 6.91565 9.69222 7.08435 9.66602 7.25C9.61756 7.55595 9.46883 7.79912 9.29785 8.01172C9.13382 8.21561 8.90706 8.44157 8.65137 8.69727L5.92383 11.4238L5.5 11.8486L4.65137 11L5.07617 10.5762L7.80273 7.84863C8.07732 7.57405 8.24849 7.40124 8.3623 7.25977C8.46904 7.12709 8.47813 7.07728 8.48047 7.0625C8.48703 7.02105 8.48703 6.97895 8.48047 6.9375C8.47813 6.92272 8.46904 6.87291 8.3623 6.74023C8.24848 6.59876 8.07732 6.42595 7.80273 6.15137L5.07617 3.42383L4.65137 3L5.5 2.15137Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isReasoningCollapsed(block.id) === false" class="chat-message__reasoning-body">
|
||||
<div
|
||||
v-if="block.text"
|
||||
class="chat-message__markdown chat-message__markdown--reasoning"
|
||||
v-html="renderMessageMarkdown(block.text || '')"
|
||||
/>
|
||||
<div v-else class="chat-message__streaming chat-message__streaming--reasoning">
|
||||
<div class="thinking-indicator">
|
||||
<span class="thinking-indicator__text">正在思考</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="block.type === 'content'" class="chat-message__assistant-content">
|
||||
<div class="chat-message__markdown chat-message__markdown--assistant" v-html="renderMessageMarkdown(block.text || '')" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="block.type === 'schedule_card' && block.schedulePreview">
|
||||
<ScheduleResultCard
|
||||
:summary="block.schedulePreview.summary"
|
||||
@click="openFineTuneModal(block.schedulePreview)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div v-else-if="block.type === 'content_indicator'" class="assistant-timeline__answering-indicator">
|
||||
<div class="thinking-indicator">
|
||||
<span class="thinking-indicator__text">正在思考</span>
|
||||
<div v-else-if="block.type === 'business_card'" class="chat-message__business-card">
|
||||
<BusinessCardRenderer :payload="block.businessCard" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="block.type === 'content'" class="chat-message__assistant-content">
|
||||
<div class="chat-message__markdown chat-message__markdown--assistant" v-html="renderMessageMarkdown(block.text || '')" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="block.type === 'schedule_card' && block.schedulePreview">
|
||||
<ScheduleResultCard
|
||||
:summary="block.schedulePreview.summary"
|
||||
@click="openFineTuneModal(block.schedulePreview)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div v-else-if="block.type === 'content_indicator'" class="assistant-timeline__answering-indicator">
|
||||
<div class="thinking-indicator">
|
||||
<span class="thinking-indicator__text">正在思考</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import DashboardView from '@/views/DashboardView.vue'
|
||||
import ScheduleView from '@/views/ScheduleView.vue'
|
||||
import ToolTracePrototypeView from '@/views/ToolTracePrototypeView.vue'
|
||||
import TaskInteractiveDemo from '@/views/TaskInteractiveDemo.vue'
|
||||
import DesignDemo from '@/views/DesignDemo.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -57,6 +58,11 @@ const router = createRouter({
|
||||
name: 'tool-trace-prototype',
|
||||
component: ToolTracePrototypeView,
|
||||
},
|
||||
{
|
||||
path: '/design-demo',
|
||||
name: 'design-demo',
|
||||
component: DesignDemo,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
253
frontend/src/views/DesignDemo.vue
Normal file
253
frontend/src/views/DesignDemo.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// --- 数据结构定义 ---
|
||||
interface Task {
|
||||
id: string
|
||||
title: string
|
||||
priority_group: 1 | 2 | 3 | 4
|
||||
deadline_at?: string
|
||||
is_completed: boolean
|
||||
}
|
||||
|
||||
// --- 四象限元数据 (深度对齐首页提示词 & 视觉) ---
|
||||
const quadMeta: any = {
|
||||
1: { title: '重要且紧急', caption: '优先处理', tone: 'danger', bg: 'linear-gradient(180deg, #fff1f2 0%, #fff7f7 100%)', text: '#ef4444' },
|
||||
2: { title: '重要不紧急', caption: '持续推进', tone: 'primary', bg: 'linear-gradient(180deg, #eef7ff 0%, #f7fbff 100%)', text: '#3b82f6' },
|
||||
3: { title: '简单不重要', caption: '顺手完成', tone: 'warning', bg: 'linear-gradient(180deg, #fff8df 0%, #fffdf1 100%)', text: '#f59e0b' },
|
||||
4: { title: '不简单不重要', caption: '谨慎投入', tone: 'slate', bg: 'linear-gradient(180deg, #f2f5fb 0%, #f8fafc 100%)', text: '#64748b' }
|
||||
}
|
||||
|
||||
// --- 卡片模拟数据 ---
|
||||
const cardData = {
|
||||
query: {
|
||||
query: '我第一象限里还有哪些事情?',
|
||||
group: 1 as const,
|
||||
tasks: [
|
||||
{ id: '1', title: '修复生产环境登录异常', priority_group: 1, deadline_at: '2024-05-20 09:00', is_completed: false },
|
||||
{ id: '2', title: '提交年度安全审计报告', priority_group: 1, deadline_at: '今天 18:00', is_completed: false },
|
||||
{ id: '3', title: '确认猎选系统的集成计划', priority_group: 1, deadline_at: '明天', is_completed: false }
|
||||
] as Task[]
|
||||
},
|
||||
receipt: {
|
||||
title: '联系供应商确认物料进度',
|
||||
group: 2 as const,
|
||||
id: 'TASK-520',
|
||||
created_at: '刚才'
|
||||
}
|
||||
}
|
||||
|
||||
// --- 交互控制 ---
|
||||
const activeView = ref<'query' | 'receipt'>('query')
|
||||
const currentTone = ref<'danger' | 'primary' | 'warning' | 'slate'>('danger')
|
||||
|
||||
const switchTone = (tone: any) => {
|
||||
currentTone.value = tone
|
||||
// 模拟不同象限的查询结果
|
||||
const toneToGroup: any = { danger: 1, primary: 2, warning: 3, slate: 4 }
|
||||
cardData.query.group = toneToGroup[tone]
|
||||
cardData.receipt.group = toneToGroup[tone]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-demo-page">
|
||||
<div class="page-background">
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="chip">UI Refined V3.0</div>
|
||||
<h1>业务卡片收敛方案</h1>
|
||||
<p>首页风格同步 · 软渐变不晃眼 · 语义对齐</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-wrapper">
|
||||
<!-- 预览控制台 -->
|
||||
<aside class="demo-sidebar">
|
||||
<div class="sidebar-block">
|
||||
<h3>切换卡片类型</h3>
|
||||
<div class="view-btns">
|
||||
<button @click="activeView = 'query'" :class="{ active: activeView === 'query' }">查询记录</button>
|
||||
<button @click="activeView = 'receipt'" :class="{ active: activeView === 'receipt' }">创建回执</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-block">
|
||||
<h3>模拟目标象限</h3>
|
||||
<div class="tone-btns">
|
||||
<button v-for="(v, k) in quadMeta" :key="k" @click="switchTone(v.tone)" :class="[v.tone, { active: currentTone === v.tone }]">
|
||||
{{ v.tone === 'danger' ? 'Q1' : v.tone === 'primary' ? 'Q2' : v.tone === 'warning' ? 'Q3' : 'Q4' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 画布区域 -->
|
||||
<main class="demo-canvas">
|
||||
<!-- 场景 A:任务查询结果 -->
|
||||
<div v-if="activeView === 'query'" class="card-stage" :key="'query-' + currentTone">
|
||||
<div class="card-label">预览:跨象限/单象限查询结果列表</div>
|
||||
<div class="chat-inline-mockup">
|
||||
<div class="business-card-final query-results" :style="{ background: quadMeta[cardData.query.group].bg }">
|
||||
<header class="card-header-final">
|
||||
<div class="header-left">
|
||||
<p class="eyebrow">{{ quadMeta[cardData.query.group].caption }}</p>
|
||||
<h3>{{ cardData.query.query }}</h3>
|
||||
</div>
|
||||
<div class="count-badge">找到 {{ cardData.query.tasks.length }} 项</div>
|
||||
</header>
|
||||
|
||||
<div class="card-content-final">
|
||||
<div class="task-items-final">
|
||||
<div v-for="task in cardData.query.tasks" :key="task.id" class="task-item-final">
|
||||
<div class="item-check">
|
||||
<div class="check-circle" :style="{ borderColor: quadMeta[cardData.query.group].text }"></div>
|
||||
</div>
|
||||
<div class="item-body">
|
||||
<div class="item-title">{{ task.title }}</div>
|
||||
<div class="item-meta">
|
||||
<span class="q-pill" :style="{ color: quadMeta[cardData.query.group].text, background: quadMeta[cardData.query.group].text + '10' }">
|
||||
Q{{ task.priority_group }} {{ quadMeta[task.priority_group].title }}
|
||||
</span>
|
||||
<span v-if="task.deadline_at" class="time-pill">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{{ task.deadline_at }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-more-final">查看完整任务列表</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 场景 B:任务创建回执 -->
|
||||
<div v-else class="card-stage" :key="'receipt-' + currentTone">
|
||||
<div class="card-label">预览:任务创建成功的轻量回执</div>
|
||||
<div class="chat-inline-mockup">
|
||||
<div class="business-card-final creation-receipt" :style="{ background: quadMeta[cardData.receipt.group].bg }">
|
||||
<div class="receipt-inner">
|
||||
<div class="receipt-header-final">
|
||||
<div class="success-ring-v3" :style="{ background: quadMeta[cardData.receipt.group].text + '20', color: quadMeta[cardData.receipt.group].text }">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
</div>
|
||||
<div class="success-msg">
|
||||
<strong>任务已由助手成功创建</strong>
|
||||
<span>归类至:{{ quadMeta[cardData.receipt.group].title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="receipt-task-card">
|
||||
<div class="task-card-title">{{ cardData.receipt.title }}</div>
|
||||
<div class="task-card-footer">
|
||||
<span class="task-id-final">ID: {{ cardData.receipt.id }}</span>
|
||||
<span class="task-time-final">创建于今日 {{ cardData.receipt.created_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="receipt-actions-final">
|
||||
<button class="btn-action-outline">调整象限</button>
|
||||
<button class="btn-action-fill" :style="{ background: quadMeta[cardData.receipt.group].text }">打开详情</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.design-demo-page {
|
||||
padding: 80px 24px;
|
||||
background: #fdfdfe;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.page-background { position: fixed; inset: 0; z-index: -1; }
|
||||
.shape { position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.1; }
|
||||
.shape-1 { width: 500px; height: 500px; background: #3b82f6; top: -10%; left: -10%; }
|
||||
.shape-2 { width: 400px; height: 400px; background: #f43f5e; bottom: -5%; right: -5%; }
|
||||
|
||||
.page-header { text-align: center; margin-bottom: 60px; }
|
||||
.chip { display: inline-block; padding: 4px 12px; background: #f1f5f9; color: #475569; border-radius: 100px; font-size: 11px; font-weight: 800; margin-bottom: 12px; }
|
||||
.page-header h1 { font-size: 32px; font-weight: 900; letter-spacing: -0.04em; color: #0f172a; margin-bottom: 8px; }
|
||||
.page-header p { font-size: 16px; color: #64748b; font-weight: 500; }
|
||||
|
||||
.demo-wrapper { display: flex; gap: 48px; max-width: 1000px; margin: 0 auto; align-items: flex-start; }
|
||||
|
||||
.demo-sidebar { width: 200px; display: flex; flex-direction: column; gap: 32px; position: sticky; top: 80px; }
|
||||
.sidebar-block h3 { font-size: 13px; font-weight: 800; color: #94a3b8; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.view-btns, .tone-btns { display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
.view-btns button, .tone-btns button { padding: 10px 14px; border: 1px solid #f1f5f9; background: white; border-radius: 12px; font-size: 13px; font-weight: 700; color: #475569; cursor: pointer; transition: all 0.2s; text-align: left; }
|
||||
.view-btns button.active { background: #0f172a; color: white; border-color: #0f172a; }
|
||||
|
||||
.tone-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.tone-btns button { text-align: center; }
|
||||
.tone-btns button.danger.active { background: #fee2e2; color: #ef4444; border-color: #ef4444; }
|
||||
.tone-btns button.primary.active { background: #dbeafe; color: #3b82f6; border-color: #3b82f6; }
|
||||
.tone-btns button.warning.active { background: #fef3c7; color: #d97706; border-color: #d97706; }
|
||||
.tone-btns button.slate.active { background: #f1f5f9; color: #475569; border-color: #475569; }
|
||||
|
||||
.demo-canvas { flex: 1; min-width: 0; }
|
||||
.card-stage { animation: stage-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
|
||||
@keyframes stage-in { 0% { opacity: 0; transform: translateY(20px); } 100% { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.card-label { font-size: 12px; color: #94a3b8; margin-bottom: 12px; font-weight: 600; padding-left: 8px; }
|
||||
.chat-inline-mockup { padding: 40px; background: rgba(255, 255, 255, 0.4); border-radius: 40px; border: 1px solid rgba(0,0,0,0.02); backdrop-filter: blur(20px); display: flex; justify-content: center; }
|
||||
|
||||
/* --- Final Business Card Refinement --- */
|
||||
.business-card-final { width: 100%; max-width: 380px; border-radius: 28px; border: 1px solid rgba(17, 24, 39, 0.08); box-shadow: 0 4px 20px rgba(0,0,0,0.02); overflow: hidden; transition: all 0.3s; }
|
||||
.business-card-final:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06); }
|
||||
|
||||
/* Header Sync with Homepage */
|
||||
.card-header-final { padding: 24px 24px 16px; display: flex; justify-content: space-between; align-items: flex-start; }
|
||||
.eyebrow { font-size: 11px; font-weight: 800; color: rgba(30, 41, 59, 0.5); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 6px; }
|
||||
.card-header-final h3 { font-size: 24px; font-weight: 850; color: #1e293b; margin: 0; line-height: 1.1; letter-spacing: -0.02em; }
|
||||
.count-badge { padding: 4px 12px; background: rgba(255, 255, 255, 0.8); border-radius: 100px; font-size: 11px; font-weight: 700; color: #475569; box-shadow: 0 2px 8px rgba(0,0,0,0.02); }
|
||||
|
||||
/* Content List Sync */
|
||||
.task-items-final { padding: 0 16px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.task-item-final { background: rgba(255, 255, 255, 0.95); border: 1px solid rgba(0,0,0,0.04); border-radius: 18px; padding: 14px 16px; display: flex; gap: 14px; align-items: center; }
|
||||
.check-circle { width: 22px; height: 22px; border-radius: 50%; border: 2px solid #e2e8f0; }
|
||||
|
||||
.item-title { font-size: 15px; font-weight: 700; color: #122033; margin-bottom: 4px; }
|
||||
.item-meta { display: flex; gap: 10px; align-items: center; }
|
||||
.q-pill { font-size: 10px; font-weight: 800; padding: 1px 8px; border-radius: 4px; }
|
||||
.time-pill { font-size: 10px; color: #94a3b8; display: flex; align-items: center; gap: 4px; font-weight: 500; }
|
||||
|
||||
.btn-more-final { width: calc(100% - 32px); margin: 16px 16px 20px; padding: 12px; border: none; background: rgba(255, 255, 255, 0.6); border-radius: 14px; font-size: 13px; font-weight: 800; color: #475569; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-more-final:hover { background: white; }
|
||||
|
||||
/* Receipt Card Refinement */
|
||||
.receipt-inner { padding: 24px; display: flex; flex-direction: column; gap: 20px; }
|
||||
.receipt-header-final { display: flex; gap: 14px; align-items: center; }
|
||||
.success-ring-v3 { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.success-msg { display: flex; flex-direction: column; }
|
||||
.success-msg strong { font-size: 15px; font-weight: 850; color: #0f172a; }
|
||||
.success-msg span { font-size: 12px; color: #64748b; font-weight: 500; }
|
||||
|
||||
.receipt-task-card { background: rgba(255, 255, 255, 0.95); border: 1px solid rgba(0,0,0,0.03); border-radius: 20px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.01); }
|
||||
.task-card-title { font-size: 17px; font-weight: 800; color: #1e293b; margin-bottom: 12px; line-height: 1.4; }
|
||||
.task-card-footer { display: flex; justify-content: space-between; font-size: 11px; font-weight: 600; color: #94a3b8; border-top: 1px solid #f1f5f9; padding-top: 10px; }
|
||||
|
||||
.receipt-actions-final { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.btn-action-outline { height: 42px; border: 1px solid #e2e8f0; background: white; border-radius: 12px; font-size: 13px; font-weight: 750; color: #475569; cursor: pointer; }
|
||||
.btn-action-fill { height: 42px; border: none; border-radius: 12px; color: white; font-size: 13px; font-weight: 800; cursor: pointer; box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 800px) {
|
||||
.demo-wrapper { flex-direction: column; }
|
||||
.demo-sidebar { width: 100%; position: static; gap: 20px; }
|
||||
.tone-btns { grid-template-columns: repeat(4, 1fr); }
|
||||
.chat-inline-mockup { padding: 20px; border-radius: 24px; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user