feat(desktop): pipeline result preview + industry templates + onboarding auto-trigger
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

Sprint 2: 产品体验打磨 + 行业模板

- Create PipelineResultPreview component with tab-based output switching
- Connect workflow/hand messages to PresentationContainer in ChatArea
- Add auto-trigger first Hand after onboarding (industry-specific queries)
- Seed 3 industry agent templates (education, healthcare, design-shantou)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-04 10:48:47 +08:00
parent eac1d9449e
commit 894c0d7b15
4 changed files with 292 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
/**
* PipelineResultPreview — Pipeline 输出结果预览
*
* 将 Pipeline outputs 的多个顶层键以 tab 切换展示,
* 每个 tab 内嵌 PresentationContainer 做智能渲染。
*/
import { useState, useMemo } from 'react';
import { PresentationContainer } from '../presentation/PresentationContainer';
import { FileText, BarChart3, HelpCircle, Presentation, PenTool } from 'lucide-react';
interface PipelineResultPreviewProps {
/** Pipeline outputs 对象(多键 JSON */
outputs: Record<string, unknown>;
/** Pipeline ID */
pipelineId?: string;
className?: string;
}
const KEY_ICONS: Record<string, React.ReactNode> = {
summary: <FileText className="w-3.5 h-3.5" />,
report: <FileText className="w-3.5 h-3.5" />,
analysis: <BarChart3 className="w-3.5 h-3.5" />,
quiz: <HelpCircle className="w-3.5 h-3.5" />,
scenes: <Presentation className="w-3.5 h-3.5" />,
slides: <Presentation className="w-3.5 h-3.5" />,
plan: <PenTool className="w-3.5 h-3.5" />,
};
function getKeyIcon(key: string): React.ReactNode {
for (const [pattern, icon] of Object.entries(KEY_ICONS)) {
if (key.toLowerCase().includes(pattern)) return icon;
}
return <FileText className="w-3.5 h-3.5" />;
}
export function PipelineResultPreview({ outputs, pipelineId, className = '' }: PipelineResultPreviewProps) {
const keys = useMemo(() => Object.keys(outputs), [outputs]);
const [activeKey, setActiveKey] = useState(keys[0]);
if (keys.length === 0) {
return (
<div className="text-sm text-gray-400 italic p-4">
Pipeline
</div>
);
}
return (
<div className={`rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden ${className}`}>
{/* Tab bar */}
{keys.length > 1 && (
<div className="flex border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 overflow-x-auto">
{keys.map((key) => (
<button
key={key}
onClick={() => setActiveKey(key)}
className={`flex items-center gap-1.5 px-4 py-2.5 text-xs font-medium whitespace-nowrap transition-colors border-b-2 ${
activeKey === key
? 'border-emerald-500 text-emerald-700 dark:text-emerald-400 bg-white dark:bg-gray-800'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{getKeyIcon(key)}
<span>{key}</span>
</button>
))}
</div>
)}
{/* Content */}
<div className="p-4 bg-white dark:bg-gray-900">
{outputs[activeKey] !== undefined && outputs[activeKey] !== null ? (
<PresentationContainer
data={outputs[activeKey]}
pipelineId={pipelineId}
allowSwitch={true}
/>
) : (
<p className="text-sm text-gray-400"></p>
)}
</div>
</div>
);
}