5148 lines
141 KiB
Vue
5148 lines
141 KiB
Vue
<template>
|
||
<div class="report-panel">
|
||
<!-- Main Split Layout -->
|
||
<div class="main-split-layout">
|
||
<!-- LEFT PANEL: Report Style -->
|
||
<div class="left-panel report-style" ref="leftPanel">
|
||
<div v-if="reportOutline" class="report-content-wrapper">
|
||
<!-- Report Header -->
|
||
<div class="report-header-block">
|
||
<div class="report-meta">
|
||
<span class="report-tag">Prediction Report</span>
|
||
<span class="report-id">ID: {{ reportId || 'REF-2024-X92' }}</span>
|
||
</div>
|
||
<h1 class="main-title">{{ reportOutline.title }}</h1>
|
||
<p class="sub-title">{{ reportOutline.summary }}</p>
|
||
<div class="header-divider"></div>
|
||
</div>
|
||
|
||
<!-- Sections List -->
|
||
<div class="sections-list">
|
||
<div
|
||
v-for="(section, idx) in reportOutline.sections"
|
||
:key="idx"
|
||
class="report-section-item"
|
||
:class="{
|
||
'is-active': currentSectionIndex === idx + 1,
|
||
'is-completed': isSectionCompleted(idx + 1),
|
||
'is-pending': !isSectionCompleted(idx + 1) && currentSectionIndex !== idx + 1
|
||
}"
|
||
>
|
||
<div class="section-header-row" @click="toggleSectionCollapse(idx)" :class="{ 'clickable': isSectionCompleted(idx + 1) }">
|
||
<span class="section-number">{{ String(idx + 1).padStart(2, '0') }}</span>
|
||
<h3 class="section-title">{{ section.title }}</h3>
|
||
<svg
|
||
v-if="isSectionCompleted(idx + 1)"
|
||
class="collapse-icon"
|
||
:class="{ 'is-collapsed': collapsedSections.has(idx) }"
|
||
viewBox="0 0 24 24"
|
||
width="20"
|
||
height="20"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<polyline points="6 9 12 15 18 9"></polyline>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="section-body" v-show="!collapsedSections.has(idx)">
|
||
<!-- Completed Content -->
|
||
<div v-if="generatedSections[idx + 1]" class="generated-content" v-html="renderMarkdown(generatedSections[idx + 1])"></div>
|
||
|
||
<!-- Loading State -->
|
||
<div v-else-if="currentSectionIndex === idx + 1" class="loading-state">
|
||
<div class="loading-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||
<circle cx="12" cy="12" r="10" stroke-width="4" stroke="#E5E7EB"></circle>
|
||
<path d="M12 2a10 10 0 0 1 10 10" stroke-width="4" stroke="#4B5563" stroke-linecap="round"></path>
|
||
</svg>
|
||
</div>
|
||
<span class="loading-text">正在生成{{ section.title }}...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Waiting State -->
|
||
<div v-if="!reportOutline" class="waiting-placeholder">
|
||
<div class="waiting-animation">
|
||
<div class="waiting-ring"></div>
|
||
<div class="waiting-ring"></div>
|
||
<div class="waiting-ring"></div>
|
||
</div>
|
||
<span class="waiting-text">Waiting for Report Agent...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT PANEL: Workflow Timeline -->
|
||
<div class="right-panel" ref="rightPanel">
|
||
<div class="panel-header" :class="`panel-header--${activeStep.status}`" v-if="!isComplete">
|
||
<span class="header-dot" v-if="activeStep.status === 'active'"></span>
|
||
<span class="header-index mono">{{ activeStep.noLabel }}</span>
|
||
<span class="header-title">{{ activeStep.title }}</span>
|
||
<span class="header-meta mono" v-if="activeStep.meta">{{ activeStep.meta }}</span>
|
||
</div>
|
||
|
||
<!-- Workflow Overview (flat, status-based palette) -->
|
||
<div class="workflow-overview" v-if="agentLogs.length > 0 || reportOutline">
|
||
<div class="workflow-metrics">
|
||
<div class="metric">
|
||
<span class="metric-label">Sections</span>
|
||
<span class="metric-value mono">{{ completedSections }}/{{ totalSections }}</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">Elapsed</span>
|
||
<span class="metric-value mono">{{ formatElapsedTime }}</span>
|
||
</div>
|
||
<div class="metric">
|
||
<span class="metric-label">Tools</span>
|
||
<span class="metric-value mono">{{ totalToolCalls }}</span>
|
||
</div>
|
||
<div class="metric metric-right">
|
||
<span class="metric-pill" :class="`pill--${statusClass}`">{{ statusText }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="workflow-steps" v-if="workflowSteps.length > 0">
|
||
<div
|
||
v-for="(step, sidx) in workflowSteps"
|
||
:key="step.key"
|
||
class="wf-step"
|
||
:class="`wf-step--${step.status}`"
|
||
>
|
||
<div class="wf-step-connector">
|
||
<div class="wf-step-dot"></div>
|
||
<div class="wf-step-line" v-if="sidx < workflowSteps.length - 1"></div>
|
||
</div>
|
||
|
||
<div class="wf-step-content">
|
||
<div class="wf-step-title-row">
|
||
<span class="wf-step-index mono">{{ step.noLabel }}</span>
|
||
<span class="wf-step-title">{{ step.title }}</span>
|
||
<span class="wf-step-meta mono" v-if="step.meta">{{ step.meta }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Next Step Button - 在完成后显示 -->
|
||
<button v-if="isComplete" class="next-step-btn" @click="goToInteraction">
|
||
<span>进入深度互动</span>
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||
<polyline points="12 5 19 12 12 19"></polyline>
|
||
</svg>
|
||
</button>
|
||
|
||
<div class="workflow-divider"></div>
|
||
</div>
|
||
|
||
<div class="workflow-timeline">
|
||
<TransitionGroup name="timeline-item">
|
||
<div
|
||
v-for="(log, idx) in displayLogs"
|
||
:key="log.timestamp + '-' + idx"
|
||
class="timeline-item"
|
||
:class="getTimelineItemClass(log, idx, displayLogs.length)"
|
||
>
|
||
<!-- Timeline Connector -->
|
||
<div class="timeline-connector">
|
||
<div class="connector-dot" :class="getConnectorClass(log, idx, displayLogs.length)"></div>
|
||
<div class="connector-line" v-if="idx < displayLogs.length - 1"></div>
|
||
</div>
|
||
|
||
<!-- Timeline Content -->
|
||
<div class="timeline-content">
|
||
<div class="timeline-header">
|
||
<span class="action-label">{{ getActionLabel(log.action) }}</span>
|
||
<span class="action-time">{{ formatTime(log.timestamp) }}</span>
|
||
</div>
|
||
|
||
<!-- Action Body - Different for each type -->
|
||
<div class="timeline-body" :class="{ 'collapsed': isLogCollapsed(log) }" @click="toggleLogExpand(log)">
|
||
|
||
<!-- Report Start -->
|
||
<template v-if="log.action === 'report_start'">
|
||
<div class="info-row">
|
||
<span class="info-key">Simulation</span>
|
||
<span class="info-val mono">{{ log.details?.simulation_id }}</span>
|
||
</div>
|
||
<div class="info-row" v-if="log.details?.simulation_requirement">
|
||
<span class="info-key">Requirement</span>
|
||
<span class="info-val">{{ log.details.simulation_requirement }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Planning -->
|
||
<template v-if="log.action === 'planning_start'">
|
||
<div class="status-message planning">{{ log.details?.message }}</div>
|
||
</template>
|
||
<template v-if="log.action === 'planning_complete'">
|
||
<div class="status-message success">{{ log.details?.message }}</div>
|
||
<div class="outline-badge" v-if="log.details?.outline">
|
||
{{ log.details.outline.sections?.length || 0 }} sections planned
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Section Start -->
|
||
<template v-if="log.action === 'section_start'">
|
||
<div class="section-tag">
|
||
<span class="tag-num">#{{ log.section_index }}</span>
|
||
<span class="tag-title">{{ log.section_title }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Section Content Generated (内容生成完成,但整个章节可能还没完成) -->
|
||
<template v-if="log.action === 'section_content'">
|
||
<div class="section-tag content-ready">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 20h9"></path>
|
||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||
</svg>
|
||
<span class="tag-title">{{ log.section_title }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Section Complete (章节生成完成) -->
|
||
<template v-if="log.action === 'section_complete'">
|
||
<div class="section-tag completed">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="20 6 9 17 4 12"></polyline>
|
||
</svg>
|
||
<span class="tag-title">{{ log.section_title }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Tool Call -->
|
||
<template v-if="log.action === 'tool_call'">
|
||
<div class="tool-badge" :class="'tool-' + getToolColor(log.details?.tool_name)">
|
||
<!-- Deep Insight - Lightbulb -->
|
||
<svg v-if="getToolIcon(log.details?.tool_name) === 'lightbulb'" class="tool-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.5V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.5A7 7 0 0 0 12 2z"></path>
|
||
</svg>
|
||
<!-- Panorama Search - Globe -->
|
||
<svg v-else-if="getToolIcon(log.details?.tool_name) === 'globe'" class="tool-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10"></circle>
|
||
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
||
</svg>
|
||
<!-- Agent Interview - Users -->
|
||
<svg v-else-if="getToolIcon(log.details?.tool_name) === 'users'" class="tool-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||
<circle cx="9" cy="7" r="4"></circle>
|
||
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||
</svg>
|
||
<!-- Quick Search - Zap -->
|
||
<svg v-else-if="getToolIcon(log.details?.tool_name) === 'zap'" class="tool-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
|
||
</svg>
|
||
<!-- Graph Stats - Chart -->
|
||
<svg v-else-if="getToolIcon(log.details?.tool_name) === 'chart'" class="tool-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||
</svg>
|
||
<!-- Entity Query - Database -->
|
||
<svg v-else-if="getToolIcon(log.details?.tool_name) === 'database'" class="tool-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||
</svg>
|
||
<!-- Default - Tool -->
|
||
<svg v-else 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>
|
||
{{ getToolDisplayName(log.details?.tool_name) }}
|
||
</div>
|
||
<div v-if="log.details?.parameters && expandedLogs.has(log.timestamp)" class="tool-params">
|
||
<pre>{{ formatParams(log.details.parameters) }}</pre>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Tool Result -->
|
||
<template v-if="log.action === 'tool_result'">
|
||
<div class="result-wrapper" :class="'result-' + log.details?.tool_name">
|
||
<!-- Hide result-meta for tools that show stats in their own header -->
|
||
<div v-if="!['interview_agents', 'insight_forge', 'panorama_search', 'quick_search'].includes(log.details?.tool_name)" class="result-meta">
|
||
<span class="result-tool">{{ getToolDisplayName(log.details?.tool_name) }}</span>
|
||
<span class="result-size">{{ formatResultSize(log.details?.result_length) }}</span>
|
||
</div>
|
||
|
||
<!-- Structured Result Display -->
|
||
<div v-if="!showRawResult[log.timestamp]" class="result-structured">
|
||
<!-- Interview Agents - Special Display -->
|
||
<template v-if="log.details?.tool_name === 'interview_agents'">
|
||
<InterviewDisplay :result="parseInterview(log.details.result)" :result-length="log.details?.result_length" />
|
||
</template>
|
||
|
||
<!-- Insight Forge -->
|
||
<template v-else-if="log.details?.tool_name === 'insight_forge'">
|
||
<InsightDisplay :result="parseInsightForge(log.details.result)" :result-length="log.details?.result_length" />
|
||
</template>
|
||
|
||
<!-- Panorama Search -->
|
||
<template v-else-if="log.details?.tool_name === 'panorama_search'">
|
||
<PanoramaDisplay :result="parsePanorama(log.details.result)" :result-length="log.details?.result_length" />
|
||
</template>
|
||
|
||
<!-- Quick Search -->
|
||
<template v-else-if="log.details?.tool_name === 'quick_search'">
|
||
<QuickSearchDisplay :result="parseQuickSearch(log.details.result)" :result-length="log.details?.result_length" />
|
||
</template>
|
||
|
||
<!-- Default -->
|
||
<template v-else>
|
||
<pre class="raw-preview">{{ truncateText(log.details?.result, 300) }}</pre>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Raw Result -->
|
||
<div v-else class="result-raw">
|
||
<pre>{{ log.details?.result }}</pre>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- LLM Response -->
|
||
<template v-if="log.action === 'llm_response'">
|
||
<div class="llm-meta">
|
||
<span class="meta-tag">Iteration {{ log.details?.iteration }}</span>
|
||
<span class="meta-tag" :class="{ active: log.details?.has_tool_calls }">
|
||
Tools: {{ log.details?.has_tool_calls ? 'Yes' : 'No' }}
|
||
</span>
|
||
<span class="meta-tag" :class="{ active: log.details?.has_final_answer, 'final-answer': log.details?.has_final_answer }">
|
||
Final: {{ log.details?.has_final_answer ? 'Yes' : 'No' }}
|
||
</span>
|
||
</div>
|
||
<!-- 当是最终答案时,显示特殊提示 -->
|
||
<div v-if="log.details?.has_final_answer" class="final-answer-hint">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="20 6 9 17 4 12"></polyline>
|
||
</svg>
|
||
<span>Section "{{ log.section_title }}" content generated</span>
|
||
</div>
|
||
<div v-if="expandedLogs.has(log.timestamp) && log.details?.response" class="llm-content">
|
||
<pre>{{ log.details.response }}</pre>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Report Complete -->
|
||
<template v-if="log.action === 'report_complete'">
|
||
<div class="complete-banner">
|
||
<svg viewBox="0 0 24 24" width="20" height="20" 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>Report Generation Complete</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Footer: Elapsed Time + Action Buttons -->
|
||
<div class="timeline-footer" v-if="log.elapsed_seconds || (log.action === 'tool_call' && log.details?.parameters) || log.action === 'tool_result' || (log.action === 'llm_response' && log.details?.response)">
|
||
<span v-if="log.elapsed_seconds" class="elapsed-badge">+{{ log.elapsed_seconds.toFixed(1) }}s</span>
|
||
<span v-else class="elapsed-placeholder"></span>
|
||
|
||
<div class="footer-actions">
|
||
<!-- Tool Call: Show/Hide Params -->
|
||
<button v-if="log.action === 'tool_call' && log.details?.parameters" class="action-btn" @click.stop="toggleLogExpand(log)">
|
||
{{ expandedLogs.has(log.timestamp) ? 'Hide Params' : 'Show Params' }}
|
||
</button>
|
||
|
||
<!-- Tool Result: Raw/Structured View -->
|
||
<button v-if="log.action === 'tool_result'" class="action-btn" @click.stop="toggleRawResult(log.timestamp, $event)">
|
||
{{ showRawResult[log.timestamp] ? 'Structured View' : 'Raw Output' }}
|
||
</button>
|
||
|
||
<!-- LLM Response: Show/Hide Response -->
|
||
<button v-if="log.action === 'llm_response' && log.details?.response" class="action-btn" @click.stop="toggleLogExpand(log)">
|
||
{{ expandedLogs.has(log.timestamp) ? 'Hide Response' : 'Show Response' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</TransitionGroup>
|
||
|
||
<!-- Empty State -->
|
||
<div v-if="agentLogs.length === 0 && !isComplete" class="workflow-empty">
|
||
<div class="empty-pulse"></div>
|
||
<span>Waiting for agent activity...</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 { useRouter } from 'vue-router'
|
||
import { getAgentLog, getConsoleLog } from '../api/report'
|
||
|
||
const router = useRouter()
|
||
|
||
const props = defineProps({
|
||
reportId: String,
|
||
simulationId: String,
|
||
systemLogs: Array
|
||
})
|
||
|
||
const emit = defineEmits(['add-log', 'update-status'])
|
||
|
||
// Navigation
|
||
const goToInteraction = () => {
|
||
if (props.reportId) {
|
||
router.push({ name: 'Interaction', params: { reportId: props.reportId } })
|
||
}
|
||
}
|
||
|
||
// 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({})
|
||
const expandedContent = ref(new Set())
|
||
const expandedLogs = ref(new Set())
|
||
const collapsedSections = ref(new Set())
|
||
const isComplete = ref(false)
|
||
const startTime = ref(null)
|
||
const leftPanel = ref(null)
|
||
const rightPanel = ref(null)
|
||
const logContent = ref(null)
|
||
const showRawResult = reactive({})
|
||
|
||
// Toggle functions
|
||
const toggleRawResult = (timestamp, event) => {
|
||
// 保存按钮相对于视口的位置
|
||
const button = event?.target
|
||
const buttonRect = button?.getBoundingClientRect()
|
||
const buttonTopBeforeToggle = buttonRect?.top
|
||
|
||
// 切换状态
|
||
showRawResult[timestamp] = !showRawResult[timestamp]
|
||
|
||
// 等待 DOM 更新后,调整滚动位置以保持按钮在相同位置
|
||
if (button && buttonTopBeforeToggle !== undefined && rightPanel.value) {
|
||
nextTick(() => {
|
||
const newButtonRect = button.getBoundingClientRect()
|
||
const buttonTopAfterToggle = newButtonRect.top
|
||
const scrollDelta = buttonTopAfterToggle - buttonTopBeforeToggle
|
||
|
||
// 调整滚动位置
|
||
rightPanel.value.scrollTop += scrollDelta
|
||
})
|
||
}
|
||
}
|
||
|
||
const toggleSectionContent = (idx) => {
|
||
if (!generatedSections.value[idx + 1]) return
|
||
const newSet = new Set(expandedContent.value)
|
||
if (newSet.has(idx)) {
|
||
newSet.delete(idx)
|
||
} else {
|
||
newSet.add(idx)
|
||
}
|
||
expandedContent.value = newSet
|
||
}
|
||
|
||
const toggleSectionCollapse = (idx) => {
|
||
// 只有已完成的章节才能折叠
|
||
if (!generatedSections.value[idx + 1]) return
|
||
const newSet = new Set(collapsedSections.value)
|
||
if (newSet.has(idx)) {
|
||
newSet.delete(idx)
|
||
} else {
|
||
newSet.add(idx)
|
||
}
|
||
collapsedSections.value = newSet
|
||
}
|
||
|
||
const toggleLogExpand = (log) => {
|
||
const newSet = new Set(expandedLogs.value)
|
||
if (newSet.has(log.timestamp)) {
|
||
newSet.delete(log.timestamp)
|
||
} else {
|
||
newSet.add(log.timestamp)
|
||
}
|
||
expandedLogs.value = newSet
|
||
}
|
||
|
||
const isLogCollapsed = (log) => {
|
||
if (['tool_call', 'tool_result', 'llm_response'].includes(log.action)) {
|
||
return !expandedLogs.value.has(log.timestamp)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Tool configurations with display names and colors
|
||
const toolConfig = {
|
||
'insight_forge': {
|
||
name: 'Deep Insight',
|
||
color: 'purple',
|
||
icon: 'lightbulb' // 灯泡图标 - 代表洞察
|
||
},
|
||
'panorama_search': {
|
||
name: 'Panorama Search',
|
||
color: 'blue',
|
||
icon: 'globe' // 地球图标 - 代表全景搜索
|
||
},
|
||
'interview_agents': {
|
||
name: 'Agent Interview',
|
||
color: 'green',
|
||
icon: 'users' // 用户图标 - 代表对话
|
||
},
|
||
'quick_search': {
|
||
name: 'Quick Search',
|
||
color: 'orange',
|
||
icon: 'zap' // 闪电图标 - 代表快速
|
||
},
|
||
'get_graph_statistics': {
|
||
name: 'Graph Stats',
|
||
color: 'cyan',
|
||
icon: 'chart' // 图表图标 - 代表统计
|
||
},
|
||
'get_entities_by_type': {
|
||
name: 'Entity Query',
|
||
color: 'pink',
|
||
icon: 'database' // 数据库图标 - 代表实体
|
||
}
|
||
}
|
||
|
||
const getToolDisplayName = (toolName) => {
|
||
return toolConfig[toolName]?.name || toolName
|
||
}
|
||
|
||
const getToolColor = (toolName) => {
|
||
return toolConfig[toolName]?.color || 'gray'
|
||
}
|
||
|
||
const getToolIcon = (toolName) => {
|
||
return toolConfig[toolName]?.icon || 'tool'
|
||
}
|
||
|
||
// Parse functions
|
||
const parseInsightForge = (text) => {
|
||
const result = {
|
||
query: '',
|
||
simulationRequirement: '',
|
||
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.simulationRequirement = reqMatch[1].trim()
|
||
|
||
// 提取统计数据 - 匹配"相关预测事实: X条"格式
|
||
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###|$)/)
|
||
if (subQSection) {
|
||
const lines = subQSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||
result.subQueries = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).filter(Boolean)
|
||
}
|
||
|
||
// 提取关键事实 - 完整提取,不限制数量
|
||
const factsSection = text.match(/### 【关键事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
|
||
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, '').trim() : l.replace(/^\d+\.\s*/, '').trim()
|
||
}).filter(Boolean)
|
||
}
|
||
|
||
// 提取核心实体 - 完整提取,包含摘要和相关事实数
|
||
const entitySection = text.match(/### 【核心实体】\n([\s\S]*?)(?=\n###|$)/)
|
||
if (entitySection) {
|
||
const entityText = entitySection[1]
|
||
// 按 "- **" 分割实体块
|
||
const entityBlocks = entityText.split(/\n(?=- \*\*)/).filter(b => b.trim().startsWith('- **'))
|
||
result.entities = entityBlocks.map(block => {
|
||
const nameMatch = block.match(/^-\s*\*\*(.+?)\*\*\s*\((.+?)\)/)
|
||
const summaryMatch = block.match(/摘要:\s*"?(.+?)"?(?:\n|$)/)
|
||
const relatedMatch = block.match(/相关事实:\s*(\d+)/)
|
||
return {
|
||
name: nameMatch ? nameMatch[1].trim() : '',
|
||
type: nameMatch ? nameMatch[2].trim() : '',
|
||
summary: summaryMatch ? summaryMatch[1].trim() : '',
|
||
relatedFactsCount: relatedMatch ? parseInt(relatedMatch[1]) : 0
|
||
}
|
||
}).filter(e => e.name)
|
||
}
|
||
|
||
// 提取关系链 - 完整提取,不限制数量
|
||
const relSection = text.match(/### 【关系链】\n([\s\S]*?)(?=\n###|$)/)
|
||
if (relSection) {
|
||
const lines = relSection[1].split('\n').filter(l => l.trim().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)
|
||
}
|
||
} catch (e) {
|
||
console.warn('Parse insight_forge failed:', e)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
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]*?)(?=\n###|$)/)
|
||
if (activeSection) {
|
||
const lines = activeSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||
result.activeFacts = lines.map(l => {
|
||
// 移除编号和引号
|
||
const factText = l.replace(/^\d+\.\s*/, '').replace(/^"|"$/g, '').trim()
|
||
return factText
|
||
}).filter(Boolean)
|
||
}
|
||
|
||
// 提取历史/过期事实 - 完整提取,不限制数量
|
||
const histSection = text.match(/### 【历史\/过期事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
|
||
if (histSection) {
|
||
const lines = histSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||
result.historicalFacts = lines.map(l => {
|
||
const factText = l.replace(/^\d+\.\s*/, '').replace(/^"|"$/g, '').trim()
|
||
return factText
|
||
}).filter(Boolean)
|
||
}
|
||
|
||
// 提取涉及实体 - 完整提取,不限制数量
|
||
const entitySection = text.match(/### 【涉及实体】\n([\s\S]*?)(?=\n###|$)/)
|
||
if (entitySection) {
|
||
const lines = entitySection[1].split('\n').filter(l => l.trim().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)
|
||
}
|
||
} catch (e) {
|
||
console.warn('Parse panorama failed:', e)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
const parseInterview = (text) => {
|
||
const result = {
|
||
topic: '',
|
||
agentCount: '',
|
||
successCount: 0,
|
||
totalCount: 0,
|
||
selectionReason: '',
|
||
interviews: [],
|
||
summary: ''
|
||
}
|
||
|
||
try {
|
||
// 提取采访主题
|
||
const topicMatch = text.match(/\*\*采访主题:\*\*\s*(.+?)(?:\n|$)/)
|
||
if (topicMatch) result.topic = topicMatch[1].trim()
|
||
|
||
// 提取采访人数(如 "5 / 9 位模拟Agent")
|
||
const countMatch = text.match(/\*\*采访人数:\*\*\s*(\d+)\s*\/\s*(\d+)/)
|
||
if (countMatch) {
|
||
result.successCount = parseInt(countMatch[1])
|
||
result.totalCount = parseInt(countMatch[2])
|
||
result.agentCount = `${countMatch[1]} / ${countMatch[2]}`
|
||
}
|
||
|
||
// 提取采访对象选择理由
|
||
const reasonMatch = text.match(/### 采访对象选择理由\n([\s\S]*?)(?=\n---\n|\n### 采访实录)/)
|
||
if (reasonMatch) {
|
||
result.selectionReason = reasonMatch[1].trim()
|
||
}
|
||
|
||
// 解析每个人的选择理由
|
||
const parseIndividualReasons = (reasonText) => {
|
||
const reasons = {}
|
||
if (!reasonText) return reasons
|
||
|
||
const lines = reasonText.split(/\n+/)
|
||
let currentName = null
|
||
let currentReason = []
|
||
|
||
for (const line of lines) {
|
||
let headerMatch = null
|
||
let name = null
|
||
let reasonStart = null
|
||
|
||
// 格式1: 数字. **名字(index=X)**:理由
|
||
// 例如: 1. **校友_345(index=1)**:作为武大校友...
|
||
headerMatch = line.match(/^\d+\.\s*\*\*([^*((]+)(?:[((]index\s*=?\s*\d+[))])?\*\*[::]\s*(.*)/)
|
||
if (headerMatch) {
|
||
name = headerMatch[1].trim()
|
||
reasonStart = headerMatch[2]
|
||
}
|
||
|
||
// 格式2: - 选择名字(index X):理由
|
||
// 例如: - 选择家长_601(index 0):作为家长群体代表...
|
||
if (!headerMatch) {
|
||
headerMatch = line.match(/^-\s*选择([^((]+)(?:[((]index\s*=?\s*\d+[))])?[::]\s*(.*)/)
|
||
if (headerMatch) {
|
||
name = headerMatch[1].trim()
|
||
reasonStart = headerMatch[2]
|
||
}
|
||
}
|
||
|
||
// 格式3: - **名字(index X)**:理由
|
||
// 例如: - **家长_601(index 0)**:作为家长群体代表...
|
||
if (!headerMatch) {
|
||
headerMatch = line.match(/^-\s*\*\*([^*((]+)(?:[((]index\s*=?\s*\d+[))])?\*\*[::]\s*(.*)/)
|
||
if (headerMatch) {
|
||
name = headerMatch[1].trim()
|
||
reasonStart = headerMatch[2]
|
||
}
|
||
}
|
||
|
||
if (name) {
|
||
// 保存上一个人的理由
|
||
if (currentName && currentReason.length > 0) {
|
||
reasons[currentName] = currentReason.join(' ').trim()
|
||
}
|
||
// 开始新的人
|
||
currentName = name
|
||
currentReason = reasonStart ? [reasonStart.trim()] : []
|
||
} else if (currentName && line.trim() && !line.match(/^未选|^综上|^最终选择/)) {
|
||
// 理由的续行(排除结尾总结段落)
|
||
currentReason.push(line.trim())
|
||
}
|
||
}
|
||
|
||
// 保存最后一个人的理由
|
||
if (currentName && currentReason.length > 0) {
|
||
reasons[currentName] = currentReason.join(' ').trim()
|
||
}
|
||
|
||
return reasons
|
||
}
|
||
|
||
const individualReasons = parseIndividualReasons(result.selectionReason)
|
||
|
||
// 提取每个采访记录
|
||
const interviewBlocks = text.split(/#### 采访 #\d+:/).slice(1)
|
||
|
||
interviewBlocks.forEach((block, index) => {
|
||
const interview = {
|
||
num: index + 1,
|
||
title: '',
|
||
name: '',
|
||
role: '',
|
||
bio: '',
|
||
selectionReason: '',
|
||
questions: [],
|
||
twitterAnswer: '',
|
||
redditAnswer: '',
|
||
quotes: []
|
||
}
|
||
|
||
// 提取标题(如 "学生"、"教育从业者" 等)
|
||
const titleMatch = block.match(/^(.+?)\n/)
|
||
if (titleMatch) interview.title = titleMatch[1].trim()
|
||
|
||
// 提取姓名和角色
|
||
const nameRoleMatch = block.match(/\*\*(.+?)\*\*\s*\((.+?)\)/)
|
||
if (nameRoleMatch) {
|
||
interview.name = nameRoleMatch[1].trim()
|
||
interview.role = nameRoleMatch[2].trim()
|
||
// 设置该人的选择理由
|
||
interview.selectionReason = individualReasons[interview.name] || ''
|
||
}
|
||
|
||
// 提取简介
|
||
const bioMatch = block.match(/_简介:\s*([\s\S]*?)_\n/)
|
||
if (bioMatch) {
|
||
interview.bio = bioMatch[1].trim().replace(/\.\.\.$/, '...')
|
||
}
|
||
|
||
// 提取问题列表
|
||
const qMatch = block.match(/\*\*Q:\*\*\s*([\s\S]*?)(?=\n\n\*\*A:\*\*|\*\*A:\*\*)/)
|
||
if (qMatch) {
|
||
const qText = qMatch[1].trim()
|
||
// 按数字编号分割问题
|
||
const questions = qText.split(/\n\d+\.\s+/).filter(q => q.trim())
|
||
if (questions.length > 0) {
|
||
// 如果第一个问题前面有"1.",需要特殊处理
|
||
const firstQ = qText.match(/^1\.\s+(.+)/)
|
||
if (firstQ) {
|
||
interview.questions = [firstQ[1].trim(), ...questions.slice(1).map(q => q.trim())]
|
||
} else {
|
||
interview.questions = questions.map(q => q.trim())
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提取回答 - 分Twitter和Reddit
|
||
const answerMatch = block.match(/\*\*A:\*\*\s*([\s\S]*?)(?=\*\*关键引言|$)/)
|
||
if (answerMatch) {
|
||
const answerText = answerMatch[1].trim()
|
||
|
||
// 分离Twitter和Reddit回答
|
||
const twitterMatch = answerText.match(/【Twitter平台回答】\n?([\s\S]*?)(?=【Reddit平台回答】|$)/)
|
||
const redditMatch = answerText.match(/【Reddit平台回答】\n?([\s\S]*?)$/)
|
||
|
||
if (twitterMatch) {
|
||
interview.twitterAnswer = twitterMatch[1].trim()
|
||
}
|
||
if (redditMatch) {
|
||
interview.redditAnswer = redditMatch[1].trim()
|
||
}
|
||
|
||
// 平台回退逻辑(兼容旧格式:只有一个平台标记的情况)
|
||
if (!twitterMatch && redditMatch) {
|
||
// 只有 Reddit 回答,仅在非占位文本时复制为默认显示
|
||
if (interview.redditAnswer && interview.redditAnswer !== '(该平台未获得回复)') {
|
||
interview.twitterAnswer = interview.redditAnswer
|
||
}
|
||
} else if (twitterMatch && !redditMatch) {
|
||
if (interview.twitterAnswer && interview.twitterAnswer !== '(该平台未获得回复)') {
|
||
interview.redditAnswer = interview.twitterAnswer
|
||
}
|
||
} else if (!twitterMatch && !redditMatch) {
|
||
// 没有分平台标记(极旧格式),整体作为回答
|
||
interview.twitterAnswer = answerText
|
||
}
|
||
}
|
||
|
||
// 提取关键引言(兼容多种引号格式)
|
||
const quotesMatch = block.match(/\*\*关键引言:\*\*\n([\s\S]*?)(?=\n---|\n####|$)/)
|
||
if (quotesMatch) {
|
||
const quotesText = quotesMatch[1]
|
||
// 优先匹配 > "text" 格式
|
||
let quoteMatches = quotesText.match(/> "([^"]+)"/g)
|
||
// 回退:匹配 > "text" 或 > \u201Ctext\u201D(中文引号)
|
||
if (!quoteMatches) {
|
||
quoteMatches = quotesText.match(/> [\u201C""]([^\u201D""]+)[\u201D""]/g)
|
||
}
|
||
if (quoteMatches) {
|
||
interview.quotes = quoteMatches
|
||
.map(q => q.replace(/^> [\u201C""]|[\u201D""]$/g, '').trim())
|
||
.filter(q => q)
|
||
}
|
||
}
|
||
|
||
if (interview.name || interview.title) {
|
||
result.interviews.push(interview)
|
||
}
|
||
})
|
||
|
||
// 提取采访摘要
|
||
const summaryMatch = text.match(/### 采访摘要与核心观点\n([\s\S]*?)$/)
|
||
if (summaryMatch) {
|
||
result.summary = summaryMatch[1].trim()
|
||
}
|
||
} catch (e) {
|
||
console.warn('Parse interview failed:', e)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
const parseQuickSearch = (text) => {
|
||
const result = {
|
||
query: '',
|
||
count: 0,
|
||
facts: [],
|
||
edges: [],
|
||
nodes: []
|
||
}
|
||
|
||
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()).filter(Boolean)
|
||
}
|
||
|
||
// 尝试提取边信息(如果有)
|
||
const edgesSection = text.match(/### 相关边:\n([\s\S]*?)(?=\n###|$)/)
|
||
if (edgesSection) {
|
||
const lines = edgesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
||
result.edges = 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)
|
||
}
|
||
|
||
// 尝试提取节点信息(如果有)
|
||
const nodesSection = text.match(/### 相关节点:\n([\s\S]*?)(?=\n###|$)/)
|
||
if (nodesSection) {
|
||
const lines = nodesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
||
result.nodes = lines.map(l => {
|
||
const match = l.match(/^-\s*\*\*(.+?)\*\*\s*\((.+?)\)/)
|
||
if (match) return { name: match[1].trim(), type: match[2].trim() }
|
||
const simpleMatch = l.match(/^-\s*(.+)$/)
|
||
if (simpleMatch) return { name: simpleMatch[1].trim(), type: '' }
|
||
return null
|
||
}).filter(Boolean)
|
||
}
|
||
} catch (e) {
|
||
console.warn('Parse quick_search failed:', e)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// ========== Sub Components ==========
|
||
|
||
// Insight Display Component - Enhanced with full data rendering (Interview-like style)
|
||
const InsightDisplay = {
|
||
props: ['result', 'resultLength'],
|
||
setup(props) {
|
||
const activeTab = ref('facts') // 'facts', 'entities', 'relations', 'subqueries'
|
||
const expandedFacts = ref(false)
|
||
const expandedEntities = ref(false)
|
||
const expandedRelations = ref(false)
|
||
const INITIAL_SHOW_COUNT = 5
|
||
|
||
// Format result size for display
|
||
const formatSize = (length) => {
|
||
if (!length) return ''
|
||
if (length >= 1000) {
|
||
return `${(length / 1000).toFixed(1)}k chars`
|
||
}
|
||
return `${length} chars`
|
||
}
|
||
|
||
return () => h('div', { class: 'insight-display' }, [
|
||
// Header Section - like interview header
|
||
h('div', { class: 'insight-header' }, [
|
||
h('div', { class: 'header-main' }, [
|
||
h('div', { class: 'header-title' }, 'Deep Insight'),
|
||
h('div', { class: 'header-stats' }, [
|
||
h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.stats.facts || props.result.facts.length),
|
||
h('span', { class: 'stat-label' }, 'Facts')
|
||
]),
|
||
h('span', { class: 'stat-divider' }, '/'),
|
||
h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.stats.entities || props.result.entities.length),
|
||
h('span', { class: 'stat-label' }, 'Entities')
|
||
]),
|
||
h('span', { class: 'stat-divider' }, '/'),
|
||
h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.stats.relationships || props.result.relations.length),
|
||
h('span', { class: 'stat-label' }, 'Relations')
|
||
]),
|
||
props.resultLength && h('span', { class: 'stat-divider' }, '·'),
|
||
props.resultLength && h('span', { class: 'stat-size' }, formatSize(props.resultLength))
|
||
])
|
||
]),
|
||
props.result.query && h('div', { class: 'header-topic' }, props.result.query),
|
||
props.result.simulationRequirement && h('div', { class: 'header-scenario' }, [
|
||
h('span', { class: 'scenario-label' }, '预测场景: '),
|
||
h('span', { class: 'scenario-text' }, props.result.simulationRequirement)
|
||
])
|
||
]),
|
||
|
||
// Tab Navigation
|
||
h('div', { class: 'insight-tabs' }, [
|
||
h('button', {
|
||
class: ['insight-tab', { active: activeTab.value === 'facts' }],
|
||
onClick: () => { activeTab.value = 'facts' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `当前关键记忆 (${props.result.facts.length})`)
|
||
]),
|
||
h('button', {
|
||
class: ['insight-tab', { active: activeTab.value === 'entities' }],
|
||
onClick: () => { activeTab.value = 'entities' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `核心实体 (${props.result.entities.length})`)
|
||
]),
|
||
h('button', {
|
||
class: ['insight-tab', { active: activeTab.value === 'relations' }],
|
||
onClick: () => { activeTab.value = 'relations' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `关系链 (${props.result.relations.length})`)
|
||
]),
|
||
props.result.subQueries.length > 0 && h('button', {
|
||
class: ['insight-tab', { active: activeTab.value === 'subqueries' }],
|
||
onClick: () => { activeTab.value = 'subqueries' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `子问题 (${props.result.subQueries.length})`)
|
||
])
|
||
]),
|
||
|
||
// Tab Content
|
||
h('div', { class: 'insight-content' }, [
|
||
// Facts Tab
|
||
activeTab.value === 'facts' && props.result.facts.length > 0 && h('div', { class: 'facts-panel' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '时序记忆中所关联的最新关键事实'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.facts.length} 条`)
|
||
]),
|
||
h('div', { class: 'facts-list' },
|
||
(expandedFacts.value ? props.result.facts : props.result.facts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||
h('div', { class: 'fact-item', key: i }, [
|
||
h('span', { class: 'fact-number' }, i + 1),
|
||
h('div', { class: 'fact-content' }, fact)
|
||
])
|
||
)
|
||
),
|
||
props.result.facts.length > INITIAL_SHOW_COUNT && h('button', {
|
||
class: 'expand-btn',
|
||
onClick: () => { expandedFacts.value = !expandedFacts.value }
|
||
}, expandedFacts.value ? `收起 ▲` : `展开全部 ${props.result.facts.length} 条 ▼`)
|
||
]),
|
||
|
||
// Entities Tab
|
||
activeTab.value === 'entities' && props.result.entities.length > 0 && h('div', { class: 'entities-panel' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '核心实体'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.entities.length} 个`)
|
||
]),
|
||
h('div', { class: 'entities-grid' },
|
||
(expandedEntities.value ? props.result.entities : props.result.entities.slice(0, 12)).map((entity, i) =>
|
||
h('div', { class: 'entity-tag', key: i, title: entity.summary || '' }, [
|
||
h('span', { class: 'entity-name' }, entity.name),
|
||
h('span', { class: 'entity-type' }, entity.type),
|
||
entity.relatedFactsCount > 0 && h('span', { class: 'entity-fact-count' }, `${entity.relatedFactsCount}条`)
|
||
])
|
||
)
|
||
),
|
||
props.result.entities.length > 12 && h('button', {
|
||
class: 'expand-btn',
|
||
onClick: () => { expandedEntities.value = !expandedEntities.value }
|
||
}, expandedEntities.value ? `收起 ▲` : `展开全部 ${props.result.entities.length} 个 ▼`)
|
||
]),
|
||
|
||
// Relations Tab
|
||
activeTab.value === 'relations' && props.result.relations.length > 0 && h('div', { class: 'relations-panel' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '关系链'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.relations.length} 条`)
|
||
]),
|
||
h('div', { class: 'relations-list' },
|
||
(expandedRelations.value ? props.result.relations : props.result.relations.slice(0, INITIAL_SHOW_COUNT)).map((rel, i) =>
|
||
h('div', { class: 'relation-item', key: i }, [
|
||
h('span', { class: 'rel-source' }, rel.source),
|
||
h('span', { class: 'rel-arrow' }, [
|
||
h('span', { class: 'rel-line' }),
|
||
h('span', { class: 'rel-label' }, rel.relation),
|
||
h('span', { class: 'rel-line' })
|
||
]),
|
||
h('span', { class: 'rel-target' }, rel.target)
|
||
])
|
||
)
|
||
),
|
||
props.result.relations.length > INITIAL_SHOW_COUNT && h('button', {
|
||
class: 'expand-btn',
|
||
onClick: () => { expandedRelations.value = !expandedRelations.value }
|
||
}, expandedRelations.value ? `收起 ▲` : `展开全部 ${props.result.relations.length} 条 ▼`)
|
||
]),
|
||
|
||
// Sub-queries Tab
|
||
activeTab.value === 'subqueries' && props.result.subQueries.length > 0 && h('div', { class: 'subqueries-panel' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '漂移查询生成分析子问题'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.subQueries.length} 个`)
|
||
]),
|
||
h('div', { class: 'subqueries-list' },
|
||
props.result.subQueries.map((sq, i) =>
|
||
h('div', { class: 'subquery-item', key: i }, [
|
||
h('span', { class: 'subquery-number' }, `Q${i + 1}`),
|
||
h('div', { class: 'subquery-text' }, sq)
|
||
])
|
||
)
|
||
)
|
||
]),
|
||
|
||
// Empty state
|
||
activeTab.value === 'facts' && props.result.facts.length === 0 && h('div', { class: 'empty-state' }, '暂无当前关键记忆'),
|
||
activeTab.value === 'entities' && props.result.entities.length === 0 && h('div', { class: 'empty-state' }, '暂无核心实体'),
|
||
activeTab.value === 'relations' && props.result.relations.length === 0 && h('div', { class: 'empty-state' }, '暂无关系链')
|
||
])
|
||
])
|
||
}
|
||
}
|
||
|
||
// Panorama Display Component - Enhanced with Active/Historical tabs
|
||
const PanoramaDisplay = {
|
||
props: ['result', 'resultLength'],
|
||
setup(props) {
|
||
const activeTab = ref('active') // 'active', 'historical', 'entities'
|
||
const expandedActive = ref(false)
|
||
const expandedHistorical = ref(false)
|
||
const expandedEntities = ref(false)
|
||
const INITIAL_SHOW_COUNT = 5
|
||
|
||
// Format result size for display
|
||
const formatSize = (length) => {
|
||
if (!length) return ''
|
||
if (length >= 1000) {
|
||
return `${(length / 1000).toFixed(1)}k chars`
|
||
}
|
||
return `${length} chars`
|
||
}
|
||
|
||
return () => h('div', { class: 'panorama-display' }, [
|
||
// Header Section
|
||
h('div', { class: 'panorama-header' }, [
|
||
h('div', { class: 'header-main' }, [
|
||
h('div', { class: 'header-title' }, 'Panorama Search'),
|
||
h('div', { class: 'header-stats' }, [
|
||
h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.stats.nodes),
|
||
h('span', { class: 'stat-label' }, 'Nodes')
|
||
]),
|
||
h('span', { class: 'stat-divider' }, '/'),
|
||
h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.stats.edges),
|
||
h('span', { class: 'stat-label' }, 'Edges')
|
||
]),
|
||
props.resultLength && h('span', { class: 'stat-divider' }, '·'),
|
||
props.resultLength && h('span', { class: 'stat-size' }, formatSize(props.resultLength))
|
||
])
|
||
]),
|
||
props.result.query && h('div', { class: 'header-topic' }, props.result.query)
|
||
]),
|
||
|
||
// Tab Navigation
|
||
h('div', { class: 'panorama-tabs' }, [
|
||
h('button', {
|
||
class: ['panorama-tab', { active: activeTab.value === 'active' }],
|
||
onClick: () => { activeTab.value = 'active' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `当前有效记忆 (${props.result.activeFacts.length})`)
|
||
]),
|
||
h('button', {
|
||
class: ['panorama-tab', { active: activeTab.value === 'historical' }],
|
||
onClick: () => { activeTab.value = 'historical' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `历史记忆 (${props.result.historicalFacts.length})`)
|
||
]),
|
||
h('button', {
|
||
class: ['panorama-tab', { active: activeTab.value === 'entities' }],
|
||
onClick: () => { activeTab.value = 'entities' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `涉及实体 (${props.result.entities.length})`)
|
||
])
|
||
]),
|
||
|
||
// Tab Content
|
||
h('div', { class: 'panorama-content' }, [
|
||
// Active Facts Tab
|
||
activeTab.value === 'active' && h('div', { class: 'facts-panel active-facts' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '当前有效记忆'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.activeFacts.length} 条`)
|
||
]),
|
||
props.result.activeFacts.length > 0 ? h('div', { class: 'facts-list' },
|
||
(expandedActive.value ? props.result.activeFacts : props.result.activeFacts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||
h('div', { class: 'fact-item active', key: i }, [
|
||
h('span', { class: 'fact-number' }, i + 1),
|
||
h('div', { class: 'fact-content' }, fact)
|
||
])
|
||
)
|
||
) : h('div', { class: 'empty-state' }, '暂无当前有效记忆'),
|
||
props.result.activeFacts.length > INITIAL_SHOW_COUNT && h('button', {
|
||
class: 'expand-btn',
|
||
onClick: () => { expandedActive.value = !expandedActive.value }
|
||
}, expandedActive.value ? `收起 ▲` : `展开全部 ${props.result.activeFacts.length} 条 ▼`)
|
||
]),
|
||
|
||
// Historical Facts Tab
|
||
activeTab.value === 'historical' && h('div', { class: 'facts-panel historical-facts' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '历史记忆'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.historicalFacts.length} 条`)
|
||
]),
|
||
props.result.historicalFacts.length > 0 ? h('div', { class: 'facts-list' },
|
||
(expandedHistorical.value ? props.result.historicalFacts : props.result.historicalFacts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||
h('div', { class: 'fact-item historical', key: i }, [
|
||
h('span', { class: 'fact-number' }, i + 1),
|
||
h('div', { class: 'fact-content' }, [
|
||
// 尝试提取时间信息 [time - time]
|
||
(() => {
|
||
const timeMatch = fact.match(/^\[(.+?)\]\s*(.*)$/)
|
||
if (timeMatch) {
|
||
return [
|
||
h('span', { class: 'fact-time' }, timeMatch[1]),
|
||
h('span', { class: 'fact-text' }, timeMatch[2])
|
||
]
|
||
}
|
||
return h('span', { class: 'fact-text' }, fact)
|
||
})()
|
||
])
|
||
])
|
||
)
|
||
) : h('div', { class: 'empty-state' }, '暂无历史记忆'),
|
||
props.result.historicalFacts.length > INITIAL_SHOW_COUNT && h('button', {
|
||
class: 'expand-btn',
|
||
onClick: () => { expandedHistorical.value = !expandedHistorical.value }
|
||
}, expandedHistorical.value ? `收起 ▲` : `展开全部 ${props.result.historicalFacts.length} 条 ▼`)
|
||
]),
|
||
|
||
// Entities Tab
|
||
activeTab.value === 'entities' && h('div', { class: 'entities-panel' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '涉及实体'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.entities.length} 个`)
|
||
]),
|
||
props.result.entities.length > 0 ? h('div', { class: 'entities-grid' },
|
||
(expandedEntities.value ? props.result.entities : props.result.entities.slice(0, 8)).map((entity, i) =>
|
||
h('div', { class: 'entity-tag', key: i }, [
|
||
h('span', { class: 'entity-name' }, entity.name),
|
||
entity.type && h('span', { class: 'entity-type' }, entity.type)
|
||
])
|
||
)
|
||
) : h('div', { class: 'empty-state' }, '暂无涉及实体'),
|
||
props.result.entities.length > 8 && h('button', {
|
||
class: 'expand-btn',
|
||
onClick: () => { expandedEntities.value = !expandedEntities.value }
|
||
}, expandedEntities.value ? `收起 ▲` : `展开全部 ${props.result.entities.length} 个 ▼`)
|
||
])
|
||
])
|
||
])
|
||
}
|
||
}
|
||
|
||
// Interview Display Component - Conversation Style (Q&A Format)
|
||
const InterviewDisplay = {
|
||
props: ['result', 'resultLength'],
|
||
setup(props) {
|
||
// Format result size for display
|
||
const formatSize = (length) => {
|
||
if (!length) return ''
|
||
if (length >= 1000) {
|
||
return `${(length / 1000).toFixed(1)}k chars`
|
||
}
|
||
return `${length} chars`
|
||
}
|
||
|
||
// Clean quote text - remove leading list numbers to avoid double numbering
|
||
const cleanQuoteText = (text) => {
|
||
if (!text) return ''
|
||
// Remove leading patterns like "1. ", "2. ", "1、", "(1)", "(1)" etc.
|
||
return text.replace(/^\s*\d+[\.\、\))]\s*/, '').trim()
|
||
}
|
||
|
||
const activeIndex = ref(0)
|
||
const expandedAnswers = ref(new Set())
|
||
// 为每个问题-回答对维护独立的平台选择状态
|
||
const platformTabs = reactive({}) // { 'agentIdx-qIdx': 'twitter' | 'reddit' }
|
||
|
||
// 获取某个问题的当前平台选择
|
||
const getPlatformTab = (agentIdx, qIdx) => {
|
||
const key = `${agentIdx}-${qIdx}`
|
||
return platformTabs[key] || 'twitter'
|
||
}
|
||
|
||
// 设置某个问题的平台选择
|
||
const setPlatformTab = (agentIdx, qIdx, platform) => {
|
||
const key = `${agentIdx}-${qIdx}`
|
||
platformTabs[key] = platform
|
||
}
|
||
|
||
const toggleAnswer = (key) => {
|
||
const newSet = new Set(expandedAnswers.value)
|
||
if (newSet.has(key)) {
|
||
newSet.delete(key)
|
||
} else {
|
||
newSet.add(key)
|
||
}
|
||
expandedAnswers.value = newSet
|
||
}
|
||
|
||
const formatAnswer = (text, expanded) => {
|
||
if (!text) return ''
|
||
if (expanded || text.length <= 400) return text
|
||
return text.substring(0, 400) + '...'
|
||
}
|
||
|
||
// 检查是否为平台占位文本
|
||
const isPlaceholderText = (text) => {
|
||
if (!text) return true
|
||
const t = text.trim()
|
||
return t === '(该平台未获得回复)' || t === '(该平台未获得回复)' || t === '[无回复]'
|
||
}
|
||
|
||
// 尝试按问题编号分割回答
|
||
const splitAnswerByQuestions = (answerText, questionCount) => {
|
||
if (!answerText || questionCount <= 0) return [answerText]
|
||
if (isPlaceholderText(answerText)) return ['']
|
||
|
||
// 支持两种编号格式:
|
||
// 1. "问题X:" 或 "问题X:" (中文格式,后端新格式)
|
||
// 2. "1. " 或 "\n1. " (数字+点,旧格式兼容)
|
||
let matches = []
|
||
let match
|
||
|
||
// 优先尝试 "问题X:" 格式
|
||
const cnPattern = /(?:^|[\r\n]+)问题(\d+)[::]\s*/g
|
||
while ((match = cnPattern.exec(answerText)) !== null) {
|
||
matches.push({
|
||
num: parseInt(match[1]),
|
||
index: match.index,
|
||
fullMatch: match[0]
|
||
})
|
||
}
|
||
|
||
// 如果没匹配到,回退到 "数字." 格式
|
||
if (matches.length === 0) {
|
||
const numPattern = /(?:^|[\r\n]+)(\d+)\.\s+/g
|
||
while ((match = numPattern.exec(answerText)) !== null) {
|
||
matches.push({
|
||
num: parseInt(match[1]),
|
||
index: match.index,
|
||
fullMatch: match[0]
|
||
})
|
||
}
|
||
}
|
||
|
||
// 如果没有找到编号或只找到一个,返回整体
|
||
if (matches.length <= 1) {
|
||
const cleaned = answerText
|
||
.replace(/^问题\d+[::]\s*/, '')
|
||
.replace(/^\d+\.\s+/, '')
|
||
.trim()
|
||
return [cleaned || answerText]
|
||
}
|
||
|
||
// 按编号提取各部分
|
||
const parts = []
|
||
for (let i = 0; i < matches.length; i++) {
|
||
const current = matches[i]
|
||
const next = matches[i + 1]
|
||
|
||
const startIdx = current.index + current.fullMatch.length
|
||
const endIdx = next ? next.index : answerText.length
|
||
|
||
let part = answerText.substring(startIdx, endIdx).trim()
|
||
part = part.replace(/[\r\n]+$/, '').trim()
|
||
parts.push(part)
|
||
}
|
||
|
||
if (parts.length > 0 && parts.some(p => p)) {
|
||
return parts
|
||
}
|
||
|
||
return [answerText]
|
||
}
|
||
|
||
// 获取某个问题对应的回答
|
||
const getAnswerForQuestion = (interview, qIdx, platform) => {
|
||
const answer = platform === 'twitter' ? interview.twitterAnswer : (interview.redditAnswer || interview.twitterAnswer)
|
||
if (!answer || isPlaceholderText(answer)) return answer || ''
|
||
|
||
const questionCount = interview.questions?.length || 1
|
||
const answers = splitAnswerByQuestions(answer, questionCount)
|
||
|
||
// 分割成功且索引有效
|
||
if (answers.length > 1 && qIdx < answers.length) {
|
||
return answers[qIdx] || ''
|
||
}
|
||
|
||
// 分割失败:第一个问题返回完整回答,其余返回空
|
||
return qIdx === 0 ? answer : ''
|
||
}
|
||
|
||
// 检查某个问题是否有双平台回答(过滤占位文本)
|
||
const hasMultiplePlatforms = (interview, qIdx) => {
|
||
if (!interview.twitterAnswer || !interview.redditAnswer) return false
|
||
const twitterAnswer = getAnswerForQuestion(interview, qIdx, 'twitter')
|
||
const redditAnswer = getAnswerForQuestion(interview, qIdx, 'reddit')
|
||
// 两个平台都有真实回答(非占位文本)且内容不同
|
||
return !isPlaceholderText(twitterAnswer) && !isPlaceholderText(redditAnswer) && twitterAnswer !== redditAnswer
|
||
}
|
||
|
||
return () => h('div', { class: 'interview-display' }, [
|
||
// Header Section
|
||
h('div', { class: 'interview-header' }, [
|
||
h('div', { class: 'header-main' }, [
|
||
h('div', { class: 'header-title' }, 'Agent Interview'),
|
||
h('div', { class: 'header-stats' }, [
|
||
h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.successCount || props.result.interviews.length),
|
||
h('span', { class: 'stat-label' }, 'Interviewed')
|
||
]),
|
||
props.result.totalCount > 0 && h('span', { class: 'stat-divider' }, '/'),
|
||
props.result.totalCount > 0 && h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.totalCount),
|
||
h('span', { class: 'stat-label' }, 'Total')
|
||
]),
|
||
props.resultLength && h('span', { class: 'stat-divider' }, '·'),
|
||
props.resultLength && h('span', { class: 'stat-size' }, formatSize(props.resultLength))
|
||
])
|
||
]),
|
||
props.result.topic && h('div', { class: 'header-topic' }, props.result.topic)
|
||
]),
|
||
|
||
// Agent Selector Tabs
|
||
props.result.interviews.length > 0 && h('div', { class: 'agent-tabs' },
|
||
props.result.interviews.map((interview, i) => h('button', {
|
||
class: ['agent-tab', { active: activeIndex.value === i }],
|
||
key: i,
|
||
onClick: () => { activeIndex.value = i }
|
||
}, [
|
||
h('span', { class: 'tab-avatar' }, interview.name ? interview.name.charAt(0) : (i + 1)),
|
||
h('span', { class: 'tab-name' }, interview.title || interview.name || `Agent ${i + 1}`)
|
||
]))
|
||
),
|
||
|
||
// Active Interview Detail
|
||
props.result.interviews.length > 0 && h('div', { class: 'interview-detail' }, [
|
||
// Agent Profile Card
|
||
h('div', { class: 'agent-profile' }, [
|
||
h('div', { class: 'profile-avatar' }, props.result.interviews[activeIndex.value]?.name?.charAt(0) || 'A'),
|
||
h('div', { class: 'profile-info' }, [
|
||
h('div', { class: 'profile-name' }, props.result.interviews[activeIndex.value]?.name || 'Agent'),
|
||
h('div', { class: 'profile-role' }, props.result.interviews[activeIndex.value]?.role || ''),
|
||
props.result.interviews[activeIndex.value]?.bio && h('div', { class: 'profile-bio' }, props.result.interviews[activeIndex.value].bio)
|
||
])
|
||
]),
|
||
|
||
// Selection Reason - 选择理由
|
||
props.result.interviews[activeIndex.value]?.selectionReason && h('div', { class: 'selection-reason' }, [
|
||
h('div', { class: 'reason-label' }, '选择理由'),
|
||
h('div', { class: 'reason-content' }, props.result.interviews[activeIndex.value].selectionReason)
|
||
]),
|
||
|
||
// Q&A Conversation Thread - 一问一答样式
|
||
h('div', { class: 'qa-thread' },
|
||
(props.result.interviews[activeIndex.value]?.questions?.length > 0
|
||
? props.result.interviews[activeIndex.value].questions
|
||
: [props.result.interviews[activeIndex.value]?.question || 'No question available']
|
||
).map((question, qIdx) => {
|
||
const interview = props.result.interviews[activeIndex.value]
|
||
const currentPlatform = getPlatformTab(activeIndex.value, qIdx)
|
||
const answerText = getAnswerForQuestion(interview, qIdx, currentPlatform)
|
||
const hasDualPlatform = hasMultiplePlatforms(interview, qIdx)
|
||
const expandKey = `${activeIndex.value}-${qIdx}`
|
||
const isExpanded = expandedAnswers.value.has(expandKey)
|
||
const isPlaceholder = isPlaceholderText(answerText)
|
||
|
||
return h('div', { class: 'qa-pair', key: qIdx }, [
|
||
// Question Block
|
||
h('div', { class: 'qa-question' }, [
|
||
h('div', { class: 'qa-badge q-badge' }, `Q${qIdx + 1}`),
|
||
h('div', { class: 'qa-content' }, [
|
||
h('div', { class: 'qa-sender' }, 'Interviewer'),
|
||
h('div', { class: 'qa-text' }, question)
|
||
])
|
||
]),
|
||
|
||
// Answer Block
|
||
answerText && h('div', { class: ['qa-answer', { 'answer-placeholder': isPlaceholder }] }, [
|
||
h('div', { class: 'qa-badge a-badge' }, `A${qIdx + 1}`),
|
||
h('div', { class: 'qa-content' }, [
|
||
h('div', { class: 'qa-answer-header' }, [
|
||
h('div', { class: 'qa-sender' }, interview?.name || 'Agent'),
|
||
// 双平台切换按钮(仅在有真实双平台回答时显示)
|
||
hasDualPlatform && h('div', { class: 'platform-switch' }, [
|
||
h('button', {
|
||
class: ['platform-btn', { active: currentPlatform === 'twitter' }],
|
||
onClick: (e) => { e.stopPropagation(); setPlatformTab(activeIndex.value, qIdx, 'twitter') }
|
||
}, [
|
||
h('svg', { class: 'platform-icon', 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('line', { x1: '2', y1: '12', x2: '22', y2: '12' }),
|
||
h('path', { d: 'M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z' })
|
||
]),
|
||
h('span', {}, '世界1')
|
||
]),
|
||
h('button', {
|
||
class: ['platform-btn', { active: currentPlatform === 'reddit' }],
|
||
onClick: (e) => { e.stopPropagation(); setPlatformTab(activeIndex.value, qIdx, 'reddit') }
|
||
}, [
|
||
h('svg', { class: 'platform-icon', viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
|
||
h('path', { d: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' })
|
||
]),
|
||
h('span', {}, '世界2')
|
||
])
|
||
])
|
||
]),
|
||
h('div', {
|
||
class: ['qa-text', 'answer-text', { 'placeholder-text': isPlaceholder }],
|
||
innerHTML: isPlaceholder
|
||
? answerText
|
||
: formatAnswer(answerText, isExpanded)
|
||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\n/g, '<br>')
|
||
}),
|
||
// Expand/Collapse Button(占位文本不显示)
|
||
!isPlaceholder && answerText.length > 400 && h('button', {
|
||
class: 'expand-answer-btn',
|
||
onClick: () => toggleAnswer(expandKey)
|
||
}, isExpanded ? 'Show Less' : 'Show More')
|
||
])
|
||
])
|
||
])
|
||
})
|
||
),
|
||
|
||
// Key Quotes Section
|
||
props.result.interviews[activeIndex.value]?.quotes?.length > 0 && h('div', { class: 'quotes-section' }, [
|
||
h('div', { class: 'quotes-header' }, 'Key Quotes'),
|
||
h('div', { class: 'quotes-list' },
|
||
props.result.interviews[activeIndex.value].quotes.slice(0, 3).map((quote, qi) => {
|
||
const cleanedQuote = cleanQuoteText(quote)
|
||
const displayQuote = cleanedQuote.length > 200 ? cleanedQuote.substring(0, 200) + '...' : cleanedQuote
|
||
return h('blockquote', {
|
||
key: qi,
|
||
class: 'quote-item',
|
||
innerHTML: renderMarkdown(displayQuote)
|
||
})
|
||
})
|
||
)
|
||
])
|
||
]),
|
||
|
||
// Summary Section (Collapsible)
|
||
props.result.summary && h('div', { class: 'summary-section' }, [
|
||
h('div', { class: 'summary-header' }, 'Interview Summary'),
|
||
h('div', {
|
||
class: 'summary-content',
|
||
innerHTML: renderMarkdown(props.result.summary.length > 500 ? props.result.summary.substring(0, 500) + '...' : props.result.summary)
|
||
})
|
||
])
|
||
])
|
||
}
|
||
}
|
||
|
||
// Quick Search Display Component - Enhanced with full data rendering
|
||
const QuickSearchDisplay = {
|
||
props: ['result', 'resultLength'],
|
||
setup(props) {
|
||
const activeTab = ref('facts') // 'facts', 'edges', 'nodes'
|
||
const expandedFacts = ref(false)
|
||
const INITIAL_SHOW_COUNT = 5
|
||
|
||
// Check if there are edges or nodes to show tabs
|
||
const hasEdges = computed(() => props.result.edges && props.result.edges.length > 0)
|
||
const hasNodes = computed(() => props.result.nodes && props.result.nodes.length > 0)
|
||
const showTabs = computed(() => hasEdges.value || hasNodes.value)
|
||
|
||
// Format result size for display
|
||
const formatSize = (length) => {
|
||
if (!length) return ''
|
||
if (length >= 1000) {
|
||
return `${(length / 1000).toFixed(1)}k chars`
|
||
}
|
||
return `${length} chars`
|
||
}
|
||
|
||
return () => h('div', { class: 'quick-search-display' }, [
|
||
// Header Section
|
||
h('div', { class: 'quicksearch-header' }, [
|
||
h('div', { class: 'header-main' }, [
|
||
h('div', { class: 'header-title' }, 'Quick Search'),
|
||
h('div', { class: 'header-stats' }, [
|
||
h('span', { class: 'stat-item' }, [
|
||
h('span', { class: 'stat-value' }, props.result.count || props.result.facts.length),
|
||
h('span', { class: 'stat-label' }, 'Results')
|
||
]),
|
||
props.resultLength && h('span', { class: 'stat-divider' }, '·'),
|
||
props.resultLength && h('span', { class: 'stat-size' }, formatSize(props.resultLength))
|
||
])
|
||
]),
|
||
props.result.query && h('div', { class: 'header-query' }, [
|
||
h('span', { class: 'query-label' }, '搜索: '),
|
||
h('span', { class: 'query-text' }, props.result.query)
|
||
])
|
||
]),
|
||
|
||
// Tab Navigation (only show if there are edges or nodes)
|
||
showTabs.value && h('div', { class: 'quicksearch-tabs' }, [
|
||
h('button', {
|
||
class: ['quicksearch-tab', { active: activeTab.value === 'facts' }],
|
||
onClick: () => { activeTab.value = 'facts' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `事实 (${props.result.facts.length})`)
|
||
]),
|
||
hasEdges.value && h('button', {
|
||
class: ['quicksearch-tab', { active: activeTab.value === 'edges' }],
|
||
onClick: () => { activeTab.value = 'edges' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `关系 (${props.result.edges.length})`)
|
||
]),
|
||
hasNodes.value && h('button', {
|
||
class: ['quicksearch-tab', { active: activeTab.value === 'nodes' }],
|
||
onClick: () => { activeTab.value = 'nodes' }
|
||
}, [
|
||
h('span', { class: 'tab-label' }, `节点 (${props.result.nodes.length})`)
|
||
])
|
||
]),
|
||
|
||
// Content Area
|
||
h('div', { class: ['quicksearch-content', { 'no-tabs': !showTabs.value }] }, [
|
||
// Facts (always show if no tabs, or when facts tab is active)
|
||
((!showTabs.value) || activeTab.value === 'facts') && h('div', { class: 'facts-panel' }, [
|
||
!showTabs.value && h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '搜索结果'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.facts.length} 条`)
|
||
]),
|
||
props.result.facts.length > 0 ? h('div', { class: 'facts-list' },
|
||
(expandedFacts.value ? props.result.facts : props.result.facts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||
h('div', { class: 'fact-item', key: i }, [
|
||
h('span', { class: 'fact-number' }, i + 1),
|
||
h('div', { class: 'fact-content' }, fact)
|
||
])
|
||
)
|
||
) : h('div', { class: 'empty-state' }, '未找到相关结果'),
|
||
props.result.facts.length > INITIAL_SHOW_COUNT && h('button', {
|
||
class: 'expand-btn',
|
||
onClick: () => { expandedFacts.value = !expandedFacts.value }
|
||
}, expandedFacts.value ? `收起 ▲` : `展开全部 ${props.result.facts.length} 条 ▼`)
|
||
]),
|
||
|
||
// Edges Tab
|
||
activeTab.value === 'edges' && hasEdges.value && h('div', { class: 'edges-panel' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '相关关系'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.edges.length} 条`)
|
||
]),
|
||
h('div', { class: 'edges-list' },
|
||
props.result.edges.map((edge, i) =>
|
||
h('div', { class: 'edge-item', key: i }, [
|
||
h('span', { class: 'edge-source' }, edge.source),
|
||
h('span', { class: 'edge-arrow' }, [
|
||
h('span', { class: 'edge-line' }),
|
||
h('span', { class: 'edge-label' }, edge.relation),
|
||
h('span', { class: 'edge-line' })
|
||
]),
|
||
h('span', { class: 'edge-target' }, edge.target)
|
||
])
|
||
)
|
||
)
|
||
]),
|
||
|
||
// Nodes Tab
|
||
activeTab.value === 'nodes' && hasNodes.value && h('div', { class: 'nodes-panel' }, [
|
||
h('div', { class: 'panel-header' }, [
|
||
h('span', { class: 'panel-title' }, '相关节点'),
|
||
h('span', { class: 'panel-count' }, `共 ${props.result.nodes.length} 个`)
|
||
]),
|
||
h('div', { class: 'nodes-grid' },
|
||
props.result.nodes.map((node, i) =>
|
||
h('div', { class: 'node-tag', key: i }, [
|
||
h('span', { class: 'node-name' }, node.name),
|
||
node.type && h('span', { class: 'node-type' }, node.type)
|
||
])
|
||
)
|
||
)
|
||
])
|
||
])
|
||
])
|
||
}
|
||
}
|
||
|
||
// 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 'Completed'
|
||
if (agentLogs.value.length > 0) return 'Generating...'
|
||
return 'Waiting'
|
||
})
|
||
|
||
const totalSections = computed(() => {
|
||
return reportOutline.value?.sections?.length || 0
|
||
})
|
||
|
||
const completedSections = computed(() => {
|
||
return Object.keys(generatedSections.value).length
|
||
})
|
||
|
||
const progressPercent = computed(() => {
|
||
if (totalSections.value === 0) return 0
|
||
return Math.round((completedSections.value / totalSections.value) * 100)
|
||
})
|
||
|
||
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
|
||
})
|
||
|
||
// Workflow steps overview (status-based, no nested cards)
|
||
const activeSectionIndex = computed(() => {
|
||
if (isComplete.value) return null
|
||
if (currentSectionIndex.value) return currentSectionIndex.value
|
||
if (totalSections.value > 0 && completedSections.value < totalSections.value) return completedSections.value + 1
|
||
return null
|
||
})
|
||
|
||
const isPlanningDone = computed(() => {
|
||
return !!reportOutline.value?.sections?.length || agentLogs.value.some(l => l.action === 'planning_complete')
|
||
})
|
||
|
||
const isPlanningStarted = computed(() => {
|
||
return agentLogs.value.some(l => l.action === 'planning_start' || l.action === 'report_start')
|
||
})
|
||
|
||
const isFinalizing = computed(() => {
|
||
return !isComplete.value && isPlanningDone.value && totalSections.value > 0 && completedSections.value >= totalSections.value
|
||
})
|
||
|
||
// 当前活跃的步骤(用于顶部显示)
|
||
const activeStep = computed(() => {
|
||
const steps = workflowSteps.value
|
||
// 找到当前 active 的步骤
|
||
const active = steps.find(s => s.status === 'active')
|
||
if (active) return active
|
||
|
||
// 如果没有 active,返回最后一个 done 的步骤
|
||
const doneSteps = steps.filter(s => s.status === 'done')
|
||
if (doneSteps.length > 0) return doneSteps[doneSteps.length - 1]
|
||
|
||
// 否则返回第一个步骤
|
||
return steps[0] || { noLabel: '--', title: '等待开始', status: 'todo', meta: '' }
|
||
})
|
||
|
||
const workflowSteps = computed(() => {
|
||
const steps = []
|
||
|
||
// Planning / Outline
|
||
const planningStatus = isPlanningDone.value ? 'done' : (isPlanningStarted.value ? 'active' : 'todo')
|
||
steps.push({
|
||
key: 'planning',
|
||
noLabel: 'PL',
|
||
title: 'Planning / Outline',
|
||
status: planningStatus,
|
||
meta: planningStatus === 'active' ? 'IN PROGRESS' : ''
|
||
})
|
||
|
||
// Sections (if outline exists)
|
||
const sections = reportOutline.value?.sections || []
|
||
sections.forEach((section, i) => {
|
||
const idx = i + 1
|
||
const status = (isComplete.value || !!generatedSections.value[idx])
|
||
? 'done'
|
||
: (activeSectionIndex.value === idx ? 'active' : 'todo')
|
||
|
||
steps.push({
|
||
key: `section-${idx}`,
|
||
noLabel: String(idx).padStart(2, '0'),
|
||
title: section.title,
|
||
status,
|
||
meta: status === 'active' ? 'IN PROGRESS' : ''
|
||
})
|
||
})
|
||
|
||
// Complete
|
||
const completeStatus = isComplete.value ? 'done' : (isFinalizing.value ? 'active' : 'todo')
|
||
steps.push({
|
||
key: 'complete',
|
||
noLabel: 'OK',
|
||
title: 'Complete',
|
||
status: completeStatus,
|
||
meta: completeStatus === 'active' ? 'FINALIZING' : ''
|
||
})
|
||
|
||
return steps
|
||
})
|
||
|
||
// Methods
|
||
const addLog = (msg) => {
|
||
emit('add-log', msg)
|
||
}
|
||
|
||
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 formatResultSize = (length) => {
|
||
if (!length) return ''
|
||
if (length < 1000) return `${length} chars`
|
||
return `${(length / 1000).toFixed(1)}k chars`
|
||
}
|
||
|
||
const truncateText = (text, maxLen) => {
|
||
if (!text) return ''
|
||
if (text.length <= maxLen) return text
|
||
return text.substring(0, maxLen) + '...'
|
||
}
|
||
|
||
const renderMarkdown = (content) => {
|
||
if (!content) return ''
|
||
|
||
// 去掉开头的二级标题(## xxx),因为章节标题已在外层显示
|
||
let processedContent = content.replace(/^##\s+.+\n+/, '')
|
||
|
||
// 处理代码块
|
||
let html = processedContent.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="code-block"><code>$2</code></pre>')
|
||
|
||
// 处理行内代码
|
||
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||
|
||
// 处理标题
|
||
html = html.replace(/^#### (.+)$/gm, '<h5 class="md-h5">$1</h5>')
|
||
html = html.replace(/^### (.+)$/gm, '<h4 class="md-h4">$1</h4>')
|
||
html = html.replace(/^## (.+)$/gm, '<h3 class="md-h3">$1</h3>')
|
||
html = html.replace(/^# (.+)$/gm, '<h2 class="md-h2">$1</h2>')
|
||
|
||
// 处理引用块
|
||
html = html.replace(/^> (.+)$/gm, '<blockquote class="md-quote">$1</blockquote>')
|
||
|
||
// 处理列表 - 支持子列表
|
||
html = html.replace(/^(\s*)- (.+)$/gm, (match, indent, text) => {
|
||
const level = Math.floor(indent.length / 2)
|
||
return `<li class="md-li" data-level="${level}">${text}</li>`
|
||
})
|
||
html = html.replace(/^(\s*)(\d+)\. (.+)$/gm, (match, indent, num, text) => {
|
||
const level = Math.floor(indent.length / 2)
|
||
return `<li class="md-oli" data-level="${level}">${text}</li>`
|
||
})
|
||
|
||
// 包装无序列表
|
||
html = html.replace(/(<li class="md-li"[^>]*>.*?<\/li>\s*)+/g, '<ul class="md-ul">$&</ul>')
|
||
// 包装有序列表
|
||
html = html.replace(/(<li class="md-oli"[^>]*>.*?<\/li>\s*)+/g, '<ol class="md-ol">$&</ol>')
|
||
|
||
// 清理列表项之间的所有空白
|
||
html = html.replace(/<\/li>\s+<li/g, '</li><li')
|
||
// 清理列表开始标签后的空白
|
||
html = html.replace(/<ul class="md-ul">\s+/g, '<ul class="md-ul">')
|
||
html = html.replace(/<ol class="md-ol">\s+/g, '<ol class="md-ol">')
|
||
// 清理列表结束标签前的空白
|
||
html = html.replace(/\s+<\/ul>/g, '</ul>')
|
||
html = html.replace(/\s+<\/ol>/g, '</ol>')
|
||
|
||
// 处理粗体和斜体
|
||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||
html = html.replace(/_(.+?)_/g, '<em>$1</em>')
|
||
|
||
// 处理分隔线
|
||
html = html.replace(/^---$/gm, '<hr class="md-hr">')
|
||
|
||
// 处理换行 - 空行变成段落分隔,单换行变成 <br>
|
||
html = html.replace(/\n\n/g, '</p><p class="md-p">')
|
||
html = html.replace(/\n/g, '<br>')
|
||
|
||
// 包装在段落中
|
||
html = '<p class="md-p">' + html + '</p>'
|
||
|
||
// 清理空段落
|
||
html = html.replace(/<p class="md-p"><\/p>/g, '')
|
||
html = html.replace(/<p class="md-p">(<h[2-5])/g, '$1')
|
||
html = html.replace(/(<\/h[2-5]>)<\/p>/g, '$1')
|
||
html = html.replace(/<p class="md-p">(<ul|<ol|<blockquote|<pre|<hr)/g, '$1')
|
||
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>|<\/pre>)<\/p>/g, '$1')
|
||
// 清理列表前后的 <br> 标签
|
||
html = html.replace(/<br>\s*(<ul|<ol)/g, '$1')
|
||
html = html.replace(/(<\/ul>|<\/ol>)\s*<br>/g, '$1')
|
||
// 清理连续的 <br> 标签
|
||
html = html.replace(/(<br>\s*){2,}/g, '<br>')
|
||
// 清理列表后紧跟的段落开始标签前的 <br>
|
||
html = html.replace(/(<\/ol>|<\/ul>)<br>(<p|<div)/g, '$1$2')
|
||
|
||
// 修复非连续有序列表的编号:当单项 <ol> 被段落内容隔开时,保持编号递增
|
||
const tokens = html.split(/(<ol class="md-ol">(?:<li class="md-oli"[^>]*>[\s\S]*?<\/li>)+<\/ol>)/g)
|
||
let olCounter = 0
|
||
let inSequence = false
|
||
for (let i = 0; i < tokens.length; i++) {
|
||
if (tokens[i].startsWith('<ol class="md-ol">')) {
|
||
const liCount = (tokens[i].match(/<li class="md-oli"/g) || []).length
|
||
if (liCount === 1) {
|
||
olCounter++
|
||
if (olCounter > 1) {
|
||
tokens[i] = tokens[i].replace('<ol class="md-ol">', `<ol class="md-ol" start="${olCounter}">`)
|
||
}
|
||
inSequence = true
|
||
} else {
|
||
olCounter = 0
|
||
inSequence = false
|
||
}
|
||
} else if (inSequence) {
|
||
if (/<h[2-5]/.test(tokens[i])) {
|
||
olCounter = 0
|
||
inSequence = false
|
||
}
|
||
}
|
||
}
|
||
html = tokens.join('')
|
||
|
||
return html
|
||
}
|
||
|
||
const getTimelineItemClass = (log, idx, total) => {
|
||
const isLatest = idx === total - 1 && !isComplete.value
|
||
const isMilestone = log.action === 'section_complete' || log.action === 'report_complete'
|
||
return {
|
||
'node--active': isLatest,
|
||
'node--done': !isLatest && isMilestone,
|
||
'node--muted': !isLatest && !isMilestone,
|
||
'node--tool': log.action === 'tool_call' || log.action === 'tool_result'
|
||
}
|
||
}
|
||
|
||
const getConnectorClass = (log, idx, total) => {
|
||
const isLatest = idx === total - 1 && !isComplete.value
|
||
if (isLatest) return 'dot-active'
|
||
if (log.action === 'section_complete' || log.action === 'report_complete') return 'dot-done'
|
||
return 'dot-muted'
|
||
}
|
||
|
||
const getActionLabel = (action) => {
|
||
const labels = {
|
||
'report_start': 'Report Started',
|
||
'planning_start': 'Planning',
|
||
'planning_complete': 'Plan Complete',
|
||
'section_start': 'Section Start',
|
||
'section_content': 'Content Ready',
|
||
'section_complete': 'Section Done',
|
||
'tool_call': 'Tool Call',
|
||
'tool_result': 'Tool Result',
|
||
'llm_response': 'LLM Response',
|
||
'report_complete': 'Complete'
|
||
}
|
||
return labels[action] || action
|
||
}
|
||
|
||
const getLogLevelClass = (log) => {
|
||
if (log.includes('ERROR') || log.includes('错误')) return 'error'
|
||
if (log.includes('WARNING') || log.includes('警告')) return 'warning'
|
||
// INFO 使用默认颜色,不标记为 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
|
||
}
|
||
|
||
// section_complete - 章节生成完成
|
||
if (log.action === 'section_complete') {
|
||
if (log.details?.content) {
|
||
generatedSections.value[log.section_index] = log.details.content
|
||
// 自动展开刚生成的章节
|
||
expandedContent.value.add(log.section_index - 1)
|
||
currentSectionIndex.value = null
|
||
}
|
||
}
|
||
|
||
if (log.action === 'report_complete') {
|
||
isComplete.value = true
|
||
currentSectionIndex.value = null // 确保清除 loading 状态
|
||
emit('update-status', 'completed')
|
||
stopPolling()
|
||
// 滚动逻辑统一在循环结束后的 nextTick 中处理
|
||
}
|
||
|
||
if (log.action === 'report_start') {
|
||
startTime.value = new Date(log.timestamp)
|
||
}
|
||
})
|
||
|
||
agentLogLine.value = res.data.from_line + newLogs.length
|
||
|
||
nextTick(() => {
|
||
if (rightPanel.value) {
|
||
// 如果任务已完成,滚动到顶部;否则滚动到底部跟随最新日志
|
||
if (isComplete.value) {
|
||
rightPanel.value.scrollTop = 0
|
||
} else {
|
||
rightPanel.value.scrollTop = rightPanel.value.scrollHeight
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Failed to fetch agent log:', err)
|
||
}
|
||
}
|
||
|
||
// 提取最终答案内容 - 从 LLM response 中提取章节内容
|
||
const extractFinalContent = (response) => {
|
||
if (!response) return null
|
||
|
||
// 尝试提取 <final_answer> 标签内的内容
|
||
const finalAnswerTagMatch = response.match(/<final_answer>([\s\S]*?)<\/final_answer>/)
|
||
if (finalAnswerTagMatch) {
|
||
return finalAnswerTagMatch[1].trim()
|
||
}
|
||
|
||
// 尝试找 Final Answer: 后面的内容(支持多种格式)
|
||
// 格式1: Final Answer:\n\n内容
|
||
// 格式2: Final Answer: 内容
|
||
const finalAnswerMatch = response.match(/Final\s*Answer:\s*\n*([\s\S]*)$/i)
|
||
if (finalAnswerMatch) {
|
||
return finalAnswerMatch[1].trim()
|
||
}
|
||
|
||
// 尝试找 最终答案: 后面的内容
|
||
const chineseFinalMatch = response.match(/最终答案[::]\s*\n*([\s\S]*)$/i)
|
||
if (chineseFinalMatch) {
|
||
return chineseFinalMatch[1].trim()
|
||
}
|
||
|
||
// 如果以 ## 或 # 或 > 开头,可能是直接的 markdown 内容
|
||
const trimmedResponse = response.trim()
|
||
if (trimmedResponse.match(/^[#>]/)) {
|
||
return trimmedResponse
|
||
}
|
||
|
||
// 如果内容较长且包含markdown格式,尝试移除思考过程后返回
|
||
if (response.length > 300 && (response.includes('**') || response.includes('>'))) {
|
||
// 移除 Thought: 开头的思考过程
|
||
const thoughtMatch = response.match(/^Thought:[\s\S]*?(?=\n\n[^T]|\n\n$)/i)
|
||
if (thoughtMatch) {
|
||
const afterThought = response.substring(thoughtMatch[0].length).trim()
|
||
if (afterThought.length > 100) {
|
||
return afterThought
|
||
}
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
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('Failed to fetch console log:', 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 initialized: ${props.reportId}`)
|
||
startPolling()
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopPolling()
|
||
})
|
||
|
||
watch(() => props.reportId, (newId) => {
|
||
if (newId) {
|
||
agentLogs.value = []
|
||
consoleLogs.value = []
|
||
agentLogLine.value = 0
|
||
consoleLogLine.value = 0
|
||
reportOutline.value = null
|
||
currentSectionIndex.value = null
|
||
generatedSections.value = {}
|
||
expandedContent.value = new Set()
|
||
expandedLogs.value = new Set()
|
||
collapsedSections.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: #F8F9FA;
|
||
font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Main Split Layout */
|
||
.main-split-layout {
|
||
flex: 1;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Panel Headers */
|
||
.panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 14px 20px;
|
||
background: #FFFFFF;
|
||
border-bottom: 1px solid #E5E7EB;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.header-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #1F2937;
|
||
box-shadow: 0 0 0 3px rgba(31, 41, 55, 0.15);
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
animation: pulse-dot 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse-dot {
|
||
0%, 100% {
|
||
box-shadow: 0 0 0 3px rgba(31, 41, 55, 0.15);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 0 5px rgba(31, 41, 55, 0.1);
|
||
}
|
||
}
|
||
|
||
.header-index {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #9CA3AF;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
text-transform: none;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.header-meta {
|
||
margin-left: auto;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: #6B7280;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Panel header status variants */
|
||
.panel-header--active {
|
||
background: #FAFAFA;
|
||
border-color: #1F2937;
|
||
}
|
||
|
||
.panel-header--active .header-index {
|
||
color: #1F2937;
|
||
}
|
||
|
||
.panel-header--active .header-title {
|
||
color: #1F2937;
|
||
}
|
||
|
||
.panel-header--active .header-meta {
|
||
color: #1F2937;
|
||
}
|
||
|
||
.panel-header--done {
|
||
background: #F9FAFB;
|
||
}
|
||
|
||
.panel-header--done .header-index {
|
||
color: #10B981;
|
||
}
|
||
|
||
.panel-header--todo .header-index,
|
||
.panel-header--todo .header-title {
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
/* Left Panel - Report Style */
|
||
.left-panel.report-style {
|
||
width: 45%;
|
||
min-width: 450px;
|
||
background: #FFFFFF;
|
||
border-right: 1px solid #E5E7EB;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 30px 50px 60px 50px;
|
||
}
|
||
|
||
.left-panel::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.left-panel::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.left-panel::-webkit-scrollbar-thumb {
|
||
background: transparent;
|
||
border-radius: 3px;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.left-panel:hover::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.left-panel::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
/* Report Header */
|
||
.report-content-wrapper {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
}
|
||
|
||
.report-header-block {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.report-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.report-tag {
|
||
background: #000000;
|
||
color: #FFFFFF;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
padding: 4px 8px;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.report-id {
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
font-weight: 500;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.main-title {
|
||
font-family: 'Times New Roman', Times, serif;
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
line-height: 1.2;
|
||
margin: 0 0 16px 0;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.sub-title {
|
||
font-family: 'Times New Roman', Times, serif;
|
||
font-size: 16px;
|
||
color: #6B7280;
|
||
font-style: italic;
|
||
line-height: 1.6;
|
||
margin: 0 0 30px 0;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.header-divider {
|
||
height: 1px;
|
||
background: #E5E7EB;
|
||
width: 100%;
|
||
}
|
||
|
||
/* Sections List */
|
||
.sections-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 32px;
|
||
}
|
||
|
||
.report-section-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.section-header-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
transition: background-color 0.2s ease;
|
||
padding: 8px 12px;
|
||
margin: -8px -12px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.section-header-row.clickable {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.section-header-row.clickable:hover {
|
||
background-color: #F9FAFB;
|
||
}
|
||
|
||
.collapse-icon {
|
||
margin-left: auto;
|
||
color: #9CA3AF;
|
||
transition: transform 0.3s ease;
|
||
flex-shrink: 0;
|
||
align-self: center;
|
||
}
|
||
|
||
.collapse-icon.is-collapsed {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.section-number {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 16px;
|
||
color: #9CA3AF; /* 深灰色,不随状态变化 */
|
||
font-weight: 500;
|
||
}
|
||
|
||
.section-title {
|
||
font-family: 'Times New Roman', Times, serif;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin: 0;
|
||
transition: color 0.3s ease;
|
||
}
|
||
|
||
/* States */
|
||
.report-section-item.is-pending .section-title {
|
||
color: #D1D5DB;
|
||
}
|
||
|
||
.report-section-item.is-active .section-title,
|
||
.report-section-item.is-completed .section-title {
|
||
color: #111827;
|
||
}
|
||
|
||
.section-body {
|
||
padding-left: 28px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Generated Content */
|
||
.generated-content {
|
||
font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: #374151;
|
||
}
|
||
|
||
.generated-content :deep(p) {
|
||
margin-bottom: 1em;
|
||
}
|
||
|
||
.generated-content :deep(.md-h2),
|
||
.generated-content :deep(.md-h3),
|
||
.generated-content :deep(.md-h4) {
|
||
font-family: 'Times New Roman', Times, serif;
|
||
color: #111827;
|
||
margin-top: 1.5em;
|
||
margin-bottom: 0.8em;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.generated-content :deep(.md-h2) { font-size: 20px; border-bottom: 1px solid #F3F4F6; padding-bottom: 8px; }
|
||
.generated-content :deep(.md-h3) { font-size: 18px; }
|
||
.generated-content :deep(.md-h4) { font-size: 16px; }
|
||
|
||
.generated-content :deep(.md-ul),
|
||
.generated-content :deep(.md-ol) {
|
||
padding-left: 24px;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.generated-content :deep(.md-li),
|
||
.generated-content :deep(.md-oli) {
|
||
margin: 6px 0;
|
||
}
|
||
|
||
.generated-content :deep(.md-quote) {
|
||
border-left: 3px solid #E5E7EB;
|
||
padding-left: 16px;
|
||
margin: 1.5em 0;
|
||
color: #6B7280;
|
||
font-style: italic;
|
||
font-family: 'Times New Roman', Times, serif;
|
||
}
|
||
|
||
.generated-content :deep(.code-block) {
|
||
background: #F9FAFB;
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 12px;
|
||
overflow-x: auto;
|
||
margin: 1em 0;
|
||
border: 1px solid #E5E7EB;
|
||
}
|
||
|
||
.generated-content :deep(strong) {
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
/* Loading State */
|
||
.loading-state {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
color: #6B7280;
|
||
font-size: 14px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.loading-icon {
|
||
width: 18px;
|
||
height: 18px;
|
||
animation: spin 1s linear infinite;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.loading-text {
|
||
font-family: 'Times New Roman', Times, serif;
|
||
font-size: 15px;
|
||
color: #4B5563;
|
||
}
|
||
|
||
.cursor-blink {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 14px;
|
||
background: #8B5CF6;
|
||
opacity: 0.5;
|
||
animation: blink 1s step-end infinite;
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 100% { opacity: 0.5; }
|
||
50% { opacity: 0; }
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Content Styles Override for this view */
|
||
.generated-content :deep(.md-h2) {
|
||
font-family: 'Times New Roman', Times, serif;
|
||
font-size: 18px;
|
||
margin-top: 0;
|
||
}
|
||
|
||
|
||
/* Slide Content Transition */
|
||
.slide-content-enter-active {
|
||
transition: opacity 0.3s ease-out;
|
||
}
|
||
|
||
.slide-content-leave-active {
|
||
transition: opacity 0.2s ease-in;
|
||
}
|
||
|
||
.slide-content-enter-from,
|
||
.slide-content-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.slide-content-enter-to,
|
||
.slide-content-leave-from {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Waiting Placeholder */
|
||
.waiting-placeholder {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
padding: 40px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
.waiting-animation {
|
||
position: relative;
|
||
width: 48px;
|
||
height: 48px;
|
||
}
|
||
|
||
.waiting-ring {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 2px solid #E5E7EB;
|
||
border-radius: 50%;
|
||
animation: ripple 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||
}
|
||
|
||
.waiting-ring:nth-child(2) {
|
||
animation-delay: 0.4s;
|
||
}
|
||
|
||
.waiting-ring:nth-child(3) {
|
||
animation-delay: 0.8s;
|
||
}
|
||
|
||
@keyframes ripple {
|
||
0% { transform: scale(0.5); opacity: 1; }
|
||
100% { transform: scale(2); opacity: 0; }
|
||
}
|
||
|
||
.waiting-text {
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* Right Panel */
|
||
.right-panel {
|
||
flex: 1;
|
||
background: #FFFFFF;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
/* Functional palette (low saturation, status-based) */
|
||
--wf-border: #E5E7EB;
|
||
--wf-divider: #F3F4F6;
|
||
|
||
--wf-active-bg: #FAFAFA;
|
||
--wf-active-border: #1F2937;
|
||
--wf-active-dot: #1F2937;
|
||
--wf-active-text: #1F2937;
|
||
|
||
--wf-done-bg: #F9FAFB;
|
||
--wf-done-border: #E5E7EB;
|
||
--wf-done-dot: #10B981;
|
||
|
||
--wf-muted-dot: #D1D5DB;
|
||
--wf-todo-text: #9CA3AF;
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar-thumb {
|
||
background: transparent;
|
||
border-radius: 3px;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.right-panel:hover::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.mono {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
/* Workflow Overview */
|
||
.workflow-overview {
|
||
padding: 16px 20px 0 20px;
|
||
}
|
||
|
||
.workflow-metrics {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.metric {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
}
|
||
|
||
.metric-right {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #9CA3AF;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 12px;
|
||
color: #374151;
|
||
}
|
||
|
||
.metric-pill {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--wf-border);
|
||
background: #F9FAFB;
|
||
color: #6B7280;
|
||
}
|
||
|
||
.metric-pill.pill--processing {
|
||
background: var(--wf-active-bg);
|
||
border-color: var(--wf-active-border);
|
||
color: var(--wf-active-text);
|
||
}
|
||
|
||
.metric-pill.pill--completed {
|
||
background: #ECFDF5;
|
||
border-color: #A7F3D0;
|
||
color: #065F46;
|
||
}
|
||
|
||
.metric-pill.pill--pending {
|
||
background: transparent;
|
||
border-style: dashed;
|
||
color: #6B7280;
|
||
}
|
||
|
||
.workflow-steps {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.wf-step {
|
||
display: grid;
|
||
grid-template-columns: 24px 1fr;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--wf-divider);
|
||
border-radius: 8px;
|
||
background: #FFFFFF;
|
||
}
|
||
|
||
.wf-step--active {
|
||
background: var(--wf-active-bg);
|
||
border-color: var(--wf-active-border);
|
||
}
|
||
|
||
.wf-step--done {
|
||
background: var(--wf-done-bg);
|
||
border-color: var(--wf-done-border);
|
||
}
|
||
|
||
.wf-step--todo {
|
||
background: transparent;
|
||
border-color: var(--wf-border);
|
||
border-style: dashed;
|
||
}
|
||
|
||
.wf-step-connector {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 24px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.wf-step-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
background: var(--wf-muted-dot);
|
||
border: 2px solid #FFFFFF;
|
||
z-index: 1;
|
||
}
|
||
|
||
.wf-step-line {
|
||
width: 2px;
|
||
flex: 1;
|
||
background: var(--wf-divider);
|
||
margin-top: -2px;
|
||
}
|
||
|
||
.wf-step--active .wf-step-dot {
|
||
background: var(--wf-active-dot);
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
||
}
|
||
|
||
.wf-step--done .wf-step-dot {
|
||
background: var(--wf-done-dot);
|
||
}
|
||
|
||
.wf-step-title-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.wf-step-index {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #9CA3AF;
|
||
letter-spacing: 0.02em;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.wf-step-title {
|
||
font-family: 'Times New Roman', Times, serif;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
line-height: 1.35;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.wf-step-meta {
|
||
margin-left: auto;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--wf-active-text);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.wf-step--todo .wf-step-title,
|
||
.wf-step--todo .wf-step-index {
|
||
color: var(--wf-todo-text);
|
||
}
|
||
|
||
.workflow-divider {
|
||
height: 1px;
|
||
background: var(--wf-divider);
|
||
margin: 14px 0 0 0;
|
||
}
|
||
|
||
/* Workflow Timeline */
|
||
.workflow-timeline {
|
||
padding: 14px 20px 24px;
|
||
flex: 1;
|
||
}
|
||
|
||
.timeline-item {
|
||
display: grid;
|
||
grid-template-columns: 24px 1fr;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 10px;
|
||
border: 1px solid var(--wf-divider);
|
||
border-radius: 8px;
|
||
background: #FFFFFF;
|
||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||
}
|
||
|
||
.timeline-item:hover {
|
||
background: #F9FAFB;
|
||
border-color: var(--wf-border);
|
||
}
|
||
|
||
.timeline-item.node--active {
|
||
background: var(--wf-active-bg);
|
||
border-color: var(--wf-active-border);
|
||
}
|
||
|
||
.timeline-item.node--active:hover {
|
||
background: var(--wf-active-bg);
|
||
border-color: var(--wf-active-border);
|
||
}
|
||
|
||
.timeline-item.node--done {
|
||
background: var(--wf-done-bg);
|
||
border-color: var(--wf-done-border);
|
||
}
|
||
|
||
.timeline-item.node--done:hover {
|
||
background: var(--wf-done-bg);
|
||
border-color: var(--wf-done-border);
|
||
}
|
||
|
||
.timeline-connector {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 24px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.connector-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
background: var(--wf-muted-dot);
|
||
border: 2px solid #FFFFFF;
|
||
z-index: 1;
|
||
}
|
||
|
||
.connector-line {
|
||
width: 2px;
|
||
flex: 1;
|
||
background: var(--wf-divider);
|
||
margin-top: -2px;
|
||
}
|
||
|
||
/* Connector dot: status only */
|
||
.dot-active {
|
||
background: var(--wf-active-dot);
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
||
}
|
||
|
||
.dot-done {
|
||
background: var(--wf-done-dot);
|
||
}
|
||
|
||
.dot-muted {
|
||
background: var(--wf-muted-dot);
|
||
}
|
||
|
||
.timeline-content {
|
||
min-width: 0;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
transition: none;
|
||
}
|
||
|
||
.timeline-content:hover {
|
||
box-shadow: none;
|
||
}
|
||
|
||
.timeline-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.action-label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
.action-time {
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
.timeline-body {
|
||
font-size: 13px;
|
||
color: #4B5563;
|
||
}
|
||
|
||
.timeline-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid #F3F4F6;
|
||
}
|
||
|
||
.elapsed-placeholder {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.footer-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.elapsed-badge {
|
||
font-size: 11px;
|
||
color: #6B7280;
|
||
background: #F3F4F6;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
/* Timeline Body Elements */
|
||
.info-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.info-key {
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.info-val {
|
||
color: #374151;
|
||
}
|
||
|
||
.status-message {
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.status-message.planning {
|
||
background: var(--wf-active-bg);
|
||
border-color: var(--wf-active-border);
|
||
color: var(--wf-active-text);
|
||
}
|
||
|
||
.status-message.success {
|
||
background: #ECFDF5;
|
||
border-color: #A7F3D0;
|
||
color: #065F46;
|
||
}
|
||
|
||
.outline-badge {
|
||
display: inline-block;
|
||
margin-top: 8px;
|
||
padding: 4px 10px;
|
||
background: #F9FAFB;
|
||
color: #6B7280;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.section-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid var(--wf-border);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.section-tag.content-ready {
|
||
background: var(--wf-active-bg);
|
||
border: 1px dashed var(--wf-active-border);
|
||
}
|
||
|
||
.section-tag.content-ready svg {
|
||
color: var(--wf-active-dot);
|
||
}
|
||
|
||
|
||
.section-tag.completed {
|
||
background: #ECFDF5;
|
||
border: 1px solid #A7F3D0;
|
||
}
|
||
|
||
.section-tag.completed svg {
|
||
color: #059669;
|
||
}
|
||
|
||
.tag-num {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #6B7280;
|
||
}
|
||
|
||
.section-tag.completed .tag-num {
|
||
color: #059669;
|
||
}
|
||
|
||
.tag-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
.tool-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
background: #F9FAFB;
|
||
color: #374151;
|
||
border: 1px solid var(--wf-border);
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.tool-icon {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Tool Colors - Purple (Deep Insight) */
|
||
.tool-badge.tool-purple {
|
||
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
|
||
border-color: #C4B5FD;
|
||
color: #6D28D9;
|
||
}
|
||
.tool-badge.tool-purple .tool-icon {
|
||
stroke: #7C3AED;
|
||
}
|
||
|
||
/* Tool Colors - Blue (Panorama Search) */
|
||
.tool-badge.tool-blue {
|
||
background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
|
||
border-color: #93C5FD;
|
||
color: #1D4ED8;
|
||
}
|
||
.tool-badge.tool-blue .tool-icon {
|
||
stroke: #2563EB;
|
||
}
|
||
|
||
/* Tool Colors - Green (Agent Interview) */
|
||
.tool-badge.tool-green {
|
||
background: linear-gradient(135deg, #F0FDF4 0%, #DCFCE7 100%);
|
||
border-color: #86EFAC;
|
||
color: #15803D;
|
||
}
|
||
.tool-badge.tool-green .tool-icon {
|
||
stroke: #16A34A;
|
||
}
|
||
|
||
/* Tool Colors - Orange (Quick Search) */
|
||
.tool-badge.tool-orange {
|
||
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
|
||
border-color: #FDBA74;
|
||
color: #C2410C;
|
||
}
|
||
.tool-badge.tool-orange .tool-icon {
|
||
stroke: #EA580C;
|
||
}
|
||
|
||
/* Tool Colors - Cyan (Graph Stats) */
|
||
.tool-badge.tool-cyan {
|
||
background: linear-gradient(135deg, #ECFEFF 0%, #CFFAFE 100%);
|
||
border-color: #67E8F9;
|
||
color: #0E7490;
|
||
}
|
||
.tool-badge.tool-cyan .tool-icon {
|
||
stroke: #0891B2;
|
||
}
|
||
|
||
/* Tool Colors - Pink (Entity Query) */
|
||
.tool-badge.tool-pink {
|
||
background: linear-gradient(135deg, #FDF2F8 0%, #FCE7F3 100%);
|
||
border-color: #F9A8D4;
|
||
color: #BE185D;
|
||
}
|
||
.tool-badge.tool-pink .tool-icon {
|
||
stroke: #DB2777;
|
||
}
|
||
|
||
/* Tool Colors - Gray (Default) */
|
||
.tool-badge.tool-gray {
|
||
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
|
||
border-color: #D1D5DB;
|
||
color: #374151;
|
||
}
|
||
.tool-badge.tool-gray .tool-icon {
|
||
stroke: #6B7280;
|
||
}
|
||
|
||
.tool-params {
|
||
margin-top: 10px;
|
||
background: transparent;
|
||
border-radius: 0;
|
||
padding: 10px 0 0 0;
|
||
border-top: 1px dashed var(--wf-divider);
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.tool-params pre {
|
||
margin: 0;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
color: #4B5563;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
padding: 10px;
|
||
}
|
||
|
||
/* Unified Action Buttons */
|
||
.action-btn {
|
||
background: #F3F4F6;
|
||
border: 1px solid #E5E7EB;
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #6B7280;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
background: #E5E7EB;
|
||
color: #374151;
|
||
border-color: #D1D5DB;
|
||
}
|
||
|
||
/* Result Wrapper */
|
||
.result-wrapper {
|
||
background: transparent;
|
||
border: none;
|
||
border-top: 1px solid var(--wf-divider);
|
||
border-radius: 0;
|
||
padding: 12px 0 0 0;
|
||
}
|
||
|
||
.result-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.result-tool {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.result-size {
|
||
font-size: 10px;
|
||
color: #6B7280;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
.result-raw {
|
||
margin-top: 10px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.result-raw pre {
|
||
margin: 0;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
color: #374151;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.raw-preview {
|
||
margin: 0;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
color: #6B7280;
|
||
}
|
||
|
||
/* Legacy toggle-raw removed - using unified .action-btn */
|
||
|
||
/* LLM Response */
|
||
.llm-meta {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.meta-tag {
|
||
font-size: 11px;
|
||
padding: 3px 8px;
|
||
background: #F3F4F6;
|
||
color: #6B7280;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.meta-tag.active {
|
||
background: #DBEAFE;
|
||
color: #1E40AF;
|
||
}
|
||
|
||
.meta-tag.final-answer {
|
||
background: #D1FAE5;
|
||
color: #059669;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.final-answer-hint {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
padding: 10px 14px;
|
||
background: #ECFDF5;
|
||
border: 1px solid #A7F3D0;
|
||
border-radius: 6px;
|
||
color: #065F46;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.final-answer-hint svg {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.llm-content {
|
||
margin-top: 10px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.llm-content pre {
|
||
margin: 0;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
color: #4B5563;
|
||
background: #F3F4F6;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
/* Complete Banner */
|
||
.complete-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 16px;
|
||
background: #ECFDF5;
|
||
border: 1px solid #A7F3D0;
|
||
border-radius: 8px;
|
||
color: #065F46;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.next-step-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
width: calc(100% - 40px);
|
||
margin: 4px 20px 0 20px;
|
||
padding: 14px 20px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
background: #1F2937;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.next-step-btn:hover {
|
||
background: #374151;
|
||
}
|
||
|
||
.next-step-btn svg {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.next-step-btn:hover svg {
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
/* Workflow Empty */
|
||
.workflow-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60px 20px;
|
||
color: #9CA3AF;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.empty-pulse {
|
||
width: 24px;
|
||
height: 24px;
|
||
background: #E5E7EB;
|
||
border-radius: 50%;
|
||
margin-bottom: 16px;
|
||
animation: pulse-ring 1.5s infinite;
|
||
}
|
||
|
||
@keyframes pulse-ring {
|
||
0%, 100% { transform: scale(1); opacity: 1; }
|
||
50% { transform: scale(1.2); opacity: 0.5; }
|
||
}
|
||
|
||
/* Timeline Transitions */
|
||
.timeline-item-enter-active {
|
||
transition: all 0.4s ease;
|
||
}
|
||
|
||
.timeline-item-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(-20px);
|
||
}
|
||
|
||
/* ========== Structured Result Display Components ========== */
|
||
|
||
/* Common Styles - using :deep() for dynamic components */
|
||
:deep(.stat-row) {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
:deep(.stat-box) {
|
||
flex: 1;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
padding: 10px 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.stat-box .stat-num) {
|
||
display: block;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
:deep(.stat-box .stat-label) {
|
||
display: block;
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
margin-top: 2px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
:deep(.stat-box.highlight) {
|
||
background: #ECFDF5;
|
||
border-color: #A7F3D0;
|
||
}
|
||
|
||
:deep(.stat-box.highlight .stat-num) {
|
||
color: #059669;
|
||
}
|
||
|
||
:deep(.stat-box.muted) {
|
||
background: #F9FAFB;
|
||
border-color: #E5E7EB;
|
||
}
|
||
|
||
:deep(.stat-box.muted .stat-num) {
|
||
color: #6B7280;
|
||
}
|
||
|
||
:deep(.query-display) {
|
||
background: #F9FAFB;
|
||
padding: 10px 14px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
color: #374151;
|
||
margin-bottom: 12px;
|
||
border: 1px solid #E5E7EB;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
:deep(.expand-details) {
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
padding: 8px 14px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #6B7280;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
:deep(.expand-details:hover) {
|
||
border-color: #D1D5DB;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.detail-content) {
|
||
margin-top: 14px;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 8px;
|
||
padding: 14px;
|
||
}
|
||
|
||
:deep(.section-label) {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #6B7280;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
margin-bottom: 10px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
}
|
||
|
||
/* Facts Section */
|
||
:deep(.facts-section) {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
:deep(.fact-row) {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
}
|
||
|
||
:deep(.fact-row:last-child) {
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.fact-row.active) {
|
||
background: #ECFDF5;
|
||
margin: 0 -10px;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.fact-idx) {
|
||
min-width: 22px;
|
||
height: 22px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #F3F4F6;
|
||
border-radius: 6px;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: #6B7280;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
:deep(.fact-row.active .fact-idx) {
|
||
background: #A7F3D0;
|
||
color: #065F46;
|
||
}
|
||
|
||
:deep(.fact-text) {
|
||
font-size: 12px;
|
||
color: #4B5563;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Entities Section */
|
||
:deep(.entities-section) {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
:deep(.entity-chips) {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.entity-chip) {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
padding: 6px 12px;
|
||
}
|
||
|
||
:deep(.chip-name) {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
}
|
||
|
||
:deep(.chip-type) {
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
background: #E5E7EB;
|
||
padding: 1px 6px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Relations Section */
|
||
:deep(.relations-section) {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
:deep(.relation-row) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 0;
|
||
flex-wrap: wrap;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
}
|
||
|
||
:deep(.relation-row:last-child) {
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.rel-node) {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
background: #F3F4F6;
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
:deep(.rel-edge) {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: #FFFFFF;
|
||
background: #4F46E5;
|
||
padding: 3px 10px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
/* ========== Interview Display - Conversation Style ========== */
|
||
:deep(.interview-display) {
|
||
padding: 0;
|
||
}
|
||
|
||
/* Header */
|
||
:deep(.interview-display .interview-header) {
|
||
padding: 0;
|
||
background: transparent;
|
||
border-bottom: none;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.interview-display .header-main) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
:deep(.interview-display .header-title) {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
:deep(.interview-display .header-stats) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
:deep(.interview-display .stat-item) {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
}
|
||
|
||
:deep(.interview-display .stat-value) {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #4F46E5;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
:deep(.interview-display .stat-label) {
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
text-transform: lowercase;
|
||
}
|
||
|
||
:deep(.interview-display .stat-divider) {
|
||
color: #D1D5DB;
|
||
font-size: 12px;
|
||
}
|
||
|
||
:deep(.interview-display .stat-size) {
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
:deep(.interview-display .header-topic) {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: #6B7280;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Agent Tabs - Card Style */
|
||
:deep(.interview-display .agent-tabs) {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 0 0 14px 0;
|
||
background: transparent;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: #E5E7EB transparent;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tabs::-webkit-scrollbar) {
|
||
height: 4px;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tabs::-webkit-scrollbar-track) {
|
||
background: transparent;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tabs::-webkit-scrollbar-thumb) {
|
||
background: #E5E7EB;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tabs::-webkit-scrollbar-thumb:hover) {
|
||
background: #D1D5DB;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tab) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #6B7280;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tab:hover) {
|
||
background: #F3F4F6;
|
||
border-color: #D1D5DB;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tab.active) {
|
||
background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%);
|
||
border-color: #A5B4FC;
|
||
color: #4338CA;
|
||
box-shadow: 0 1px 2px rgba(99, 102, 241, 0.1);
|
||
}
|
||
|
||
:deep(.interview-display .tab-avatar) {
|
||
width: 18px;
|
||
height: 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #E5E7EB;
|
||
color: #6B7280;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tab:hover .tab-avatar) {
|
||
background: #D1D5DB;
|
||
}
|
||
|
||
:deep(.interview-display .agent-tab.active .tab-avatar) {
|
||
background: #6366F1;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
:deep(.interview-display .tab-name) {
|
||
max-width: 100px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* Interview Detail */
|
||
:deep(.interview-display .interview-detail) {
|
||
padding: 12px 0;
|
||
background: transparent;
|
||
}
|
||
|
||
/* Agent Profile - No card */
|
||
:deep(.interview-display .agent-profile) {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 0;
|
||
background: transparent;
|
||
border: none;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.interview-display .profile-avatar) {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #E5E7EB;
|
||
color: #6B7280;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
:deep(.interview-display .profile-info) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
:deep(.interview-display .profile-name) {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
:deep(.interview-display .profile-role) {
|
||
font-size: 11px;
|
||
color: #6B7280;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
:deep(.interview-display .profile-bio) {
|
||
font-size: 11px;
|
||
color: #9CA3AF;
|
||
line-height: 1.4;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Selection Reason - 选择理由 */
|
||
:deep(.interview-display .selection-reason) {
|
||
background: #F8FAFC;
|
||
border: 1px solid #E2E8F0;
|
||
border-radius: 8px;
|
||
padding: 12px 14px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.interview-display .reason-label) {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #64748B;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
:deep(.interview-display .reason-content) {
|
||
font-size: 12px;
|
||
color: #475569;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Q&A Thread - Clean list */
|
||
:deep(.interview-display .qa-thread) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
:deep(.interview-display .qa-pair) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 0;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 0;
|
||
}
|
||
|
||
:deep(.interview-display .qa-question),
|
||
:deep(.interview-display .qa-answer) {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
:deep(.interview-display .qa-badge) {
|
||
width: 20px;
|
||
height: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
:deep(.interview-display .q-badge) {
|
||
background: transparent;
|
||
color: #9CA3AF;
|
||
border: 1px solid #E5E7EB;
|
||
}
|
||
|
||
:deep(.interview-display .a-badge) {
|
||
background: #4F46E5;
|
||
color: #FFFFFF;
|
||
border: 1px solid #4F46E5;
|
||
}
|
||
|
||
:deep(.interview-display .qa-content) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
:deep(.interview-display .qa-sender) {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #9CA3AF;
|
||
margin-bottom: 4px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
:deep(.interview-display .qa-text) {
|
||
font-size: 13px;
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
:deep(.interview-display .qa-answer) {
|
||
background: transparent;
|
||
padding: 0;
|
||
border: none;
|
||
margin-top: 0;
|
||
}
|
||
|
||
:deep(.interview-display .answer-placeholder) {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
:deep(.interview-display .placeholder-text) {
|
||
font-style: italic;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:deep(.interview-display .qa-answer-header) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* Platform Switch */
|
||
:deep(.interview-display .platform-switch) {
|
||
display: flex;
|
||
gap: 2px;
|
||
background: transparent;
|
||
padding: 0;
|
||
border-radius: 0;
|
||
}
|
||
|
||
:deep(.interview-display .platform-btn) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 2px 6px;
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: 500;
|
||
color: #9CA3AF;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
:deep(.interview-display .platform-btn:hover) {
|
||
color: #6B7280;
|
||
}
|
||
|
||
:deep(.interview-display .platform-btn.active) {
|
||
background: transparent;
|
||
color: #4F46E5;
|
||
border-color: #E5E7EB;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.interview-display .platform-icon) {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
:deep(.interview-display .answer-text) {
|
||
font-size: 13px;
|
||
color: #111827;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
:deep(.interview-display .answer-text strong) {
|
||
color: #111827;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.interview-display .expand-answer-btn) {
|
||
display: inline-block;
|
||
margin-top: 8px;
|
||
padding: 0;
|
||
background: transparent;
|
||
border: none;
|
||
border-bottom: 1px dotted #D1D5DB;
|
||
border-radius: 0;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #9CA3AF;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
:deep(.interview-display .expand-answer-btn:hover) {
|
||
background: transparent;
|
||
color: #6B7280;
|
||
border-bottom-style: solid;
|
||
}
|
||
|
||
/* Quotes Section - Clean list */
|
||
:deep(.interview-display .quotes-section) {
|
||
background: transparent;
|
||
border: none;
|
||
border-top: 1px solid #F3F4F6;
|
||
border-radius: 0;
|
||
padding: 16px 0 0 0;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
:deep(.interview-display .quotes-header) {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #9CA3AF;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
:deep(.interview-display .quotes-list) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
:deep(.interview-display .quote-item) {
|
||
margin: 0;
|
||
padding: 10px 12px;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-style: italic;
|
||
color: #4B5563;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Summary Section */
|
||
:deep(.interview-display .summary-section) {
|
||
margin-top: 20px;
|
||
padding: 16px 0 0 0;
|
||
background: transparent;
|
||
border: none;
|
||
border-top: 1px solid #F3F4F6;
|
||
border-radius: 0;
|
||
}
|
||
|
||
:deep(.interview-display .summary-header) {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #9CA3AF;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content) {
|
||
font-size: 13px;
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Markdown styles in summary */
|
||
:deep(.interview-display .summary-content h2),
|
||
:deep(.interview-display .summary-content h3),
|
||
:deep(.interview-display .summary-content h4),
|
||
:deep(.interview-display .summary-content h5) {
|
||
margin: 12px 0 8px 0;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content h2) {
|
||
font-size: 15px;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content h3) {
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content h4),
|
||
:deep(.interview-display .summary-content h5) {
|
||
font-size: 13px;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content p) {
|
||
margin: 8px 0;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content strong) {
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content em) {
|
||
font-style: italic;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content ul),
|
||
:deep(.interview-display .summary-content ol) {
|
||
margin: 8px 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content li) {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
:deep(.interview-display .summary-content blockquote) {
|
||
margin: 8px 0;
|
||
padding-left: 12px;
|
||
border-left: 3px solid #E5E7EB;
|
||
color: #6B7280;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* Markdown styles in quotes */
|
||
:deep(.interview-display .quote-item strong) {
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.interview-display .quote-item em) {
|
||
font-style: italic;
|
||
}
|
||
|
||
/* ========== Enhanced Insight Display Styles ========== */
|
||
:deep(.insight-display) {
|
||
padding: 0;
|
||
}
|
||
|
||
:deep(.insight-header) {
|
||
padding: 12px 16px;
|
||
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
|
||
border-radius: 8px 8px 0 0;
|
||
border: 1px solid #C4B5FD;
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.insight-header .header-main) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.insight-header .header-title) {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #6D28D9;
|
||
}
|
||
|
||
:deep(.insight-header .header-stats) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
:deep(.insight-header .stat-item) {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 2px;
|
||
}
|
||
|
||
:deep(.insight-header .stat-value) {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 700;
|
||
color: #7C3AED;
|
||
}
|
||
|
||
:deep(.insight-header .stat-label) {
|
||
color: #8B5CF6;
|
||
font-size: 10px;
|
||
}
|
||
|
||
:deep(.insight-header .stat-divider) {
|
||
color: #C4B5FD;
|
||
margin: 0 4px;
|
||
}
|
||
|
||
:deep(.insight-header .stat-size) {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:deep(.insight-header .header-topic) {
|
||
font-size: 13px;
|
||
color: #5B21B6;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
:deep(.insight-header .header-scenario) {
|
||
margin-top: 6px;
|
||
font-size: 11px;
|
||
color: #7C3AED;
|
||
}
|
||
|
||
:deep(.insight-header .scenario-label) {
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.insight-tabs) {
|
||
display: flex;
|
||
gap: 2px;
|
||
padding: 8px 12px;
|
||
background: #FAFAFA;
|
||
border: 1px solid #E5E7EB;
|
||
border-top: none;
|
||
}
|
||
|
||
:deep(.insight-tab) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 6px 10px;
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #6B7280;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
:deep(.insight-tab:hover) {
|
||
background: #F3F4F6;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.insight-tab.active) {
|
||
background: #FFFFFF;
|
||
color: #7C3AED;
|
||
border-color: #C4B5FD;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
|
||
:deep(.insight-content) {
|
||
padding: 12px;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-top: none;
|
||
border-radius: 0 0 8px 8px;
|
||
}
|
||
|
||
:deep(.insight-display .panel-header) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
}
|
||
|
||
:deep(.insight-display .panel-title) {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.insight-display .panel-count) {
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:deep(.insight-display .facts-list),
|
||
:deep(.insight-display .relations-list),
|
||
:deep(.insight-display .subqueries-list) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.insight-display .entities-grid) {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
:deep(.insight-display .fact-item) {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.insight-display .fact-number) {
|
||
flex-shrink: 0;
|
||
width: 20px;
|
||
height: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #E5E7EB;
|
||
border-radius: 50%;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: #6B7280;
|
||
}
|
||
|
||
:deep(.insight-display .fact-content) {
|
||
flex: 1;
|
||
font-size: 12px;
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Entity Tag Styles - Compact multi-column layout */
|
||
:deep(.insight-display .entity-tag) {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 4px 8px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
cursor: default;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
:deep(.insight-display .entity-tag:hover) {
|
||
background: #F3F4F6;
|
||
border-color: #D1D5DB;
|
||
}
|
||
|
||
:deep(.insight-display .entity-tag .entity-name) {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
}
|
||
|
||
:deep(.insight-display .entity-tag .entity-type) {
|
||
font-size: 9px;
|
||
color: #7C3AED;
|
||
background: #EDE9FE;
|
||
padding: 1px 4px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
:deep(.insight-display .entity-tag .entity-fact-count) {
|
||
font-size: 9px;
|
||
color: #9CA3AF;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
/* Legacy entity card styles for backwards compatibility */
|
||
:deep(.insight-display .entity-card) {
|
||
padding: 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
:deep(.insight-display .entity-header) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
:deep(.insight-display .entity-info) {
|
||
flex: 1;
|
||
}
|
||
|
||
:deep(.insight-display .entity-card .entity-name) {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
:deep(.insight-display .entity-card .entity-type) {
|
||
font-size: 10px;
|
||
color: #7C3AED;
|
||
background: #EDE9FE;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
:deep(.insight-display .entity-card .entity-fact-count) {
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
background: #F3F4F6;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
:deep(.insight-display .entity-summary) {
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #E5E7EB;
|
||
font-size: 11px;
|
||
color: #6B7280;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Relation Item Styles */
|
||
:deep(.insight-display .relation-item) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.insight-display .rel-source),
|
||
:deep(.insight-display .rel-target) {
|
||
padding: 4px 8px;
|
||
background: #FFFFFF;
|
||
border: 1px solid #D1D5DB;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.insight-display .rel-arrow) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex: 1;
|
||
}
|
||
|
||
:deep(.insight-display .rel-line) {
|
||
flex: 1;
|
||
height: 1px;
|
||
background: #D1D5DB;
|
||
}
|
||
|
||
:deep(.insight-display .rel-label) {
|
||
padding: 2px 6px;
|
||
background: #EDE9FE;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: 500;
|
||
color: #7C3AED;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Sub-query Styles */
|
||
:deep(.insight-display .subquery-item) {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.insight-display .subquery-number) {
|
||
flex-shrink: 0;
|
||
padding: 2px 6px;
|
||
background: #7C3AED;
|
||
border-radius: 4px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
:deep(.insight-display .subquery-text) {
|
||
font-size: 12px;
|
||
color: #374151;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Expand Button */
|
||
:deep(.insight-display .expand-btn),
|
||
:deep(.panorama-display .expand-btn),
|
||
:deep(.quick-search-display .expand-btn) {
|
||
display: block;
|
||
width: 100%;
|
||
margin-top: 12px;
|
||
padding: 8px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #6B7280;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.insight-display .expand-btn:hover),
|
||
:deep(.panorama-display .expand-btn:hover),
|
||
:deep(.quick-search-display .expand-btn:hover) {
|
||
background: #F3F4F6;
|
||
color: #374151;
|
||
border-color: #D1D5DB;
|
||
}
|
||
|
||
/* Empty State */
|
||
:deep(.insight-display .empty-state),
|
||
:deep(.panorama-display .empty-state),
|
||
:deep(.quick-search-display .empty-state) {
|
||
padding: 24px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
/* ========== Enhanced Panorama Display Styles ========== */
|
||
:deep(.panorama-display) {
|
||
padding: 0;
|
||
}
|
||
|
||
:deep(.panorama-header) {
|
||
padding: 12px 16px;
|
||
background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
|
||
border-radius: 8px 8px 0 0;
|
||
border: 1px solid #93C5FD;
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.panorama-header .header-main) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.panorama-header .header-title) {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #1D4ED8;
|
||
}
|
||
|
||
:deep(.panorama-header .header-stats) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
:deep(.panorama-header .stat-item) {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 2px;
|
||
}
|
||
|
||
:deep(.panorama-header .stat-value) {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 700;
|
||
color: #2563EB;
|
||
}
|
||
|
||
:deep(.panorama-header .stat-label) {
|
||
color: #60A5FA;
|
||
font-size: 10px;
|
||
}
|
||
|
||
:deep(.panorama-header .stat-divider) {
|
||
color: #93C5FD;
|
||
margin: 0 4px;
|
||
}
|
||
|
||
:deep(.panorama-header .stat-size) {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:deep(.panorama-header .header-topic) {
|
||
font-size: 13px;
|
||
color: #1E40AF;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
:deep(.panorama-tabs) {
|
||
display: flex;
|
||
gap: 2px;
|
||
padding: 8px 12px;
|
||
background: #FAFAFA;
|
||
border: 1px solid #E5E7EB;
|
||
border-top: none;
|
||
}
|
||
|
||
:deep(.panorama-tab) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 6px 10px;
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #6B7280;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
:deep(.panorama-tab:hover) {
|
||
background: #F3F4F6;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.panorama-tab.active) {
|
||
background: #FFFFFF;
|
||
color: #2563EB;
|
||
border-color: #93C5FD;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
|
||
:deep(.panorama-content) {
|
||
padding: 12px;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-top: none;
|
||
border-radius: 0 0 8px 8px;
|
||
}
|
||
|
||
:deep(.panorama-display .panel-header) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
}
|
||
|
||
:deep(.panorama-display .panel-title) {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.panorama-display .panel-count) {
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:deep(.panorama-display .facts-list) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-item) {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-item.active) {
|
||
background: #F9FAFB;
|
||
border-color: #E5E7EB;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-item.historical) {
|
||
background: #F9FAFB;
|
||
border-color: #E5E7EB;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-number) {
|
||
flex-shrink: 0;
|
||
width: 20px;
|
||
height: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #E5E7EB;
|
||
border-radius: 50%;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: #6B7280;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-item.active .fact-number) {
|
||
background: #E5E7EB;
|
||
color: #6B7280;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-item.historical .fact-number) {
|
||
background: #9CA3AF;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-content) {
|
||
flex: 1;
|
||
font-size: 12px;
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-time) {
|
||
display: block;
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
margin-bottom: 4px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
:deep(.panorama-display .fact-text) {
|
||
display: block;
|
||
}
|
||
|
||
/* Entities Grid */
|
||
:deep(.panorama-display .entities-grid) {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.panorama-display .entity-tag) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.panorama-display .entity-name) {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.panorama-display .entity-type) {
|
||
font-size: 10px;
|
||
color: #2563EB;
|
||
background: #DBEAFE;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* ========== Enhanced Quick Search Display Styles ========== */
|
||
:deep(.quick-search-display) {
|
||
padding: 0;
|
||
}
|
||
|
||
:deep(.quicksearch-header) {
|
||
padding: 12px 16px;
|
||
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
|
||
border-radius: 8px 8px 0 0;
|
||
border: 1px solid #FDBA74;
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.quicksearch-header .header-main) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.quicksearch-header .header-title) {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #C2410C;
|
||
}
|
||
|
||
:deep(.quicksearch-header .header-stats) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
:deep(.quicksearch-header .stat-item) {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 2px;
|
||
}
|
||
|
||
:deep(.quicksearch-header .stat-value) {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 700;
|
||
color: #EA580C;
|
||
}
|
||
|
||
:deep(.quicksearch-header .stat-label) {
|
||
color: #FB923C;
|
||
font-size: 10px;
|
||
}
|
||
|
||
:deep(.quicksearch-header .stat-divider) {
|
||
color: #FDBA74;
|
||
margin: 0 4px;
|
||
}
|
||
|
||
:deep(.quicksearch-header .stat-size) {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:deep(.quicksearch-header .header-query) {
|
||
font-size: 13px;
|
||
color: #9A3412;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
:deep(.quicksearch-header .query-label) {
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.quicksearch-tabs) {
|
||
display: flex;
|
||
gap: 2px;
|
||
padding: 8px 12px;
|
||
background: #FAFAFA;
|
||
border: 1px solid #E5E7EB;
|
||
border-top: none;
|
||
}
|
||
|
||
:deep(.quicksearch-tab) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 6px 10px;
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #6B7280;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
:deep(.quicksearch-tab:hover) {
|
||
background: #F3F4F6;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.quicksearch-tab.active) {
|
||
background: #FFFFFF;
|
||
color: #EA580C;
|
||
border-color: #FDBA74;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
|
||
:deep(.quicksearch-content) {
|
||
padding: 12px;
|
||
background: #FFFFFF;
|
||
border: 1px solid #E5E7EB;
|
||
border-top: none;
|
||
border-radius: 0 0 8px 8px;
|
||
}
|
||
|
||
/* When there are no tabs, content connects directly to header */
|
||
:deep(.quicksearch-content.no-tabs) {
|
||
border-top: none;
|
||
}
|
||
|
||
:deep(.quick-search-display .panel-header) {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
}
|
||
|
||
:deep(.quick-search-display .panel-title) {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.quick-search-display .panel-count) {
|
||
font-size: 10px;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
:deep(.quick-search-display .facts-list) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.quick-search-display .fact-item) {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.quick-search-display .fact-item.active) {
|
||
background: #F9FAFB;
|
||
border-color: #E5E7EB;
|
||
}
|
||
|
||
:deep(.quick-search-display .fact-number) {
|
||
flex-shrink: 0;
|
||
width: 20px;
|
||
height: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #E5E7EB;
|
||
border-radius: 50%;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: #6B7280;
|
||
}
|
||
|
||
:deep(.quick-search-display .fact-item.active .fact-number) {
|
||
background: #E5E7EB;
|
||
color: #6B7280;
|
||
}
|
||
|
||
:deep(.quick-search-display .fact-content) {
|
||
flex: 1;
|
||
font-size: 12px;
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Edges Panel */
|
||
:deep(.quick-search-display .edges-list) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.quick-search-display .edge-item) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.quick-search-display .edge-source),
|
||
:deep(.quick-search-display .edge-target) {
|
||
padding: 4px 8px;
|
||
background: #FFFFFF;
|
||
border: 1px solid #D1D5DB;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.quick-search-display .edge-arrow) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex: 1;
|
||
}
|
||
|
||
:deep(.quick-search-display .edge-line) {
|
||
flex: 1;
|
||
height: 1px;
|
||
background: #D1D5DB;
|
||
}
|
||
|
||
:deep(.quick-search-display .edge-label) {
|
||
padding: 2px 6px;
|
||
background: #FFEDD5;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: 500;
|
||
color: #C2410C;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Nodes Grid */
|
||
:deep(.quick-search-display .nodes-grid) {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.quick-search-display .node-tag) {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
background: #F9FAFB;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.quick-search-display .node-name) {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.quick-search-display .node-type) {
|
||
font-size: 10px;
|
||
color: #EA580C;
|
||
background: #FFEDD5;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* Console Logs - 与 Step3Simulation.vue 保持一致 */
|
||
.console-logs {
|
||
background: #000;
|
||
color: #DDD;
|
||
padding: 16px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
border-top: 1px solid #222;
|
||
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: 4px;
|
||
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: #BBB;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.log-msg.error { color: #EF5350; }
|
||
.log-msg.warning { color: #FFA726; }
|
||
.log-msg.success { color: #66BB6A; }
|
||
</style>
|