Files
microfish/frontend/src/components/Step4Report.vue
666ghj fde79721e8 Enhance agent bio display and tool result presentation in Step4Report component
- Updated the AgentInterview class to display the full agent bio, truncating only if it exceeds 1000 characters for better readability.
- Enhanced the Step4Report component to include structured display for tool results, allowing users to toggle between raw and structured views for various tools, improving user experience and clarity.
- Introduced new components for parsing and displaying results from different tools, including InsightForge, PanoramaSearch, InterviewAgents, and QuickSearch, providing a comprehensive view of the data.
2025-12-14 01:29:57 +08:00

2386 lines
64 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="report-panel">
<!-- Top Status Bar -->
<div class="status-bar">
<div class="status-left">
<div class="report-badge">
<span class="badge-icon">📊</span>
<span class="badge-text">Report Agent</span>
</div>
<div class="status-indicator" :class="statusClass">
<span class="dot"></span>
<span class="status-text">{{ statusText }}</span>
</div>
</div>
<div class="status-right">
<div class="stats-group" v-if="reportOutline">
<span class="stat-item">
<span class="stat-label">章节</span>
<span class="stat-value mono">{{ completedSections }}/{{ totalSections }}</span>
</span>
<span class="stat-item">
<span class="stat-label">工具调用</span>
<span class="stat-value mono">{{ totalToolCalls }}</span>
</span>
<span class="stat-item">
<span class="stat-label">耗时</span>
<span class="stat-value mono">{{ formatElapsedTime }}</span>
</span>
</div>
</div>
</div>
<!-- Main Content: Agent Workflow -->
<div class="main-content-area" ref="mainContent">
<!-- Report Outline Card (显示在顶部) -->
<div v-if="reportOutline" class="outline-card">
<div class="outline-header">
<div class="outline-title-wrapper">
<svg class="outline-icon" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<h2 class="outline-title">{{ reportOutline.title }}</h2>
</div>
<span class="outline-badge">大纲已生成</span>
</div>
<p class="outline-summary">{{ reportOutline.summary }}</p>
<div class="outline-sections">
<div
v-for="(section, idx) in reportOutline.sections"
:key="idx"
class="outline-section-item"
:class="{
'completed': isSectionCompleted(idx + 1),
'current': currentSectionIndex === idx + 1,
'expanded': expandedSections.has(idx)
}"
@click="toggleSection(idx)"
>
<div class="section-header">
<span class="section-num">{{ String(idx + 1).padStart(2, '0') }}</span>
<span class="section-title">{{ section.title }}</span>
<span class="section-status">
<svg v-if="isSectionCompleted(idx + 1)" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span v-else-if="currentSectionIndex === idx + 1" class="generating-dot"></span>
</span>
<span class="section-toggle" v-if="generatedSections[idx + 1]">
{{ expandedSections.has(idx) ? '' : '+' }}
</span>
</div>
<!-- 已生成的章节内容预览 -->
<div v-if="expandedSections.has(idx) && generatedSections[idx + 1]" class="section-content-preview">
<div class="content-markdown" v-html="renderMarkdown(generatedSections[idx + 1])"></div>
</div>
</div>
</div>
</div>
<!-- Agent Action Feed -->
<div class="action-feed">
<div class="feed-header" v-if="agentLogs.length > 0">
<span class="feed-title">Agent 工作流</span>
<span class="feed-count">{{ agentLogs.length }} 条记录</span>
</div>
<div class="feed-timeline">
<TransitionGroup name="feed-item">
<div
v-for="(log, idx) in displayLogs"
:key="log.timestamp + '-' + idx"
class="feed-item"
:class="getLogClass(log)"
>
<div class="item-marker">
<div class="marker-icon" :class="getMarkerClass(log)">
<component :is="getLogIcon(log)" />
</div>
</div>
<div class="item-content">
<div class="item-header">
<span class="item-action">{{ getActionLabel(log.action) }}</span>
<span class="item-stage" :class="log.stage">{{ log.stage }}</span>
<span class="item-time">{{ formatTime(log.timestamp) }}</span>
</div>
<!-- 根据不同 action 类型展示不同内容 -->
<div class="item-body">
<!-- report_start -->
<template v-if="log.action === 'report_start'">
<div class="info-block">
<span class="info-label">Simulation:</span>
<span class="info-value mono">{{ log.details?.simulation_id }}</span>
</div>
<div class="info-block" v-if="log.details?.simulation_requirement">
<span class="info-label">需求:</span>
<span class="info-value">{{ log.details.simulation_requirement }}</span>
</div>
</template>
<!-- planning_start / planning_complete -->
<template v-if="log.action === 'planning_start'">
<div class="message-text">{{ log.details?.message }}</div>
</template>
<template v-if="log.action === 'planning_complete'">
<div class="message-text success">{{ log.details?.message }}</div>
<div class="outline-mini" v-if="log.details?.outline">
<span class="mini-label"> {{ log.details.outline.sections?.length || 0 }} 个章节</span>
</div>
</template>
<!-- section_start -->
<template v-if="log.action === 'section_start'">
<div class="section-info">
<span class="section-badge">章节 {{ log.section_index }}</span>
<span class="section-name">{{ log.section_title }}</span>
</div>
</template>
<!-- tool_call -->
<template v-if="log.action === 'tool_call'">
<div class="tool-call-block">
<div class="tool-name">
<svg class="tool-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<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"></path>
</svg>
{{ log.details?.tool_name }}
</div>
<div class="tool-params" v-if="log.details?.parameters">
<pre>{{ formatParams(log.details.parameters) }}</pre>
</div>
</div>
</template>
<!-- tool_result -->
<template v-if="log.action === 'tool_result'">
<div class="tool-result-block" :class="'tool-' + log.details?.tool_name">
<div class="result-header">
<span class="result-tool">{{ getToolDisplayName(log.details?.tool_name) }}</span>
<span class="result-length">{{ log.details?.result_length }} chars</span>
<button
class="toggle-raw-btn"
@click.stop="toggleRawResult(log.timestamp)"
>
{{ showRawResult[log.timestamp] ? '收起原文' : '查看原文' }}
</button>
</div>
<!-- 结构化展示 -->
<div class="result-structured" v-if="!showRawResult[log.timestamp] && log.details?.result">
<!-- insight_forge 深度洞察 -->
<template v-if="log.details?.tool_name === 'insight_forge'">
<InsightForgeResult :result="parseInsightForge(log.details.result)" />
</template>
<!-- panorama_search 广度搜索 -->
<template v-else-if="log.details?.tool_name === 'panorama_search'">
<PanoramaResult :result="parsePanorama(log.details.result)" />
</template>
<!-- interview_agents 深度采访 -->
<template v-else-if="log.details?.tool_name === 'interview_agents'">
<InterviewResult :result="parseInterview(log.details.result)" />
</template>
<!-- quick_search 简单搜索 -->
<template v-else-if="log.details?.tool_name === 'quick_search'">
<QuickSearchResult :result="parseQuickSearch(log.details.result)" />
</template>
<!-- 其他工具 - 显示原文 -->
<template v-else>
<div class="result-content">
<pre>{{ log.details.result }}</pre>
</div>
</template>
</div>
<!-- 原文展示 -->
<div class="result-raw" v-if="showRawResult[log.timestamp] && log.details?.result">
<pre>{{ log.details.result }}</pre>
</div>
</div>
</template>
<!-- llm_response -->
<template v-if="log.action === 'llm_response'">
<div class="llm-response-block">
<div class="response-meta">
<span class="meta-item" v-if="log.details?.iteration">
迭代 #{{ log.details.iteration }}
</span>
<span class="meta-item" :class="{ active: log.details?.has_tool_calls }">
工具调用: {{ log.details?.has_tool_calls ? '是' : '否' }}
</span>
<span class="meta-item" :class="{ active: log.details?.has_final_answer }">
最终答案: {{ log.details?.has_final_answer ? '是' : '否' }}
</span>
</div>
<div class="response-content" v-if="log.details?.response">
<pre>{{ log.details.response }}</pre>
</div>
</div>
</template>
<!-- section_complete -->
<template v-if="log.action === 'section_complete'">
<div class="complete-info">
<svg class="complete-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<span>章节{{ log.section_title }}生成完成</span>
</div>
</template>
<!-- report_complete -->
<template v-if="log.action === 'report_complete'">
<div class="complete-info success">
<svg class="complete-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<span>报告生成完成</span>
</div>
</template>
<!-- 通用消息 -->
<template v-if="!['report_start', 'planning_start', 'planning_complete', 'section_start', 'tool_call', 'tool_result', 'llm_response', 'section_complete', 'report_complete'].includes(log.action)">
<div class="message-text">{{ log.details?.message || log.action }}</div>
</template>
</div>
<div class="item-footer" v-if="log.elapsed_seconds">
<span class="elapsed">+{{ log.elapsed_seconds.toFixed(2) }}s</span>
</div>
</div>
</div>
</TransitionGroup>
<!-- 等待状态 -->
<div v-if="agentLogs.length === 0 && !isComplete" class="waiting-state">
<div class="pulse-ring"></div>
<span>等待 Report Agent 启动...</span>
</div>
</div>
</div>
</div>
<!-- Bottom Console Logs -->
<div class="console-logs">
<div class="log-header">
<span class="log-title">CONSOLE OUTPUT</span>
<span class="log-id">{{ reportId || 'NO_REPORT' }}</span>
</div>
<div class="log-content" ref="logContent">
<div class="log-line" v-for="(log, idx) in consoleLogs" :key="idx">
<span class="log-msg" :class="getLogLevelClass(log)">{{ log }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick, h, reactive } from 'vue'
import { getAgentLog, getConsoleLog } from '../api/report'
const props = defineProps({
reportId: String,
simulationId: String,
systemLogs: Array
})
const emit = defineEmits(['add-log', 'update-status'])
// State
const agentLogs = ref([])
const consoleLogs = ref([])
const agentLogLine = ref(0)
const consoleLogLine = ref(0)
const reportOutline = ref(null)
const currentSectionIndex = ref(null)
const generatedSections = ref({}) // { sectionIndex: content }
const expandedSections = ref(new Set())
const isComplete = ref(false)
const startTime = ref(null)
const mainContent = ref(null)
const logContent = ref(null)
const showRawResult = reactive({}) // 控制显示原文
// 切换显示原文
const toggleRawResult = (timestamp) => {
showRawResult[timestamp] = !showRawResult[timestamp]
}
// 工具显示名称
const getToolDisplayName = (toolName) => {
const names = {
'insight_forge': '🔍 深度洞察检索',
'panorama_search': '🌐 广度搜索',
'interview_agents': '🎤 深度采访',
'quick_search': '⚡ 快速检索',
'get_graph_statistics': '📊 图谱统计',
'get_entities_by_type': '👥 实体查询'
}
return names[toolName] || toolName
}
// ========== 工具结果解析器 ==========
// 解析 insight_forge 结果
const parseInsightForge = (text) => {
const result = {
query: '',
requirement: '',
stats: { facts: 0, entities: 0, relationships: 0 },
subQueries: [],
facts: [],
entities: [],
relations: []
}
try {
// 提取原始问题
const queryMatch = text.match(/原始问题:\s*(.+?)(?:\n|$)/)
if (queryMatch) result.query = queryMatch[1].trim()
// 提取模拟需求
const reqMatch = text.match(/模拟需求:\s*(.+?)(?:\n|$)/)
if (reqMatch) result.requirement = reqMatch[1].trim()
// 提取统计
const factMatch = text.match(/相关事实:\s*(\d+)/)
const entityMatch = text.match(/涉及实体:\s*(\d+)/)
const relMatch = text.match(/关系链:\s*(\d+)/)
if (factMatch) result.stats.facts = parseInt(factMatch[1])
if (entityMatch) result.stats.entities = parseInt(entityMatch[1])
if (relMatch) result.stats.relationships = parseInt(relMatch[1])
// 提取子问题
const subQSection = text.match(/### 分析的子问题\n([\s\S]*?)(?=###|\n\n###|$)/)
if (subQSection) {
const lines = subQSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.subQueries = lines.map(l => l.replace(/^\d+\.\s*/, '').trim())
}
// 提取关键事实
const factsSection = text.match(/### 【关键事实】[\s\S]*?\n([\s\S]*?)(?=###|$)/)
if (factsSection) {
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.facts = lines.map(l => {
const match = l.match(/^\d+\.\s*"?(.+?)"?\s*$/)
return match ? match[1].replace(/^"|"$/g, '') : l.replace(/^\d+\.\s*/, '').trim()
}).slice(0, 15) // 限制显示数量
}
// 提取核心实体
const entitySection = text.match(/### 【核心实体】\n([\s\S]*?)(?=###|$)/)
if (entitySection) {
const entityBlocks = entitySection[1].split(/\n- \*\*/).slice(1)
result.entities = entityBlocks.map(block => {
const nameMatch = block.match(/^(.+?)\*\*\s*\((.+?)\)/)
const summaryMatch = block.match(/摘要:\s*"?(.+?)"?\n/)
const factsMatch = block.match(/相关事实:\s*(\d+)/)
return {
name: nameMatch ? nameMatch[1].trim() : '',
type: nameMatch ? nameMatch[2].trim() : '',
summary: summaryMatch ? summaryMatch[1].trim() : '',
factCount: factsMatch ? parseInt(factsMatch[1]) : 0
}
}).filter(e => e.name).slice(0, 10)
}
// 提取关系链
const relSection = text.match(/### 【关系链】\n([\s\S]*?)(?=###|$)/)
if (relSection) {
const lines = relSection[1].split('\n').filter(l => l.startsWith('-'))
result.relations = lines.map(l => {
const match = l.match(/^-\s*(.+?)\s*--\[(.+?)\]-->\s*(.+)$/)
if (match) {
return { source: match[1].trim(), relation: match[2].trim(), target: match[3].trim() }
}
return null
}).filter(Boolean).slice(0, 10)
}
} catch (e) {
console.warn('解析 insight_forge 结果失败:', e)
}
return result
}
// 解析 panorama_search 结果
const parsePanorama = (text) => {
const result = {
query: '',
stats: { nodes: 0, edges: 0, activeFacts: 0, historicalFacts: 0 },
activeFacts: [],
historicalFacts: [],
entities: []
}
try {
// 提取查询
const queryMatch = text.match(/查询:\s*(.+?)(?:\n|$)/)
if (queryMatch) result.query = queryMatch[1].trim()
// 提取统计
const nodesMatch = text.match(/总节点数:\s*(\d+)/)
const edgesMatch = text.match(/总边数:\s*(\d+)/)
const activeMatch = text.match(/当前有效事实:\s*(\d+)/)
const histMatch = text.match(/历史\/过期事实:\s*(\d+)/)
if (nodesMatch) result.stats.nodes = parseInt(nodesMatch[1])
if (edgesMatch) result.stats.edges = parseInt(edgesMatch[1])
if (activeMatch) result.stats.activeFacts = parseInt(activeMatch[1])
if (histMatch) result.stats.historicalFacts = parseInt(histMatch[1])
// 提取当前有效事实
const activeSection = text.match(/### 【当前有效事实】[\s\S]*?\n([\s\S]*?)(?=###|$)/)
if (activeSection) {
const lines = activeSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.activeFacts = lines.map(l => {
const match = l.match(/^\d+\.\s*"?(.+?)"?\s*$/)
return match ? match[1].replace(/^"|"$/g, '') : l.replace(/^\d+\.\s*/, '').trim()
}).slice(0, 15)
}
// 提取历史事实
const histSection = text.match(/### 【历史\/过期事实】[\s\S]*?\n([\s\S]*?)(?=###|$)/)
if (histSection) {
const lines = histSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.historicalFacts = lines.map(l => {
const content = l.replace(/^\d+\.\s*/, '').trim()
// 提取时间范围
const timeMatch = content.match(/^\[(.+?)\s*-\s*(.+?)\]\s*(.+)$/)
if (timeMatch) {
return { timeRange: `${timeMatch[1]} - ${timeMatch[2]}`, content: timeMatch[3].replace(/^"|"$/g, '') }
}
return { timeRange: '', content: content.replace(/^"|"$/g, '') }
}).slice(0, 10)
}
// 提取涉及实体
const entitySection = text.match(/### 【涉及实体】\n([\s\S]*?)(?=###|$)/)
if (entitySection) {
const lines = entitySection[1].split('\n').filter(l => l.startsWith('-'))
result.entities = lines.map(l => {
const match = l.match(/^-\s*\*\*(.+?)\*\*\s*\((.+?)\)/)
if (match) return { name: match[1].trim(), type: match[2].trim() }
return null
}).filter(Boolean).slice(0, 15)
}
} catch (e) {
console.warn('解析 panorama_search 结果失败:', e)
}
return result
}
// 解析 interview_agents 结果
const parseInterview = (text) => {
const result = {
topic: '',
agentCount: '',
selectionReason: '',
interviews: [],
summary: ''
}
try {
// 提取采访主题
const topicMatch = text.match(/\*\*采访主题:\*\*\s*(.+?)(?:\n|$)/)
if (topicMatch) result.topic = topicMatch[1].trim()
// 提取采访人数
const countMatch = text.match(/\*\*采访人数:\*\*\s*(.+?)(?:\n|$)/)
if (countMatch) result.agentCount = countMatch[1].trim()
// 提取选择理由
const reasonSection = text.match(/### 采访对象选择理由\n([\s\S]*?)(?=---|###|$)/)
if (reasonSection) {
result.selectionReason = reasonSection[1].trim().substring(0, 300) + '...'
}
// 提取采访实录
const interviewMatches = text.matchAll(/#### 采访 #(\d+):\s*(.+?)\n\*\*(.+?)\*\*\s*\((.+?)\)\n_简介:\s*(.+?)_\n\n\*\*Q:\*\*\s*([\s\S]*?)\n\n\*\*A:\*\*\s*([\s\S]*?)(?=\*\*关键引言|\n---|\n####|$)/g)
for (const match of interviewMatches) {
const interview = {
num: match[1],
title: match[2].trim(),
name: match[3].trim(),
role: match[4].trim(),
bio: match[5].trim().substring(0, 100) + '...',
question: match[6].trim().split('\n')[0].substring(0, 150) + '...',
answer: match[7].trim().substring(0, 500) + '...',
quotes: []
}
// 提取关键引言
const quoteSection = text.match(new RegExp(`#### 采访 #${match[1]}[\\s\\S]*?\\*\\*关键引言:\\*\\*\\n([\\s\\S]*?)(?=\\n---)`))
if (quoteSection) {
const quotes = quoteSection[1].match(/> "(.+?)"/g)
if (quotes) {
interview.quotes = quotes.map(q => q.replace(/^> "|"$/g, '')).slice(0, 2)
}
}
result.interviews.push(interview)
}
// 提取采访摘要
const summarySection = text.match(/### 采访摘要与核心观点\n([\s\S]*?)$/)
if (summarySection) {
result.summary = summarySection[1].trim().substring(0, 500) + '...'
}
} catch (e) {
console.warn('解析 interview_agents 结果失败:', e)
}
return result
}
// 解析 quick_search 结果
const parseQuickSearch = (text) => {
const result = {
query: '',
count: 0,
facts: []
}
try {
const queryMatch = text.match(/搜索查询:\s*(.+?)(?:\n|$)/)
if (queryMatch) result.query = queryMatch[1].trim()
const countMatch = text.match(/找到\s*(\d+)\s*条/)
if (countMatch) result.count = parseInt(countMatch[1])
const factsSection = text.match(/### 相关事实:\n([\s\S]*)$/)
if (factsSection) {
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.facts = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).slice(0, 20)
}
} catch (e) {
console.warn('解析 quick_search 结果失败:', e)
}
return result
}
// ========== 子组件定义 ==========
// InsightForge 结果展示组件
const InsightForgeResult = {
props: ['result'],
setup(props) {
const expanded = ref({ facts: true, entities: false, relations: false, subQueries: false })
const toggleSection = (section) => { expanded.value[section] = !expanded.value[section] }
return () => h('div', { class: 'insight-result' }, [
// 统计卡片
h('div', { class: 'stats-cards' }, [
h('div', { class: 'stat-card facts' }, [
h('span', { class: 'stat-num' }, props.result.stats.facts),
h('span', { class: 'stat-name' }, '相关事实')
]),
h('div', { class: 'stat-card entities' }, [
h('span', { class: 'stat-num' }, props.result.stats.entities),
h('span', { class: 'stat-name' }, '涉及实体')
]),
h('div', { class: 'stat-card relations' }, [
h('span', { class: 'stat-num' }, props.result.stats.relationships),
h('span', { class: 'stat-name' }, '关系链')
])
]),
// 子问题
props.result.subQueries.length > 0 && h('div', { class: 'collapsible-section' }, [
h('div', { class: 'section-title', onClick: () => toggleSection('subQueries') }, [
h('span', {}, '📋 分析的子问题'),
h('span', { class: 'toggle-icon' }, expanded.value.subQueries ? '' : '+')
]),
expanded.value.subQueries && h('div', { class: 'sub-queries' },
props.result.subQueries.map((q, i) => h('div', { class: 'sub-query', key: i }, [
h('span', { class: 'query-num' }, i + 1),
h('span', { class: 'query-text' }, q)
]))
)
]),
// 关键事实
props.result.facts.length > 0 && h('div', { class: 'collapsible-section' }, [
h('div', { class: 'section-title', onClick: () => toggleSection('facts') }, [
h('span', {}, `📌 关键事实 (${props.result.facts.length})`),
h('span', { class: 'toggle-icon' }, expanded.value.facts ? '' : '+')
]),
expanded.value.facts && h('div', { class: 'facts-list' },
props.result.facts.map((fact, i) => h('div', { class: 'fact-item', key: i }, [
h('span', { class: 'fact-num' }, i + 1),
h('span', { class: 'fact-text' }, fact)
]))
)
]),
// 核心实体
props.result.entities.length > 0 && h('div', { class: 'collapsible-section' }, [
h('div', { class: 'section-title', onClick: () => toggleSection('entities') }, [
h('span', {}, `👥 核心实体 (${props.result.entities.length})`),
h('span', { class: 'toggle-icon' }, expanded.value.entities ? '' : '+')
]),
expanded.value.entities && h('div', { class: 'entities-grid' },
props.result.entities.map((e, i) => h('div', { class: 'entity-card', key: i }, [
h('div', { class: 'entity-header' }, [
h('span', { class: 'entity-name' }, e.name),
h('span', { class: 'entity-type' }, e.type)
]),
e.summary && h('div', { class: 'entity-summary' }, e.summary.substring(0, 100) + '...')
]))
)
]),
// 关系链
props.result.relations.length > 0 && h('div', { class: 'collapsible-section' }, [
h('div', { class: 'section-title', onClick: () => toggleSection('relations') }, [
h('span', {}, `🔗 关系链 (${props.result.relations.length})`),
h('span', { class: 'toggle-icon' }, expanded.value.relations ? '' : '+')
]),
expanded.value.relations && h('div', { class: 'relations-list' },
props.result.relations.map((r, i) => h('div', { class: 'relation-item', key: i }, [
h('span', { class: 'rel-source' }, r.source),
h('span', { class: 'rel-arrow' }, '→'),
h('span', { class: 'rel-type' }, r.relation),
h('span', { class: 'rel-arrow' }, '→'),
h('span', { class: 'rel-target' }, r.target)
]))
)
])
])
}
}
// PanoramaResult 展示组件
const PanoramaResult = {
props: ['result'],
setup(props) {
const expanded = ref({ active: true, history: false, entities: false })
const toggleSection = (section) => { expanded.value[section] = !expanded.value[section] }
return () => h('div', { class: 'panorama-result' }, [
// 统计卡片
h('div', { class: 'stats-cards' }, [
h('div', { class: 'stat-card nodes' }, [
h('span', { class: 'stat-num' }, props.result.stats.nodes),
h('span', { class: 'stat-name' }, '总节点')
]),
h('div', { class: 'stat-card edges' }, [
h('span', { class: 'stat-num' }, props.result.stats.edges),
h('span', { class: 'stat-name' }, '总边数')
]),
h('div', { class: 'stat-card active' }, [
h('span', { class: 'stat-num' }, props.result.stats.activeFacts),
h('span', { class: 'stat-name' }, '有效事实')
]),
h('div', { class: 'stat-card history' }, [
h('span', { class: 'stat-num' }, props.result.stats.historicalFacts),
h('span', { class: 'stat-name' }, '历史事实')
])
]),
// 当前有效事实
props.result.activeFacts.length > 0 && h('div', { class: 'collapsible-section' }, [
h('div', { class: 'section-title active', onClick: () => toggleSection('active') }, [
h('span', {}, `✅ 当前有效事实 (${props.result.activeFacts.length})`),
h('span', { class: 'toggle-icon' }, expanded.value.active ? '' : '+')
]),
expanded.value.active && h('div', { class: 'facts-list' },
props.result.activeFacts.map((fact, i) => h('div', { class: 'fact-item active', key: i }, [
h('span', { class: 'fact-num' }, i + 1),
h('span', { class: 'fact-text' }, fact)
]))
)
]),
// 历史事实
props.result.historicalFacts.length > 0 && h('div', { class: 'collapsible-section' }, [
h('div', { class: 'section-title history', onClick: () => toggleSection('history') }, [
h('span', {}, `📜 历史/过期事实 (${props.result.historicalFacts.length})`),
h('span', { class: 'toggle-icon' }, expanded.value.history ? '' : '+')
]),
expanded.value.history && h('div', { class: 'facts-list' },
props.result.historicalFacts.map((fact, i) => h('div', { class: 'fact-item history', key: i }, [
h('span', { class: 'fact-num' }, i + 1),
h('div', { class: 'fact-content' }, [
fact.timeRange && h('span', { class: 'time-range' }, fact.timeRange),
h('span', { class: 'fact-text' }, fact.content)
])
]))
)
]),
// 涉及实体
props.result.entities.length > 0 && h('div', { class: 'collapsible-section' }, [
h('div', { class: 'section-title', onClick: () => toggleSection('entities') }, [
h('span', {}, `👥 涉及实体 (${props.result.entities.length})`),
h('span', { class: 'toggle-icon' }, expanded.value.entities ? '' : '+')
]),
expanded.value.entities && h('div', { class: 'entity-tags' },
props.result.entities.map((e, i) => h('span', { class: 'entity-tag', key: i }, [
h('span', { class: 'tag-name' }, e.name),
h('span', { class: 'tag-type' }, e.type)
]))
)
])
])
}
}
// InterviewResult 展示组件
const InterviewResult = {
props: ['result'],
setup(props) {
const expandedInterview = ref(0)
return () => h('div', { class: 'interview-result' }, [
// 采访信息
h('div', { class: 'interview-header' }, [
h('div', { class: 'interview-topic' }, props.result.topic),
h('div', { class: 'interview-count' }, props.result.agentCount)
]),
// 采访列表
props.result.interviews.length > 0 && h('div', { class: 'interviews-list' },
props.result.interviews.map((interview, i) => h('div', {
class: ['interview-card', { expanded: expandedInterview.value === i }],
key: i,
onClick: () => { expandedInterview.value = expandedInterview.value === i ? -1 : i }
}, [
h('div', { class: 'interview-card-header' }, [
h('span', { class: 'interview-num' }, `#${interview.num}`),
h('span', { class: 'interview-name' }, interview.name),
h('span', { class: 'interview-role' }, interview.role),
h('span', { class: 'expand-icon' }, expandedInterview.value === i ? '' : '+')
]),
expandedInterview.value === i && h('div', { class: 'interview-card-body' }, [
h('div', { class: 'interview-bio' }, interview.bio),
h('div', { class: 'interview-qa' }, [
h('div', { class: 'qa-question' }, [
h('span', { class: 'qa-label' }, 'Q:'),
h('span', {}, interview.question)
]),
h('div', { class: 'qa-answer' }, [
h('span', { class: 'qa-label' }, 'A:'),
h('span', {}, interview.answer)
])
]),
interview.quotes.length > 0 && h('div', { class: 'interview-quotes' },
interview.quotes.map((q, qi) => h('div', { class: 'quote-item', key: qi }, `"${q}"`))
)
])
]))
),
// 摘要
props.result.summary && h('div', { class: 'interview-summary' }, [
h('div', { class: 'summary-title' }, '📋 核心观点摘要'),
h('div', { class: 'summary-content' }, props.result.summary)
])
])
}
}
// QuickSearchResult 展示组件
const QuickSearchResult = {
props: ['result'],
setup(props) {
return () => h('div', { class: 'quick-search-result' }, [
h('div', { class: 'search-header' }, [
h('span', { class: 'search-query' }, props.result.query),
h('span', { class: 'search-count' }, `${props.result.count} 条结果`)
]),
props.result.facts.length > 0 && h('div', { class: 'search-facts' },
props.result.facts.map((fact, i) => h('div', { class: 'search-fact-item', key: i }, [
h('span', { class: 'fact-num' }, i + 1),
h('span', { class: 'fact-text' }, fact)
]))
)
])
}
}
// Computed
const statusClass = computed(() => {
if (isComplete.value) return 'completed'
if (agentLogs.value.length > 0) return 'processing'
return 'pending'
})
const statusText = computed(() => {
if (isComplete.value) return '已完成'
if (agentLogs.value.length > 0) return '生成中...'
return '等待中'
})
const totalSections = computed(() => {
return reportOutline.value?.sections?.length || 0
})
const completedSections = computed(() => {
return Object.keys(generatedSections.value).length
})
const totalToolCalls = computed(() => {
return agentLogs.value.filter(l => l.action === 'tool_call').length
})
const formatElapsedTime = computed(() => {
if (!startTime.value) return '0s'
const lastLog = agentLogs.value[agentLogs.value.length - 1]
const elapsed = lastLog?.elapsed_seconds || 0
if (elapsed < 60) return `${Math.round(elapsed)}s`
const mins = Math.floor(elapsed / 60)
const secs = Math.round(elapsed % 60)
return `${mins}m ${secs}s`
})
// 只显示最近的重要日志,避免列表过长
const displayLogs = computed(() => {
// 显示所有日志,但可以根据需要过滤
return agentLogs.value
})
// Methods
const addLog = (msg) => {
emit('add-log', msg)
}
const toggleSection = (idx) => {
if (!generatedSections.value[idx + 1]) return
const newSet = new Set(expandedSections.value)
if (newSet.has(idx)) {
newSet.delete(idx)
} else {
newSet.add(idx)
}
expandedSections.value = newSet
}
const isSectionCompleted = (sectionIndex) => {
return !!generatedSections.value[sectionIndex]
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
try {
return new Date(timestamp).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch {
return ''
}
}
const formatParams = (params) => {
if (!params) return ''
try {
return JSON.stringify(params, null, 2)
} catch {
return String(params)
}
}
const renderMarkdown = (content) => {
if (!content) return ''
// 简单的 markdown 渲染:转换换行为 <br>,处理标题
return content
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
.replace(/^# (.+)$/gm, '<h2>$1</h2>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>')
}
const getLogClass = (log) => {
return {
'is-tool': log.action === 'tool_call' || log.action === 'tool_result',
'is-section': log.action === 'section_start' || log.action === 'section_complete',
'is-complete': log.action === 'report_complete',
'is-planning': log.action === 'planning_start' || log.action === 'planning_complete'
}
}
const getMarkerClass = (log) => {
const classes = {
'report_start': 'marker-start',
'planning_start': 'marker-planning',
'planning_complete': 'marker-planning',
'section_start': 'marker-section',
'section_complete': 'marker-section-done',
'tool_call': 'marker-tool',
'tool_result': 'marker-tool-result',
'llm_response': 'marker-llm',
'report_complete': 'marker-complete'
}
return classes[log.action] || 'marker-default'
}
const getLogIcon = (log) => {
const icons = {
'report_start': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('circle', { cx: 12, cy: 12, r: 10 }),
h('polygon', { points: '10 8 16 12 10 16 10 8' })
]),
'planning_start': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('line', { x1: 8, y1: 6, x2: 21, y2: 6 }),
h('line', { x1: 8, y1: 12, x2: 21, y2: 12 }),
h('line', { x1: 8, y1: 18, x2: 21, y2: 18 }),
h('line', { x1: 3, y1: 6, x2: 3.01, y2: 6 }),
h('line', { x1: 3, y1: 12, x2: 3.01, y2: 12 }),
h('line', { x1: 3, y1: 18, x2: 3.01, y2: 18 })
]),
'planning_complete': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('polyline', { points: '20 6 9 17 4 12' })
]),
'section_start': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('path', { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }),
h('polyline', { points: '14 2 14 8 20 8' })
]),
'section_complete': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('path', { d: 'M22 11.08V12a10 10 0 1 1-5.93-9.14' }),
h('polyline', { points: '22 4 12 14.01 9 11.01' })
]),
'tool_call': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('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' })
]),
'tool_result': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('polyline', { points: '22 12 18 12 15 21 9 3 6 12 2 12' })
]),
'llm_response': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('path', { d: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z' })
]),
'report_complete': () => h('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
h('path', { d: 'M22 11.08V12a10 10 0 1 1-5.93-9.14' }),
h('polyline', { points: '22 4 12 14.01 9 11.01' })
])
}
return icons[log.action] || icons['report_start']
}
const getActionLabel = (action) => {
const labels = {
'report_start': '报告启动',
'planning_start': '开始规划',
'planning_complete': '规划完成',
'section_start': '章节开始',
'section_complete': '章节完成',
'tool_call': '工具调用',
'tool_result': '工具返回',
'llm_response': 'LLM 响应',
'report_complete': '报告完成'
}
return labels[action] || action
}
const getLogLevelClass = (log) => {
if (log.includes('ERROR') || log.includes('错误')) return 'error'
if (log.includes('WARNING') || log.includes('警告')) return 'warning'
if (log.includes('✓') || log.includes('完成')) return 'success'
return ''
}
// Polling
let agentLogTimer = null
let consoleLogTimer = null
const fetchAgentLog = async () => {
if (!props.reportId) return
try {
const res = await getAgentLog(props.reportId, agentLogLine.value)
if (res.success && res.data) {
const newLogs = res.data.logs || []
if (newLogs.length > 0) {
// 处理新日志
newLogs.forEach(log => {
agentLogs.value.push(log)
// 提取大纲
if (log.action === 'planning_complete' && log.details?.outline) {
reportOutline.value = log.details.outline
}
// 追踪当前章节
if (log.action === 'section_start') {
currentSectionIndex.value = log.section_index
}
// 记录已完成章节(简单标记,实际内容需要从其他地方获取)
if (log.action === 'section_complete') {
// 这里简单标记为完成,实际内容可能需要另外获取
if (!generatedSections.value[log.section_index]) {
generatedSections.value[log.section_index] = `## ${log.section_title}\n\n章节内容已生成。`
}
currentSectionIndex.value = null
}
// 检测报告完成
if (log.action === 'report_complete') {
isComplete.value = true
emit('update-status', 'completed')
stopPolling()
}
// 记录开始时间
if (log.action === 'report_start') {
startTime.value = new Date(log.timestamp)
}
})
agentLogLine.value = res.data.from_line + newLogs.length
// 滚动到底部
nextTick(() => {
if (mainContent.value) {
mainContent.value.scrollTop = mainContent.value.scrollHeight
}
})
}
}
} catch (err) {
console.warn('获取 Agent 日志失败:', err)
}
}
const fetchConsoleLog = async () => {
if (!props.reportId) return
try {
const res = await getConsoleLog(props.reportId, consoleLogLine.value)
if (res.success && res.data) {
const newLogs = res.data.logs || []
if (newLogs.length > 0) {
consoleLogs.value.push(...newLogs)
consoleLogLine.value = res.data.from_line + newLogs.length
// 滚动到底部
nextTick(() => {
if (logContent.value) {
logContent.value.scrollTop = logContent.value.scrollHeight
}
})
}
}
} catch (err) {
console.warn('获取控制台日志失败:', err)
}
}
const startPolling = () => {
if (agentLogTimer || consoleLogTimer) return
// 立即获取一次
fetchAgentLog()
fetchConsoleLog()
// 开始轮询
agentLogTimer = setInterval(fetchAgentLog, 2000)
consoleLogTimer = setInterval(fetchConsoleLog, 1500)
}
const stopPolling = () => {
if (agentLogTimer) {
clearInterval(agentLogTimer)
agentLogTimer = null
}
if (consoleLogTimer) {
clearInterval(consoleLogTimer)
consoleLogTimer = null
}
}
// Lifecycle
onMounted(() => {
if (props.reportId) {
addLog(`Report Agent 初始化: ${props.reportId}`)
startPolling()
}
})
onUnmounted(() => {
stopPolling()
})
// Watch for reportId changes
watch(() => props.reportId, (newId) => {
if (newId) {
// 重置状态
agentLogs.value = []
consoleLogs.value = []
agentLogLine.value = 0
consoleLogLine.value = 0
reportOutline.value = null
currentSectionIndex.value = null
generatedSections.value = {}
expandedSections.value = new Set()
isComplete.value = false
startTime.value = null
startPolling()
}
}, { immediate: true })
</script>
<style scoped>
.report-panel {
height: 100%;
display: flex;
flex-direction: column;
background: #FFFFFF;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
overflow: hidden;
}
/* Status Bar */
.status-bar {
background: #FFF;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #EAEAEA;
flex-shrink: 0;
}
.status-left {
display: flex;
align-items: center;
gap: 16px;
}
.report-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
color: #FFF;
}
.badge-icon {
font-size: 14px;
}
.badge-text {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #666;
}
.status-indicator .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #CCC;
}
.status-indicator.pending .dot { background: #999; }
.status-indicator.processing .dot { background: #FF9800; animation: pulse 1s infinite; }
.status-indicator.completed .dot { background: #4CAF50; }
@keyframes pulse { 50% { opacity: 0.5; } }
.stats-group {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.stat-label {
font-size: 10px;
color: #999;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
.mono {
font-family: 'JetBrains Mono', monospace;
}
/* Main Content */
.main-content-area {
flex: 1;
overflow-y: auto;
padding: 24px;
background: #FAFAFA;
}
/* Outline Card */
.outline-card {
background: #FFF;
border: 1px solid #E0E0E0;
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.outline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.outline-title-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.outline-icon {
color: #667eea;
}
.outline-title {
font-size: 18px;
font-weight: 700;
color: #1a1a1a;
margin: 0;
}
.outline-badge {
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
background: #E8F5E9;
color: #2E7D32;
border-radius: 12px;
}
.outline-summary {
font-size: 14px;
color: #666;
line-height: 1.6;
margin: 0 0 16px;
padding-bottom: 16px;
border-bottom: 1px solid #F0F0F0;
}
.outline-sections {
display: flex;
flex-direction: column;
gap: 8px;
}
.outline-section-item {
background: #FAFAFA;
border: 1px solid #EAEAEA;
border-radius: 8px;
transition: all 0.2s;
cursor: pointer;
}
.outline-section-item:hover {
background: #F5F5F5;
border-color: #DDD;
}
.outline-section-item.current {
border-color: #FF9800;
background: #FFF8E1;
}
.outline-section-item.completed {
border-color: #4CAF50;
background: #F1F8E9;
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.section-num {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
font-weight: 600;
color: #999;
min-width: 24px;
}
.section-title {
flex: 1;
font-size: 14px;
font-weight: 500;
color: #333;
}
.section-status {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.section-status svg {
color: #4CAF50;
}
.generating-dot {
width: 8px;
height: 8px;
background: #FF9800;
border-radius: 50%;
animation: pulse 1s infinite;
}
.section-toggle {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: #E0E0E0;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
color: #666;
}
.section-content-preview {
padding: 0 16px 16px;
border-top: 1px solid #EAEAEA;
margin-top: 8px;
}
.content-markdown {
font-size: 13px;
line-height: 1.7;
color: #444;
}
.content-markdown :deep(h1),
.content-markdown :deep(h2),
.content-markdown :deep(h3) {
margin-top: 16px;
margin-bottom: 8px;
color: #1a1a1a;
}
.content-markdown :deep(p) {
margin-bottom: 12px;
}
.content-markdown :deep(ul),
.content-markdown :deep(ol) {
padding-left: 20px;
margin-bottom: 12px;
}
/* Action Feed */
.action-feed {
background: #FFF;
border: 1px solid #E0E0E0;
border-radius: 12px;
overflow: hidden;
}
.feed-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 20px;
background: #FAFAFA;
border-bottom: 1px solid #EAEAEA;
}
.feed-title {
font-size: 13px;
font-weight: 600;
color: #333;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.feed-count {
font-size: 12px;
color: #999;
font-family: 'JetBrains Mono', monospace;
}
.feed-timeline {
padding: 16px 20px;
}
.feed-item {
display: flex;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid #F5F5F5;
transition: background 0.2s;
}
.feed-item:last-child {
border-bottom: none;
}
.feed-item:hover {
background: #FAFAFA;
margin: 0 -20px;
padding: 12px 20px;
}
.item-marker {
flex-shrink: 0;
}
.marker-icon {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #F0F0F0;
color: #666;
}
.marker-start { background: #E3F2FD; color: #1976D2; }
.marker-planning { background: #FFF3E0; color: #F57C00; }
.marker-section { background: #E8F5E9; color: #388E3C; }
.marker-section-done { background: #C8E6C9; color: #2E7D32; }
.marker-tool { background: #F3E5F5; color: #7B1FA2; }
.marker-tool-result { background: #FCE4EC; color: #C2185B; }
.marker-llm { background: #E0F7FA; color: #00838F; }
.marker-complete { background: #C8E6C9; color: #1B5E20; }
.item-content {
flex: 1;
min-width: 0;
}
.item-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.item-action {
font-size: 13px;
font-weight: 600;
color: #333;
}
.item-stage {
font-size: 10px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
background: #F0F0F0;
color: #666;
text-transform: uppercase;
}
.item-stage.pending { background: #FFF3E0; color: #E65100; }
.item-stage.planning { background: #E3F2FD; color: #1565C0; }
.item-stage.generating { background: #F3E5F5; color: #7B1FA2; }
.item-stage.completed { background: #E8F5E9; color: #2E7D32; }
.item-time {
font-size: 11px;
color: #999;
font-family: 'JetBrains Mono', monospace;
margin-left: auto;
}
.item-body {
font-size: 13px;
color: #555;
line-height: 1.5;
}
.item-footer {
margin-top: 8px;
}
.elapsed {
font-size: 11px;
color: #999;
font-family: 'JetBrains Mono', monospace;
}
/* Item Body Blocks */
.info-block {
display: flex;
gap: 8px;
margin-bottom: 6px;
}
.info-label {
color: #888;
font-size: 12px;
}
.info-value {
color: #333;
}
.message-text {
color: #555;
}
.message-text.success {
color: #2E7D32;
font-weight: 500;
}
.outline-mini {
margin-top: 8px;
padding: 8px 12px;
background: #F5F5F5;
border-radius: 6px;
}
.mini-label {
font-size: 12px;
color: #666;
}
.section-info {
display: flex;
align-items: center;
gap: 10px;
}
.section-badge {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
background: #667eea;
color: #FFF;
border-radius: 4px;
}
.section-name {
font-weight: 500;
color: #333;
}
.tool-call-block {
background: #F8F8F8;
border: 1px solid #EAEAEA;
border-radius: 6px;
overflow: hidden;
}
.tool-name {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: #F3E5F5;
font-weight: 600;
font-size: 12px;
color: #7B1FA2;
}
.tool-icon {
flex-shrink: 0;
}
.tool-params {
padding: 10px 12px;
font-size: 11px;
}
.tool-params pre {
margin: 0;
font-family: 'JetBrains Mono', monospace;
white-space: pre-wrap;
word-break: break-all;
color: #555;
}
.tool-result-block {
background: #FFF8E1;
border: 1px solid #FFE082;
border-radius: 6px;
padding: 10px 12px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.result-tool {
font-weight: 600;
font-size: 12px;
color: #F57C00;
}
.result-length {
font-size: 10px;
color: #999;
font-family: 'JetBrains Mono', monospace;
}
.result-content {
font-size: 12px;
color: #555;
line-height: 1.6;
}
.result-content pre {
margin: 0;
font-family: 'JetBrains Mono', monospace;
white-space: pre-wrap;
word-break: break-word;
background: rgba(0, 0, 0, 0.03);
padding: 12px;
border-radius: 4px;
}
.llm-response-block {
background: #E0F7FA;
border: 1px solid #B2EBF2;
border-radius: 6px;
padding: 10px 12px;
}
.response-meta {
display: flex;
gap: 12px;
margin-bottom: 8px;
}
.meta-item {
font-size: 11px;
color: #666;
}
.meta-item.active {
color: #00838F;
font-weight: 600;
}
.response-content {
font-size: 12px;
color: #444;
line-height: 1.6;
}
.response-content pre {
margin: 0;
font-family: 'JetBrains Mono', monospace;
white-space: pre-wrap;
word-break: break-word;
background: rgba(0, 0, 0, 0.03);
padding: 12px;
border-radius: 4px;
}
.complete-info {
display: flex;
align-items: center;
gap: 8px;
color: #2E7D32;
font-weight: 500;
}
.complete-info.success {
font-size: 15px;
}
.complete-icon {
flex-shrink: 0;
}
/* Waiting State */
.waiting-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 48px;
color: #999;
font-size: 13px;
}
.pulse-ring {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid #EAEAEA;
animation: ripple 2s infinite;
}
@keyframes ripple {
0% { transform: scale(0.8); opacity: 1; border-color: #CCC; }
100% { transform: scale(2.5); opacity: 0; border-color: #EAEAEA; }
}
/* Animation */
.feed-item-enter-active,
.feed-item-leave-active {
transition: all 0.3s ease;
}
.feed-item-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.feed-item-leave-to {
opacity: 0;
}
/* Console Logs */
.console-logs {
background: #1a1a1a;
color: #DDD;
padding: 16px;
font-family: 'JetBrains Mono', monospace;
border-top: 1px solid #333;
flex-shrink: 0;
}
.log-header {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #333;
padding-bottom: 8px;
margin-bottom: 8px;
font-size: 10px;
color: #666;
}
.log-title {
text-transform: uppercase;
letter-spacing: 0.1em;
}
.log-content {
display: flex;
flex-direction: column;
gap: 3px;
max-height: 100px;
overflow-y: auto;
padding-right: 4px;
}
.log-content::-webkit-scrollbar { width: 4px; }
.log-content::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
.log-line {
font-size: 11px;
line-height: 1.5;
}
.log-msg {
color: #AAA;
word-break: break-all;
}
.log-msg.error { color: #EF5350; }
.log-msg.warning { color: #FFA726; }
.log-msg.success { color: #66BB6A; }
/* ========== 工具结果结构化展示样式 ========== */
/* 切换原文按钮 */
.toggle-raw-btn {
background: transparent;
border: 1px solid #DDD;
border-radius: 4px;
padding: 2px 8px;
font-size: 10px;
color: #666;
cursor: pointer;
transition: all 0.2s;
}
.toggle-raw-btn:hover {
background: #F5F5F5;
border-color: #CCC;
}
/* 原文展示 */
.result-raw {
margin-top: 10px;
}
.result-raw pre {
margin: 0;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
white-space: pre-wrap;
word-break: break-word;
background: rgba(0, 0, 0, 0.03);
padding: 12px;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
}
/* 工具类型特定背景 */
.tool-result-block.tool-insight_forge {
background: linear-gradient(135deg, #FFF8E1 0%, #FFF3E0 100%);
border-color: #FFE082;
}
.tool-result-block.tool-panorama_search {
background: linear-gradient(135deg, #E3F2FD 0%, #E1F5FE 100%);
border-color: #90CAF9;
}
.tool-result-block.tool-interview_agents {
background: linear-gradient(135deg, #F3E5F5 0%, #FCE4EC 100%);
border-color: #CE93D8;
}
.tool-result-block.tool-quick_search {
background: linear-gradient(135deg, #E8F5E9 0%, #F1F8E9 100%);
border-color: #A5D6A7;
}
/* 统计卡片 */
.stats-cards {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.stat-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 12px;
background: #FFF;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
min-width: 60px;
}
.stat-card .stat-num {
font-size: 18px;
font-weight: 700;
color: #333;
font-family: 'JetBrains Mono', monospace;
}
.stat-card .stat-name {
font-size: 10px;
color: #888;
margin-top: 2px;
}
.stat-card.facts .stat-num { color: #E65100; }
.stat-card.entities .stat-num { color: #7B1FA2; }
.stat-card.relations .stat-num { color: #1565C0; }
.stat-card.nodes .stat-num { color: #1976D2; }
.stat-card.edges .stat-num { color: #00838F; }
.stat-card.active .stat-num { color: #2E7D32; }
.stat-card.history .stat-num { color: #795548; }
/* 可折叠区块 */
.collapsible-section {
margin-top: 10px;
background: rgba(255,255,255,0.7);
border-radius: 6px;
overflow: hidden;
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(0,0,0,0.03);
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: #555;
transition: background 0.2s;
}
.section-title:hover {
background: rgba(0,0,0,0.06);
}
.section-title.active { color: #2E7D32; }
.section-title.history { color: #795548; }
.toggle-icon {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.08);
border-radius: 4px;
font-size: 14px;
}
/* 子问题列表 */
.sub-queries {
padding: 8px 12px;
}
.sub-query {
display: flex;
gap: 8px;
padding: 6px 0;
border-bottom: 1px dashed #EEE;
}
.sub-query:last-child {
border-bottom: none;
}
.query-num {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: #E3F2FD;
color: #1565C0;
border-radius: 50%;
font-size: 11px;
font-weight: 600;
flex-shrink: 0;
}
.query-text {
font-size: 12px;
color: #444;
line-height: 1.5;
}
/* 事实列表 */
.facts-list {
padding: 8px 12px;
max-height: 300px;
overflow-y: auto;
}
.fact-item {
display: flex;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid #F0F0F0;
}
.fact-item:last-child {
border-bottom: none;
}
.fact-item.active {
background: rgba(46, 125, 50, 0.05);
margin: 0 -12px;
padding: 6px 12px;
}
.fact-item.history {
background: rgba(121, 85, 72, 0.05);
margin: 0 -12px;
padding: 6px 12px;
}
.fact-num {
min-width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: #F5F5F5;
color: #888;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
flex-shrink: 0;
}
.fact-text {
font-size: 12px;
color: #444;
line-height: 1.5;
}
.fact-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.time-range {
font-size: 10px;
color: #888;
font-family: 'JetBrains Mono', monospace;
}
/* 实体网格 */
.entities-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
padding: 8px 12px;
}
.entity-card {
background: #FFF;
border: 1px solid #EEE;
border-radius: 6px;
padding: 8px 10px;
}
.entity-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.entity-name {
font-size: 12px;
font-weight: 600;
color: #333;
}
.entity-type {
font-size: 10px;
color: #7B1FA2;
background: #F3E5F5;
padding: 2px 6px;
border-radius: 10px;
}
.entity-summary {
font-size: 11px;
color: #666;
line-height: 1.4;
}
/* 实体标签 */
.entity-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 12px;
}
.entity-tag {
display: flex;
align-items: center;
gap: 4px;
background: #FFF;
border: 1px solid #EEE;
border-radius: 15px;
padding: 4px 10px;
}
.tag-name {
font-size: 11px;
font-weight: 500;
color: #333;
}
.tag-type {
font-size: 9px;
color: #888;
}
/* 关系链 */
.relations-list {
padding: 8px 12px;
}
.relation-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 0;
border-bottom: 1px solid #F0F0F0;
flex-wrap: wrap;
}
.relation-item:last-child {
border-bottom: none;
}
.rel-source, .rel-target {
font-size: 11px;
font-weight: 500;
color: #333;
background: #E3F2FD;
padding: 2px 8px;
border-radius: 4px;
}
.rel-arrow {
font-size: 12px;
color: #999;
}
.rel-type {
font-size: 10px;
color: #FFF;
background: #1565C0;
padding: 2px 8px;
border-radius: 10px;
}
/* 采访结果 */
.interview-result {
padding: 8px 0;
}
.interview-header {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
background: rgba(255,255,255,0.7);
border-radius: 6px;
margin-bottom: 10px;
}
.interview-topic {
font-size: 13px;
font-weight: 600;
color: #333;
}
.interview-count {
font-size: 11px;
color: #7B1FA2;
}
.interviews-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.interview-card {
background: #FFF;
border: 1px solid #EEE;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.interview-card:hover {
border-color: #DDD;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.interview-card.expanded {
border-color: #CE93D8;
}
.interview-card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
}
.interview-num {
font-size: 11px;
font-weight: 600;
color: #7B1FA2;
background: #F3E5F5;
padding: 2px 6px;
border-radius: 4px;
}
.interview-name {
font-size: 12px;
font-weight: 600;
color: #333;
}
.interview-role {
font-size: 10px;
color: #888;
background: #F5F5F5;
padding: 2px 8px;
border-radius: 10px;
}
.expand-icon {
margin-left: auto;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: #F5F5F5;
border-radius: 4px;
font-size: 14px;
color: #666;
}
.interview-card-body {
padding: 0 12px 12px;
border-top: 1px solid #F0F0F0;
}
.interview-bio {
font-size: 11px;
color: #888;
font-style: italic;
padding: 8px 0;
}
.interview-qa {
display: flex;
flex-direction: column;
gap: 8px;
}
.qa-question, .qa-answer {
font-size: 11px;
line-height: 1.5;
}
.qa-label {
font-weight: 600;
color: #7B1FA2;
margin-right: 4px;
}
.qa-question {
color: #555;
background: #FAFAFA;
padding: 6px 8px;
border-radius: 4px;
}
.qa-answer {
color: #333;
}
.interview-quotes {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #EEE;
}
.quote-item {
font-size: 11px;
color: #666;
font-style: italic;
padding: 4px 0 4px 12px;
border-left: 2px solid #CE93D8;
margin-bottom: 4px;
}
.interview-summary {
margin-top: 10px;
background: rgba(255,255,255,0.7);
border-radius: 6px;
padding: 10px 12px;
}
.summary-title {
font-size: 12px;
font-weight: 600;
color: #555;
margin-bottom: 6px;
}
.summary-content {
font-size: 11px;
color: #666;
line-height: 1.5;
}
/* 快速搜索结果 */
.quick-search-result {
padding: 8px 0;
}
.search-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255,255,255,0.7);
border-radius: 6px;
margin-bottom: 10px;
}
.search-query {
font-size: 12px;
font-weight: 600;
color: #333;
}
.search-count {
font-size: 11px;
color: #2E7D32;
background: #E8F5E9;
padding: 2px 8px;
border-radius: 10px;
}
.search-facts {
padding: 0 12px;
}
.search-fact-item {
display: flex;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid #F0F0F0;
}
.search-fact-item:last-child {
border-bottom: none;
}
</style>