feat(ssa): Implement dual-channel architecture Phase 1-3 (QPER + LLM Agent pipeline)
Completed: - Phase 1: DB schema (execution_mode + ssa_agent_executions), ModeToggle component, Session PATCH API - Phase 2: AgentPlannerService + AgentCoderService (streaming) + CodeRunnerService + R Docker /execute-code endpoint - Phase 3: AgentCodePanel (3-step confirmation UI), SSE event handling (7 agent events), streaming code display - Three-step confirmation pipeline: plan -> user confirm -> stream code -> user confirm -> execute R code -> results - R Docker sandbox /execute-code endpoint with 120s timeout + block_helpers preloaded - E2E dual-channel test script (8 tests) - Updated R engine architecture doc (v1.5) and SSA module status doc (v4.0) Technical details: - AgentCoderService uses LLM streaming (chatStream) for real-time code generation feedback - ReviewerAgent temporarily disabled, prioritizing Plan -> Code -> Execute flow - CodeRunnerService wraps user code with auto data loading (df variable injection) - Frontend handles agent_planning, agent_plan_ready, code_generating, code_generated, code_executing, code_result events - ask_user mechanism used for plan and code confirmation steps Files: 24 files (4 new services, 2 new components, 1 migration, 1 E2E test, 16 modified) Made-with: Cursor
This commit is contained in:
@@ -969,9 +969,7 @@
|
||||
.message-bubble .markdown-content h2:first-child,
|
||||
.message-bubble .markdown-content h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message-bubble .markdown-content h1 {
|
||||
}.message-bubble .markdown-content h1 {
|
||||
font-size: 1.3em;
|
||||
}.message-bubble .markdown-content h2 {
|
||||
font-size: 1.2em;
|
||||
@@ -1004,4 +1002,4 @@
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
164
frontend-v2/src/modules/ssa/components/AgentCodePanel.tsx
Normal file
164
frontend-v2/src/modules/ssa/components/AgentCodePanel.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* AgentCodePanel — Agent 通道工作区面板
|
||||
*
|
||||
* 分步展示:计划 → 流式代码生成 → 执行结果
|
||||
* 在 Agent 模式下替代 WorkflowTimeline。
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
Code,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
FileText,
|
||||
Play,
|
||||
} from 'lucide-react';
|
||||
import { useSSAStore } from '../stores/ssaStore';
|
||||
import type { AgentExecutionStatus } from '../types';
|
||||
|
||||
const STATUS_LABEL: Record<AgentExecutionStatus, string> = {
|
||||
pending: '等待中',
|
||||
planning: '制定计划中...',
|
||||
plan_pending: '等待确认计划',
|
||||
coding: '生成 R 代码中...',
|
||||
code_pending: '等待确认执行',
|
||||
executing: '执行 R 代码中...',
|
||||
completed: '执行完成',
|
||||
error: '执行出错',
|
||||
};
|
||||
|
||||
const StatusBadge: React.FC<{ status: AgentExecutionStatus }> = ({ status }) => {
|
||||
const spinning = ['planning', 'coding', 'executing'].includes(status);
|
||||
const waiting = ['plan_pending', 'code_pending'].includes(status);
|
||||
return (
|
||||
<span className={`agent-code-status ${status}`}>
|
||||
{spinning && <Loader2 size={12} className="spin" />}
|
||||
{waiting && <Play size={12} />}
|
||||
{status === 'completed' && <CheckCircle size={12} />}
|
||||
{status === 'error' && <XCircle size={12} />}
|
||||
{STATUS_LABEL[status]}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentCodePanel: React.FC = () => {
|
||||
const { agentExecution, executionMode } = useSSAStore();
|
||||
|
||||
if (executionMode !== 'agent') return null;
|
||||
|
||||
if (!agentExecution) {
|
||||
return (
|
||||
<div className="agent-code-panel">
|
||||
<div className="agent-code-header">
|
||||
<h4><Sparkles size={14} /> Agent 代码生成通道</h4>
|
||||
</div>
|
||||
<div className="agent-code-empty">
|
||||
<Sparkles size={32} style={{ color: '#94a3b8', marginBottom: 12 }} />
|
||||
<p>在对话区描述你的统计分析需求,</p>
|
||||
<p>Agent 将制定计划并生成 R 代码执行。</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { status, planText, planSteps, generatedCode, partialCode, errorMessage, retryCount, durationMs } = agentExecution;
|
||||
|
||||
const displayCode = generatedCode || partialCode;
|
||||
const isStreamingCode = status === 'coding' && !!partialCode && !generatedCode;
|
||||
|
||||
return (
|
||||
<div className="agent-code-panel">
|
||||
<div className="agent-code-header">
|
||||
<h4><Code size={14} /> Agent 分析流水线</h4>
|
||||
<StatusBadge status={status} />
|
||||
</div>
|
||||
|
||||
{/* Step 1: 分析计划 */}
|
||||
{(planText || planSteps) && (
|
||||
<div className="agent-section">
|
||||
<div className="agent-section-title">
|
||||
<FileText size={13} />
|
||||
<span>分析计划</span>
|
||||
{status === 'plan_pending' && <span className="badge-waiting">等待确认</span>}
|
||||
{['coding', 'code_pending', 'executing', 'completed'].includes(status) && (
|
||||
<span className="badge-done">已确认</span>
|
||||
)}
|
||||
</div>
|
||||
{planSteps && planSteps.length > 0 && (
|
||||
<div className="agent-plan-steps">
|
||||
{planSteps.map((s, i) => (
|
||||
<div key={i} className="plan-step-item">
|
||||
<span className="step-num">{s.order}</span>
|
||||
<span className="step-method">{s.method}</span>
|
||||
<span className="step-desc">{s.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{planText && !planSteps?.length && (
|
||||
<div className="agent-plan-text">{planText}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: R 代码 */}
|
||||
{(displayCode || status === 'coding') && (
|
||||
<div className="agent-section">
|
||||
<div className="agent-section-title">
|
||||
<Code size={13} />
|
||||
<span>R 代码</span>
|
||||
{isStreamingCode && <span className="badge-streaming"><Loader2 size={11} className="spin" /> 生成中...</span>}
|
||||
{status === 'code_pending' && <span className="badge-waiting">等待确认执行</span>}
|
||||
{['executing', 'completed'].includes(status) && <span className="badge-done">已确认</span>}
|
||||
</div>
|
||||
<div className="agent-code-body">
|
||||
{displayCode ? (
|
||||
<pre className={isStreamingCode ? 'streaming' : ''}>{displayCode}</pre>
|
||||
) : (
|
||||
<div className="agent-code-loading">
|
||||
<Loader2 size={16} className="spin" />
|
||||
<span>Agent 正在编写 R 代码...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 重试信息 */}
|
||||
{retryCount > 0 && (
|
||||
<div className="agent-retry-info">
|
||||
<RefreshCw size={14} />
|
||||
<span>第 {retryCount} 次重试(Agent 正在修复代码错误)</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误信息 */}
|
||||
{status === 'error' && errorMessage && errorMessage !== '用户取消' && (
|
||||
<div className="agent-error-bar">
|
||||
<XCircle size={13} />
|
||||
<pre>{errorMessage}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 执行中 */}
|
||||
{status === 'executing' && (
|
||||
<div className="agent-executing-bar">
|
||||
<Loader2 size={14} className="spin" />
|
||||
<span>R 引擎正在执行代码,请稍候...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 完成状态 */}
|
||||
{status === 'completed' && durationMs != null && (
|
||||
<div className="agent-success-bar">
|
||||
<CheckCircle size={13} />
|
||||
<span>执行完成,耗时 {(durationMs / 1000).toFixed(1)}s</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentCodePanel;
|
||||
74
frontend-v2/src/modules/ssa/components/ModeToggle.tsx
Normal file
74
frontend-v2/src/modules/ssa/components/ModeToggle.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* ModeToggle — 双通道切换组件
|
||||
*
|
||||
* 在 QPER 管线(预制工具)与 Agent 通道(LLM 代码生成)之间切换。
|
||||
* 切换时调用后端 PATCH API 更新 session.executionMode。
|
||||
*/
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Cpu, Sparkles, Loader2 } from 'lucide-react';
|
||||
import { useSSAStore } from '../stores/ssaStore';
|
||||
import type { ExecutionMode } from '../types';
|
||||
import apiClient from '@/common/api/axios';
|
||||
|
||||
export const ModeToggle: React.FC = () => {
|
||||
const {
|
||||
executionMode,
|
||||
setExecutionMode,
|
||||
currentSession,
|
||||
isExecuting,
|
||||
addToast,
|
||||
} = useSSAStore();
|
||||
|
||||
const [isSwitching, setIsSwitching] = useState(false);
|
||||
|
||||
const handleToggle = useCallback(async () => {
|
||||
if (!currentSession || isExecuting || isSwitching) return;
|
||||
|
||||
const nextMode: ExecutionMode = executionMode === 'qper' ? 'agent' : 'qper';
|
||||
|
||||
try {
|
||||
setIsSwitching(true);
|
||||
await apiClient.patch(`/api/v1/ssa/sessions/${currentSession.id}/execution-mode`, {
|
||||
executionMode: nextMode,
|
||||
});
|
||||
setExecutionMode(nextMode);
|
||||
addToast(
|
||||
nextMode === 'agent'
|
||||
? '已切换到 Agent 通道(LLM 代码生成)'
|
||||
: '已切换到 QPER 管线(预制工具)',
|
||||
'info',
|
||||
);
|
||||
} catch (err: any) {
|
||||
addToast(err?.message || '切换失败', 'error');
|
||||
} finally {
|
||||
setIsSwitching(false);
|
||||
}
|
||||
}, [currentSession, executionMode, isExecuting, isSwitching, setExecutionMode, addToast]);
|
||||
|
||||
if (!currentSession) return null;
|
||||
|
||||
return (
|
||||
<div className="ssa-mode-toggle">
|
||||
<button
|
||||
className={`mode-toggle-btn ${executionMode === 'qper' ? 'active' : ''}`}
|
||||
onClick={() => executionMode !== 'qper' && handleToggle()}
|
||||
disabled={isSwitching || isExecuting}
|
||||
title="QPER 管线:预制统计工具,结构化流程"
|
||||
>
|
||||
<Cpu size={14} />
|
||||
<span>QPER</span>
|
||||
</button>
|
||||
<button
|
||||
className={`mode-toggle-btn ${executionMode === 'agent' ? 'active' : ''}`}
|
||||
onClick={() => executionMode !== 'agent' && handleToggle()}
|
||||
disabled={isSwitching || isExecuting}
|
||||
title="Agent 通道:LLM 生成 R 代码,灵活分析"
|
||||
>
|
||||
{isSwitching ? <Loader2 size={14} className="spin" /> : <Sparkles size={14} />}
|
||||
<span>Agent</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModeToggle;
|
||||
@@ -43,6 +43,7 @@ import { ClarificationCard } from './ClarificationCard';
|
||||
import { AskUserCard } from './AskUserCard';
|
||||
import type { AskUserResponseData } from './AskUserCard';
|
||||
import { ThinkingBlock } from '@/shared/components/Chat';
|
||||
import { ModeToggle } from './ModeToggle';
|
||||
import type { ClarificationCardData, IntentResult } from '../types';
|
||||
|
||||
export const SSAChatPane: React.FC = () => {
|
||||
@@ -285,14 +286,17 @@ export const SSAChatPane: React.FC = () => {
|
||||
{currentSession?.title || '新的统计分析'}
|
||||
</span>
|
||||
</div>
|
||||
<EngineStatus
|
||||
isExecuting={isExecuting}
|
||||
isLoading={isLoading || isPlanLoading}
|
||||
isUploading={uploadStatus === 'uploading' || uploadStatus === 'parsing'}
|
||||
isProfileLoading={isProfileLoading || dataProfileLoading}
|
||||
isStreaming={isGenerating}
|
||||
streamIntent={currentIntent}
|
||||
/>
|
||||
<div className="chat-header-right">
|
||||
{currentSession && <ModeToggle />}
|
||||
<EngineStatus
|
||||
isExecuting={isExecuting}
|
||||
isLoading={isLoading || isPlanLoading}
|
||||
isUploading={uploadStatus === 'uploading' || uploadStatus === 'parsing'}
|
||||
isProfileLoading={isProfileLoading || dataProfileLoading}
|
||||
isStreaming={isGenerating}
|
||||
streamIntent={currentIntent}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Chat Messages */}
|
||||
|
||||
@@ -27,6 +27,7 @@ import type { AnalysisRecord } from '../stores/ssaStore';
|
||||
import { useWorkflow } from '../hooks/useWorkflow';
|
||||
import type { TraceStep, ReportBlock, WorkflowStepResult } from '../types';
|
||||
import { WorkflowTimeline, detectPlanMismatches } from './WorkflowTimeline';
|
||||
import { AgentCodePanel } from './AgentCodePanel';
|
||||
import { DynamicReport } from './DynamicReport';
|
||||
import { exportBlocksToWord } from '../utils/exportBlocksToWord';
|
||||
import apiClient from '@/common/api/axios';
|
||||
@@ -48,6 +49,8 @@ export const SSAWorkspacePane: React.FC = () => {
|
||||
hasUnsavedPlanChanges,
|
||||
setHasUnsavedPlanChanges,
|
||||
dataContext,
|
||||
executionMode,
|
||||
agentExecution,
|
||||
} = useSSAStore();
|
||||
|
||||
const { executeWorkflow, cancelWorkflow, isExecuting: isWorkflowExecuting } = useWorkflow();
|
||||
@@ -299,8 +302,29 @@ export const SSAWorkspacePane: React.FC = () => {
|
||||
{/* Canvas — single scroll */}
|
||||
<div className="workspace-canvas" ref={containerRef}>
|
||||
<div className="workspace-scroll-container">
|
||||
{/* ===== Agent 模式工作区 ===== */}
|
||||
{executionMode === 'agent' && (
|
||||
<div className="section-block">
|
||||
<AgentCodePanel />
|
||||
{/* Agent 模式的报告输出复用 DynamicReport */}
|
||||
{agentExecution?.status === 'completed' && agentExecution.reportBlocks && agentExecution.reportBlocks.length > 0 && (
|
||||
<div className="section-block result-section" ref={resultRef}>
|
||||
<div className="section-divider">
|
||||
<span className="divider-line" />
|
||||
<span className="divider-label">分析结果</span>
|
||||
<span className="divider-line" />
|
||||
</div>
|
||||
<div className="view-result fade-in">
|
||||
<DynamicReport blocks={agentExecution.reportBlocks} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== QPER 模式工作区 ===== */}
|
||||
{/* Empty state */}
|
||||
{!plan && (
|
||||
{executionMode === 'qper' && !plan && (
|
||||
<div className="view-empty fade-in">
|
||||
<div className="empty-icon">
|
||||
<FileQuestion size={48} />
|
||||
@@ -314,8 +338,8 @@ export const SSAWorkspacePane: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== Block 1: SAP ===== */}
|
||||
{plan && (
|
||||
{/* ===== Block 1: SAP (QPER only) ===== */}
|
||||
{executionMode === 'qper' && plan && (
|
||||
<div className="section-block sap-section-block">
|
||||
<div className="section-divider">
|
||||
<span className="divider-line" />
|
||||
@@ -372,8 +396,8 @@ export const SSAWorkspacePane: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== Block 2: Execution log ===== */}
|
||||
{steps.length > 0 && (
|
||||
{/* ===== Block 2: Execution log (QPER only) ===== */}
|
||||
{executionMode === 'qper' && steps.length > 0 && (
|
||||
<div className="section-block execution-section" ref={executionRef}>
|
||||
<div className="section-divider">
|
||||
<span className="divider-line" />
|
||||
@@ -450,8 +474,8 @@ export const SSAWorkspacePane: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== Block 3: Results ===== */}
|
||||
{hasResults && (
|
||||
{/* ===== Block 3: Results (QPER only) ===== */}
|
||||
{executionMode === 'qper' && hasResults && (
|
||||
<div className="section-block result-section" ref={resultRef}>
|
||||
<div className="section-divider">
|
||||
<span className="divider-line" />
|
||||
|
||||
@@ -257,6 +257,88 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Agent 通道 SSE 事件 ──
|
||||
|
||||
if (parsed.type === 'agent_planning') {
|
||||
const { setAgentExecution, setWorkspaceOpen, setActivePane } = useSSAStore.getState();
|
||||
setAgentExecution({
|
||||
id: parsed.executionId || crypto.randomUUID(),
|
||||
sessionId: '',
|
||||
query: content,
|
||||
retryCount: 0,
|
||||
status: 'planning',
|
||||
});
|
||||
setActivePane('execution');
|
||||
setWorkspaceOpen(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'agent_plan_ready') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
planText: parsed.planText,
|
||||
planSteps: parsed.plan?.steps,
|
||||
status: 'plan_pending',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'code_generating') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
partialCode: parsed.partialCode,
|
||||
status: 'coding',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'code_generated') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
generatedCode: parsed.code,
|
||||
partialCode: undefined,
|
||||
status: parsed.code ? 'code_pending' : 'coding',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'code_executing') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({ status: 'executing' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'code_result') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
reportBlocks: parsed.reportBlocks,
|
||||
generatedCode: parsed.code || useSSAStore.getState().agentExecution?.generatedCode,
|
||||
status: 'completed',
|
||||
durationMs: parsed.durationMs,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'code_error') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
status: 'error',
|
||||
errorMessage: parsed.message,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'code_retry') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
status: 'coding',
|
||||
retryCount: parsed.retryCount || 0,
|
||||
generatedCode: parsed.code,
|
||||
errorMessage: undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 错误事件
|
||||
if (parsed.type === 'error') {
|
||||
const errMsg = parsed.message || '处理消息时发生错误';
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { create } from 'zustand';
|
||||
import type {
|
||||
SSAMode,
|
||||
ExecutionMode,
|
||||
SSASession,
|
||||
SSAMessage,
|
||||
DataProfile,
|
||||
@@ -18,6 +19,7 @@ import type {
|
||||
VariableDictEntry,
|
||||
FiveSectionReport,
|
||||
VariableDetailData,
|
||||
AgentExecutionRecord,
|
||||
} from '../types';
|
||||
|
||||
type ArtifactPane = 'empty' | 'sap' | 'execution' | 'result';
|
||||
@@ -73,8 +75,16 @@ interface SSAState {
|
||||
dataProfileModalVisible: boolean;
|
||||
workflowPlanLoading: boolean;
|
||||
|
||||
/** 双通道执行模式 */
|
||||
executionMode: ExecutionMode;
|
||||
/** Agent 通道当前执行记录 */
|
||||
agentExecution: AgentExecutionRecord | null;
|
||||
|
||||
// ---- actions ----
|
||||
setMode: (mode: SSAMode) => void;
|
||||
setExecutionMode: (mode: ExecutionMode) => void;
|
||||
setAgentExecution: (exec: AgentExecutionRecord | null) => void;
|
||||
updateAgentExecution: (patch: Partial<AgentExecutionRecord>) => void;
|
||||
setCurrentSession: (session: SSASession | null) => void;
|
||||
addMessage: (message: SSAMessage) => void;
|
||||
setMessages: (messages: SSAMessage[]) => void;
|
||||
@@ -141,6 +151,8 @@ const initialState = {
|
||||
dataProfileModalVisible: false,
|
||||
workflowPlanLoading: false,
|
||||
hasUnsavedPlanChanges: false,
|
||||
executionMode: 'qper' as ExecutionMode,
|
||||
agentExecution: null as AgentExecutionRecord | null,
|
||||
dataContext: {
|
||||
dataOverview: null,
|
||||
variableDictionary: [],
|
||||
@@ -157,6 +169,12 @@ export const useSSAStore = create<SSAState>((set) => ({
|
||||
...initialState,
|
||||
|
||||
setMode: (mode) => set({ mode }),
|
||||
setExecutionMode: (mode) => set({ executionMode: mode }),
|
||||
setAgentExecution: (exec) => set({ agentExecution: exec }),
|
||||
updateAgentExecution: (patch) =>
|
||||
set((state) => ({
|
||||
agentExecution: state.agentExecution ? { ...state.agentExecution, ...patch } : null,
|
||||
})),
|
||||
setCurrentSession: (session) => set({ currentSession: session }),
|
||||
addMessage: (message) =>
|
||||
set((state) => ({ messages: [...state.messages, message] })),
|
||||
|
||||
@@ -340,6 +340,12 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1220,3 +1220,281 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
.wt-mismatch-force:hover { background: #fee2e2; }
|
||||
|
||||
/* ========== ModeToggle — 双通道切换 ========== */
|
||||
.ssa-mode-toggle {
|
||||
display: inline-flex;
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mode-toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mode-toggle-btn:hover:not(:disabled):not(.active) {
|
||||
color: #334155;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.mode-toggle-btn.active {
|
||||
background: #fff;
|
||||
color: #1e40af;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.mode-toggle-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ========== AgentCodePanel — Agent 代码面板 ========== */
|
||||
/* ── Agent Code Panel ── */
|
||||
.agent-code-panel {
|
||||
background: #1e293b;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.agent-code-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
background: #0f172a;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.agent-code-header h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.agent-code-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.agent-code-status.planning { background: #1e3a5f; color: #60a5fa; }
|
||||
.agent-code-status.plan_pending { background: #3b2f1e; color: #fbbf24; }
|
||||
.agent-code-status.coding { background: #1e3a5f; color: #60a5fa; }
|
||||
.agent-code-status.code_pending { background: #3b2f1e; color: #fbbf24; }
|
||||
.agent-code-status.executing { background: #1e3a5f; color: #60a5fa; }
|
||||
.agent-code-status.completed { background: #1a3a2a; color: #4ade80; }
|
||||
.agent-code-status.error { background: #3b1e1e; color: #f87171; }
|
||||
|
||||
/* Agent 分步区块 */
|
||||
.agent-section {
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.agent-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.agent-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.badge-waiting {
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: #3b2f1e;
|
||||
color: #fbbf24;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-done {
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: #1a3a2a;
|
||||
color: #4ade80;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-streaming {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: #1e3a5f;
|
||||
color: #60a5fa;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 计划步骤列表 */
|
||||
.agent-plan-steps {
|
||||
padding: 4px 16px 12px;
|
||||
}
|
||||
|
||||
.plan-step-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 5px 0;
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.plan-step-item .step-num {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #334155;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.plan-step-item .step-method {
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
color: #60a5fa;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.plan-step-item .step-desc {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.agent-plan-text {
|
||||
padding: 4px 16px 12px;
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 代码区域 */
|
||||
.agent-code-body {
|
||||
padding: 12px 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.agent-code-body pre {
|
||||
margin: 0;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #e2e8f0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.agent-code-body pre.streaming {
|
||||
border-right: 2px solid #60a5fa;
|
||||
animation: cursor-blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes cursor-blink {
|
||||
50% { border-color: transparent; }
|
||||
}
|
||||
|
||||
.agent-code-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.agent-code-empty {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
/* 状态条 */
|
||||
.agent-retry-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #3b2f1e;
|
||||
border-top: 1px solid #534120;
|
||||
font-size: 12px;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.agent-error-bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #3b1e1e;
|
||||
border-top: 1px solid #7f1d1d;
|
||||
font-size: 12px;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.agent-error-bar pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.agent-executing-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #1e3a5f;
|
||||
border-top: 1px solid #1e40af;
|
||||
font-size: 12px;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.agent-success-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #1a3a2a;
|
||||
border-top: 1px solid #166534;
|
||||
font-size: 12px;
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
export type SSAMode = 'analysis' | 'consult';
|
||||
|
||||
/** 双通道执行模式:QPER 管线 / LLM Agent 代码生成 */
|
||||
export type ExecutionMode = 'qper' | 'agent';
|
||||
|
||||
export type SessionStatus = 'active' | 'completed' | 'archived';
|
||||
|
||||
export interface SSASession {
|
||||
@@ -367,12 +370,36 @@ export interface ClarificationOptionData {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** SSE 消息类型(Phase R 扩展:qper_status + reflection_complete) */
|
||||
/** SSE 消息类型(Phase R 扩展:qper_status + reflection_complete + Agent 通道事件) */
|
||||
export type SSEMessageType =
|
||||
| 'connected'
|
||||
| 'step_start' | 'step_progress' | 'step_complete' | 'step_error'
|
||||
| 'workflow_complete' | 'workflow_error'
|
||||
| 'qper_status' | 'reflection_complete';
|
||||
| 'qper_status' | 'reflection_complete'
|
||||
| 'agent_planning' | 'agent_plan_ready'
|
||||
| 'code_generating' | 'code_generated'
|
||||
| 'code_executing' | 'code_result' | 'code_error' | 'code_retry';
|
||||
|
||||
/** Agent 通道执行状态 */
|
||||
export type AgentExecutionStatus =
|
||||
| 'pending' | 'planning' | 'plan_pending'
|
||||
| 'coding' | 'code_pending'
|
||||
| 'executing' | 'completed' | 'error';
|
||||
|
||||
export interface AgentExecutionRecord {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
query: string;
|
||||
planText?: string;
|
||||
planSteps?: Array<{ order: number; method: string; description: string }>;
|
||||
generatedCode?: string;
|
||||
partialCode?: string;
|
||||
reportBlocks?: ReportBlock[];
|
||||
retryCount: number;
|
||||
status: AgentExecutionStatus;
|
||||
errorMessage?: string;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
/** SSE 消息 */
|
||||
export interface SSEMessage {
|
||||
|
||||
Reference in New Issue
Block a user