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
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:
@@ -357,6 +357,26 @@ function App() {
|
||||
time: '',
|
||||
});
|
||||
setShowOnboarding(false);
|
||||
|
||||
// Auto-trigger first Hand based on industry template
|
||||
const templateId = clone.source_template_id || '';
|
||||
const industryQueries: Record<string, { hand: string; action: string; query: string }> = {
|
||||
'edu-teacher-001': { hand: 'researcher', action: 'report', query: '帮我研究2026年教育数字化转型趋势,包括AI教学工具的最新进展' },
|
||||
'healthcare-admin-001': { hand: 'collector', action: 'collect', query: '帮我采集最新的医疗政策文件摘要,重点关注基层医疗改革方向' },
|
||||
'design-shantou-001': { hand: 'researcher', action: 'report', query: '帮我研究2026年服装设计流行趋势,包括色彩、面料和款式创新' },
|
||||
};
|
||||
const task = industryQueries[templateId];
|
||||
if (task) {
|
||||
// Delay slightly to let UI settle
|
||||
setTimeout(() => {
|
||||
useHandStore.getState().triggerHand(task.hand, {
|
||||
action: task.action,
|
||||
query: { query: task.query },
|
||||
}).catch(() => {
|
||||
// Non-critical — user can trigger manually
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理主视图切换
|
||||
|
||||
@@ -32,6 +32,8 @@ import { ChatMode } from './ai/ChatMode';
|
||||
import { ModelSelector } from './ai/ModelSelector';
|
||||
import { TaskProgress } from './ai/TaskProgress';
|
||||
import { SuggestionChips } from './ai/SuggestionChips';
|
||||
import { PipelineResultPreview } from './pipeline/PipelineResultPreview';
|
||||
import { PresentationContainer } from './presentation/PresentationContainer';
|
||||
// TokenMeter temporarily unused — using inline text counter instead
|
||||
|
||||
// Default heights for virtualized messages
|
||||
@@ -665,6 +667,20 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex
|
||||
)
|
||||
: '...'}
|
||||
</div>
|
||||
{/* Pipeline / Hand result presentation */}
|
||||
{!isUser && (message.role === 'workflow' || message.role === 'hand') && message.workflowResult && typeof message.workflowResult === 'object' && message.workflowResult !== null && (
|
||||
<div className="mt-3">
|
||||
<PipelineResultPreview
|
||||
outputs={message.workflowResult as Record<string, unknown>}
|
||||
pipelineId={message.workflowId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isUser && message.role === 'hand' && message.handResult && typeof message.handResult === 'object' && message.handResult !== null && !message.workflowResult && (
|
||||
<div className="mt-3">
|
||||
<PresentationContainer data={message.handResult} />
|
||||
</div>
|
||||
)}
|
||||
{message.error && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<p className="text-xs text-red-500">{message.error}</p>
|
||||
|
||||
85
desktop/src/components/pipeline/PipelineResultPreview.tsx
Normal file
85
desktop/src/components/pipeline/PipelineResultPreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user