Version: 0.9.50.dev.260428
后端: 1. 工具执行结果协议升级为结构化 ToolExecutionResult——execute/tool_runtime、ToolRegistry、stream extra 与 timeline 持久化统一改为透传 observation_text / summary / argument_view / result_view,不再只回写纯文本结果;context_tools、upsert_task_class 与旧 schedule/web 工具通过兼容包装接入新协议 2. 日程写工具注册继续收口——place / move / swap / batch_move / unplace / queue_apply_head_move 从 registry 内联实现下沉为独立 handler,降低注册表内参数解析与业务逻辑混写 3. 工具结果展示基础能力补齐——新增 execution_result / schedule_operation_handlers 公共件,为日程操作结果、参数本地化展示、blocked/failed/done 状态统一建模 前端: 4. AssistantPanel 接入结构化工具卡片渲染——新增 ToolCardRenderer,tool_call / tool_result 支持 argument_view / result_view 展示;schedule_completed 恢复为时间线内的占位卡片块,避免排程卡片脱离原消息顺序 5. 时间线类型与渲染收敛——schedule_agent.ts 补齐 ToolView 协议,AssistantPanel 改为按块渲染 tool / schedule_card / business_card,并移除旧 demo/prototype 路由与页面,收束正式面板代码路径 仓库: 6. AGENTS.md 新增协作约束——禁止擅自回滚、覆盖或删除用户/其他代理产生的工作区改动
This commit is contained in:
@@ -22,7 +22,8 @@ import {
|
||||
getConversationTimeline,
|
||||
type TimelineEvent,
|
||||
type TimelineToolPayload,
|
||||
type TimelineConfirmPayload
|
||||
type TimelineConfirmPayload,
|
||||
type ToolView
|
||||
} from '@/api/schedule_agent'
|
||||
import { refreshToken } from '@/api/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -41,6 +42,7 @@ import ScheduleFineTuneModal from '@/components/assistant/ScheduleFineTuneModal.
|
||||
import { formatConversationTime, formatMessageTime } from '@/utils/date'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
import BusinessCardRenderer from '@/components/assistant/cards/BusinessCardRenderer.vue'
|
||||
import ToolCardRenderer from '@/components/dashboard/ToolCardRenderer.vue'
|
||||
import type {
|
||||
TimelineBusinessCardPayload,
|
||||
TaskQueryCardData,
|
||||
@@ -77,6 +79,8 @@ interface StreamToolExtraPayload {
|
||||
status?: string
|
||||
summary?: string
|
||||
arguments_preview?: string
|
||||
argument_view?: ToolView
|
||||
result_view?: ToolView
|
||||
}
|
||||
|
||||
interface StreamExtraPayload {
|
||||
@@ -108,6 +112,8 @@ interface ToolTraceEvent {
|
||||
summary: string
|
||||
detail?: string
|
||||
toolName?: string
|
||||
argumentView?: ToolView
|
||||
resultView?: ToolView
|
||||
}
|
||||
|
||||
interface StatusTraceEvent {
|
||||
@@ -265,6 +271,7 @@ 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 scheduleResultSeqMap = reactive<Record<string, number>>({})
|
||||
const businessCardEventsMap = reactive<Record<string, TimelineBusinessCardPayload[]>>({})
|
||||
const isFineTuneModalVisible = ref(false)
|
||||
const fineTuneLoading = ref(false)
|
||||
@@ -693,6 +700,7 @@ function clearToolTraceState(messageId: string) {
|
||||
delete assistantContentBlocksMap[messageId]
|
||||
delete assistantTimelineLastKindMap[messageId]
|
||||
delete scheduleResultMap[messageId]
|
||||
delete scheduleResultSeqMap[messageId]
|
||||
delete businessCardEventsMap[messageId]
|
||||
for (const key of Object.keys(toolTraceExpandedMap)) {
|
||||
if (key.startsWith(`${messageId}:tool:`)) {
|
||||
@@ -707,6 +715,8 @@ function appendToolTraceEvent(
|
||||
summary: string,
|
||||
detail = '',
|
||||
toolName = '',
|
||||
argumentView?: ToolView,
|
||||
resultView?: ToolView,
|
||||
) {
|
||||
const normalizedSummary = summary.trim()
|
||||
if (!normalizedSummary) {
|
||||
@@ -745,6 +755,8 @@ function appendToolTraceEvent(
|
||||
summary: normalizedSummary,
|
||||
detail: normalizedDetail || undefined,
|
||||
toolName: normalizedToolName || undefined,
|
||||
argumentView,
|
||||
resultView,
|
||||
})
|
||||
assistantTimelineLastKindMap[messageId] = 'tool'
|
||||
}
|
||||
@@ -1569,6 +1581,17 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
})
|
||||
}
|
||||
|
||||
if (scheduleResultMap[source.id]) {
|
||||
blocks.push({
|
||||
id: `${source.id}:schedule-card`,
|
||||
type: 'schedule_card',
|
||||
seq: scheduleResultSeqMap[source.id] || 1000000,
|
||||
schedulePreview: scheduleResultMap[source.id],
|
||||
sourceId: source.id,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
const contentBlocks = assistantContentBlocksMap[source.id] || []
|
||||
if (contentBlocks.length > 0) {
|
||||
hasContentBlock = true
|
||||
@@ -1599,16 +1622,6 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
||||
}
|
||||
}
|
||||
|
||||
const schedulePreview = scheduleResultMap[dm.id]
|
||||
if (schedulePreview) {
|
||||
blocks.push({
|
||||
id: `${dm.id}:schedule-card`,
|
||||
type: 'schedule_card',
|
||||
seq: nextAssistantTimelineSeq(),
|
||||
schedulePreview,
|
||||
})
|
||||
}
|
||||
|
||||
if (!hasContentBlock && dm.content) {
|
||||
fallbackSeq += 1
|
||||
blocks.push({
|
||||
@@ -2038,17 +2051,32 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
|
||||
case 'tool_call':
|
||||
if (event.payload?.tool) {
|
||||
const t = event.payload.tool
|
||||
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name)
|
||||
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view)
|
||||
}
|
||||
break
|
||||
|
||||
case 'tool_result':
|
||||
if (event.payload?.tool) {
|
||||
const t = event.payload.tool
|
||||
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name)
|
||||
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view)
|
||||
}
|
||||
break
|
||||
|
||||
case 'schedule_completed':
|
||||
// 为该 assistant message 添加一个 schedule_card 占位卡
|
||||
scheduleResultMap[mid] = {
|
||||
conversation_id: conversationId,
|
||||
trace_id: '',
|
||||
summary: '日程表编排已就绪',
|
||||
candidate_plans: [],
|
||||
hybrid_entries: [],
|
||||
task_class_ids: [],
|
||||
generated_at: event.created_at || new Date().toISOString(),
|
||||
is_placeholder: true
|
||||
} as any
|
||||
scheduleResultSeqMap[mid] = event.seq || nextAssistantTimelineSeq()
|
||||
break
|
||||
|
||||
case 'confirm_request':
|
||||
confirmOnlyStreamMap[mid] = true
|
||||
// 记录确认卡片
|
||||
@@ -2532,6 +2560,24 @@ function handleStreamExtraEvent(extra: StreamExtraPayload | undefined, assistant
|
||||
return
|
||||
}
|
||||
|
||||
if (extra.kind === 'schedule_completed') {
|
||||
// 为当前助理消息添加一个排程卡片占位符
|
||||
const mid = assistantMessage.id
|
||||
scheduleResultMap[mid] = {
|
||||
conversation_id: selectedConversationId.value,
|
||||
trace_id: '',
|
||||
summary: '日程表编排已就绪',
|
||||
candidate_plans: [],
|
||||
hybrid_entries: [],
|
||||
task_class_ids: [],
|
||||
generated_at: new Date().toISOString(),
|
||||
is_placeholder: true
|
||||
} as any
|
||||
scheduleResultSeqMap[mid] = nextAssistantTimelineSeq()
|
||||
scheduleScrollMessagesToBottom(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (extra.kind === 'tool_call' && extra.tool) {
|
||||
appendToolTraceEvent(
|
||||
assistantMessage.id,
|
||||
@@ -2539,6 +2585,8 @@ function handleStreamExtraEvent(extra: StreamExtraPayload | undefined, assistant
|
||||
normalizeToolSummary(extra.tool),
|
||||
buildToolDetail(extra.tool),
|
||||
`${extra.tool.name || ''}`,
|
||||
extra.tool.argument_view,
|
||||
extra.tool.result_view,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -2550,6 +2598,8 @@ function handleStreamExtraEvent(extra: StreamExtraPayload | undefined, assistant
|
||||
normalizeToolSummary(extra.tool),
|
||||
buildToolDetail(extra.tool),
|
||||
`${extra.tool.name || ''}`,
|
||||
extra.tool.argument_view,
|
||||
extra.tool.result_view,
|
||||
)
|
||||
if (extra.tool.status === 'done') {
|
||||
void loadConversationContextStats(selectedConversationId.value, true)
|
||||
@@ -3160,30 +3210,19 @@ onBeforeUnmount(() => {
|
||||
<div v-else class="chat-message__assistant-flow">
|
||||
<TransitionGroup name="inner-fade">
|
||||
<div v-for="block in getDisplayAssistantBlocks(dm)" :key="block.id">
|
||||
<article v-if="block.type === 'tool'" class="chat-message__tool">
|
||||
<button
|
||||
type="button"
|
||||
class="chat-message__tool-head"
|
||||
@click="block.event && toggleToolTraceExpanded(block.event.id)"
|
||||
>
|
||||
<span class="chat-message__tool-icon" aria-hidden="true">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="chat-message__tool-summary">{{ block.event?.summary }}</span>
|
||||
<em class="chat-message__tool-badge">{{ getToolTraceStateLabel(block.event?.state || 'completed') }}</em>
|
||||
<span class="chat-message__tool-chevron" :class="{ 'chat-message__tool-chevron--expanded': block.event ? isToolTraceExpanded(block.event.id) : false }" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p v-if="block.event && isToolTraceExpanded(block.event.id) && block.event.detail" class="chat-message__tool-detail">
|
||||
{{ block.event.detail }}
|
||||
</p>
|
||||
</article>
|
||||
<ToolCardRenderer
|
||||
v-if="block.type === 'tool' && block.event"
|
||||
:payload="{
|
||||
name: block.event.toolName || '',
|
||||
status: block.event.state === 'called' ? 'start' : (block.event.state === 'completed' ? 'done' : block.event.state),
|
||||
summary: block.event.summary,
|
||||
arguments_preview: block.event.detail,
|
||||
argument_view: block.event.argumentView,
|
||||
result_view: block.event.resultView
|
||||
}"
|
||||
:expanded="isToolTraceExpanded(block.id)"
|
||||
@toggle="toggleToolTraceExpanded(block.id)"
|
||||
/>
|
||||
|
||||
<div v-else-if="block.type === 'status'" class="chat-message__status-line">
|
||||
<span class="chat-message__status-icon" aria-hidden="true">
|
||||
|
||||
633
frontend/src/components/dashboard/ToolCardRenderer.vue
Normal file
633
frontend/src/components/dashboard/ToolCardRenderer.vue
Normal file
@@ -0,0 +1,633 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { TimelineToolPayload, ToolView } from '@/api/schedule_agent'
|
||||
|
||||
const props = defineProps<{
|
||||
payload: TimelineToolPayload
|
||||
expanded: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle'): void
|
||||
}>()
|
||||
|
||||
function getStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
start: '进行中',
|
||||
done: '已完成',
|
||||
failed: '失败',
|
||||
blocked: '已拦截',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
// 模拟原有的 getOperationLabel 逻辑(如果后端没传标签)
|
||||
function getOperationFallbackLabel(op: string) {
|
||||
const map: Record<string, string> = {
|
||||
move: '移动',
|
||||
place: '放置',
|
||||
swap: '交换',
|
||||
batch_move: '批量移动',
|
||||
unplace: '取消放置',
|
||||
queue_apply_head_move: '队列首项确认',
|
||||
}
|
||||
return map[op] || op
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="tool-card"
|
||||
:class="[
|
||||
`tool-card--${payload.status}`,
|
||||
{ 'tool-card--expanded': expanded },
|
||||
]"
|
||||
>
|
||||
<!-- 1. 折叠态头部 (优先取 result_view.collapsed) -->
|
||||
<header class="tool-card__header" @click="emit('toggle')">
|
||||
<div class="tool-card__icon-box">
|
||||
<svg v-if="payload.status === 'failed'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="tool-card__title-group">
|
||||
<div class="tool-card__title-row">
|
||||
<h3 class="tool-card__title">
|
||||
{{ payload.result_view?.collapsed?.title || payload.summary }}
|
||||
</h3>
|
||||
<span class="tool-card__badge">
|
||||
{{ payload.result_view?.collapsed?.status_label || getStatusLabel(payload.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="tool-card__subtitle">
|
||||
{{ payload.result_view?.collapsed?.subtitle || payload.arguments_preview }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 简短指标区 -->
|
||||
<div v-if="!expanded && payload.result_view?.collapsed?.metrics" class="tool-card__metrics">
|
||||
<div v-for="(m, mi) in payload.result_view.collapsed.metrics" :key="mi" class="metric-item">
|
||||
<span class="metric-value">{{ m.value }}</span>
|
||||
<span class="metric-label">{{ m.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!expanded && payload.result_view?.collapsed?.operation_label" class="tool-card__metrics">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">{{ payload.result_view.collapsed.operation_label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-card__chevron" :class="{ 'tool-card__chevron--expanded': expanded }">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 2. 展开态详情 -->
|
||||
<transition name="tool-expand">
|
||||
<section v-if="expanded" class="tool-card__content">
|
||||
<div class="tool-card__divider"></div>
|
||||
|
||||
<!-- 2.1 参数展示 (优先读取 argument_view) -->
|
||||
<div v-if="payload.argument_view" class="section-block section-arguments">
|
||||
<h4 class="detail-section-title">参数详情</h4>
|
||||
<p v-if="payload.argument_view.collapsed?.summary" class="arg-summary">
|
||||
{{ payload.argument_view.collapsed.summary }}
|
||||
</p>
|
||||
<div v-if="payload.argument_view.expanded?.fields" class="arg-fields">
|
||||
<div v-for="(f, fi) in payload.argument_view.expanded.fields" :key="fi" class="arg-field-item">
|
||||
<span class="arg-label">{{ f.label }}</span>
|
||||
<span class="arg-value">{{ f.display }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2.2 结果渲染: schedule.operation_result -->
|
||||
<div v-if="payload.result_view?.view_type === 'schedule.operation_result'" class="section-block view-operation">
|
||||
<h4 class="detail-section-title">操作结果</h4>
|
||||
<div v-if="payload.result_view.expanded?.changes?.length" class="changes-list">
|
||||
<div v-for="(change, idx) in payload.result_view.expanded.changes" :key="idx" class="change-item">
|
||||
<div class="change-item__header">
|
||||
<span class="change-item__task-icon"></span>
|
||||
<span class="change-item__task-name">{{ change.task_label }}</span>
|
||||
<span v-if="change.status_label" class="change-item__status-tag">{{ change.status_label }}</span>
|
||||
</div>
|
||||
|
||||
<div class="change-item__path">
|
||||
<div class="slot-box slot-box--before">
|
||||
<span class="slot-tag">之前</span>
|
||||
<div class="slot-text">{{ change.before_label || '未排程' }}</div>
|
||||
</div>
|
||||
<div class="path-arrow">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="slot-box slot-box--after">
|
||||
<span class="slot-tag">之后</span>
|
||||
<div class="slot-text">{{ change.after_label || '未排程' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 队列快照 (带标签) -->
|
||||
<div v-if="payload.result_view.expanded?.queue_snapshot" class="queue-snapshot">
|
||||
<h5 class="sub-section-title">{{ payload.result_view.expanded.queue_snapshot.summary_label || '队列变更' }}</h5>
|
||||
<div class="queue-compare">
|
||||
<div class="queue-side">
|
||||
<span class="queue-count">{{ payload.result_view.expanded.queue_snapshot.before_label }}</span>
|
||||
</div>
|
||||
<div class="queue-arrow"></div>
|
||||
<div class="queue-side">
|
||||
<span class="queue-count highlight">{{ payload.result_view.expanded.queue_snapshot.after_label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 失败信息 -->
|
||||
<div v-if="payload.result_view.expanded?.failure_reason" class="failure-box">
|
||||
<span class="failure-icon">!</span>
|
||||
<p class="failure-text">{{ payload.result_view.expanded.failure_reason }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2.3 结果渲染: legacy_text -->
|
||||
<div v-else-if="payload.result_view?.view_type === 'legacy_text'" class="section-block view-legacy">
|
||||
<h4 class="detail-section-title">{{ payload.result_view.expanded?.raw_text_label || '输出内容' }}</h4>
|
||||
<div class="raw-text-container">
|
||||
<pre class="raw-text">{{ payload.result_view.expanded?.raw_text }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2.4 旧协议兜底 -->
|
||||
<div v-else-if="!payload.result_view" class="section-block view-old-fallback">
|
||||
<h4 class="detail-section-title">工具输出 (兼容模式)</h4>
|
||||
<div class="fallback-summary-box">
|
||||
<p class="fallback-summary">{{ payload.summary }}</p>
|
||||
<div v-if="payload.arguments_preview" class="fallback-json-box">
|
||||
<span class="json-label">调用参数:</span>
|
||||
<code>{{ payload.arguments_preview }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原始 Observation (仅开发/调试可见入口, 默认收起) -->
|
||||
<details v-if="payload.result_view?.expanded?.raw_text" class="debug-details">
|
||||
<summary>调试信息 (RAW Observation)</summary>
|
||||
<pre class="debug-raw-pre">{{ payload.result_view.expanded.raw_text }}</pre>
|
||||
</details>
|
||||
</section>
|
||||
</transition>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Tool Card Styles */
|
||||
.tool-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef2f6;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.02);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
border-color: #d1d5db;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.tool-card--expanded {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 8px 16px -4px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.tool-card--failed {
|
||||
border-left: 4px solid #f43f5e;
|
||||
}
|
||||
|
||||
.tool-card--done {
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.tool-card__header {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tool-card__icon-box {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tool-card--done .tool-card__icon-box {
|
||||
color: #10b981;
|
||||
background: #f0fdf4;
|
||||
border-color: #dcfce7;
|
||||
}
|
||||
|
||||
.tool-card--failed .tool-card__icon-box {
|
||||
color: #f43f5e;
|
||||
background: #fff1f2;
|
||||
border-color: #fee2e2;
|
||||
}
|
||||
|
||||
.tool-card__title-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-card__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.tool-card__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.tool-card__badge {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 1px 8px;
|
||||
border-radius: 6px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tool-card--done .tool-card__badge {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.tool-card--failed .tool-card__badge {
|
||||
background: #fff1f2;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.tool-card__subtitle {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tool-card__metrics {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #334155;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool-card__chevron {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #cbd5e1;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.tool-card__chevron--expanded {
|
||||
transform: rotate(180deg);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.tool-card__content {
|
||||
padding: 0 16px 20px;
|
||||
}
|
||||
|
||||
.tool-card__divider {
|
||||
height: 1px;
|
||||
background: #f1f5f9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-block + .section-block {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Arguments */
|
||||
.arg-summary {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.arg-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
background: #f8fafc;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.arg-field-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.arg-label {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.arg-value {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Operation Changes */
|
||||
.changes-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.change-item {
|
||||
background: #ffffff;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.change-item__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.change-item__task-icon {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #3b82f6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.change-item__task-name {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.change-item__status-tag {
|
||||
font-size: 10px;
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
padding: 0px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.change-item__path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.slot-box {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.slot-box--before {
|
||||
background: #fdfdfd;
|
||||
border: 1px dashed #e2e8f0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.slot-box--after {
|
||||
background: #f0f7ff;
|
||||
border: 1px solid #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.slot-tag {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.slot-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Queue Snapshot */
|
||||
.sub-section-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.queue-snapshot {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f6;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.queue-compare {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.queue-side {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.queue-count {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.queue-count.highlight {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.queue-arrow {
|
||||
flex: 2;
|
||||
height: 2px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Legacy Text */
|
||||
.raw-text-container {
|
||||
background: #1e293b;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.raw-text {
|
||||
margin: 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #e2e8f0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Fallback Old */
|
||||
.fallback-summary-box {
|
||||
padding: 12px;
|
||||
background: #fefce8;
|
||||
border: 1px solid #fef3c7;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.fallback-summary {
|
||||
font-size: 13px;
|
||||
color: #92400e;
|
||||
font-weight: 600;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.fallback-json-box {
|
||||
font-size: 11px;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
/* Failure */
|
||||
.failure-box {
|
||||
margin-top: 14px;
|
||||
padding: 12px;
|
||||
background: #fff1f2;
|
||||
border: 1px solid #fee2e2;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.failure-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #f43f5e;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.failure-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #9f1239;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Debug */
|
||||
.debug-details {
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.debug-details summary {
|
||||
font-size: 11px;
|
||||
color: #cbd5e1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.debug-raw-pre {
|
||||
margin-top: 10px;
|
||||
font-size: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: #94a3b8;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.tool-expand-enter-active,
|
||||
.tool-expand-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
.tool-expand-enter-from,
|
||||
.tool-expand-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user