Files
zclaw_openfang/desktop/src/components/WorkflowRecommendations.tsx
iven bf6d81f9c6
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor: 清理未使用代码并添加未来功能标记
style: 统一代码格式和注释风格

docs: 更新多个功能文档的完整度和状态

feat(runtime): 添加路径验证工具支持

fix(pipeline): 改进条件判断和变量解析逻辑

test(types): 为ID类型添加全面测试用例

chore: 更新依赖项和Cargo.lock文件

perf(mcp): 优化MCP协议传输和错误处理
2026-03-25 21:55:12 +08:00

341 lines
11 KiB
TypeScript

/**
* Workflow Recommendations Component
*
* Displays proactive workflow recommendations from the Adaptive Intelligence Mesh.
* Shows detected patterns and suggested workflows based on user behavior.
*/
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useMeshStore } from '../store/meshStore';
import type { WorkflowRecommendation, BehaviorPattern, PatternTypeVariant } from '../lib/intelligence-client';
// === Main Component ===
export const WorkflowRecommendations: React.FC = () => {
const {
recommendations,
patterns,
isLoading,
error,
analyze,
acceptRecommendation,
dismissRecommendation,
} = useMeshStore();
const [selectedPattern, setSelectedPattern] = useState<string | null>(null);
useEffect(() => {
// Initial analysis
analyze();
}, [analyze]);
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
<span className="ml-3 text-gray-400">Analyzing patterns...</span>
</div>
);
}
if (error) {
return (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg">
<p className="text-red-400 text-sm">{error}</p>
</div>
);
}
return (
<div className="space-y-6">
{/* Recommendations Section */}
<section>
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<span className="text-2xl">💡</span>
Recommended Workflows
{recommendations.length > 0 && (
<span className="ml-2 px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full">
{recommendations.length}
</span>
)}
</h3>
<AnimatePresence mode="popLayout">
{recommendations.length === 0 ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="p-6 bg-gray-800/30 rounded-lg border border-gray-700/50 text-center"
>
<p className="text-gray-400">No recommendations available yet.</p>
<p className="text-gray-500 text-sm mt-2">
Continue using the app to build up behavior patterns.
</p>
</motion.div>
) : (
<div className="space-y-3">
{recommendations.map((rec) => (
<RecommendationCard
key={rec.id}
recommendation={rec}
onAccept={() => acceptRecommendation(rec.id)}
onDismiss={() => dismissRecommendation(rec.id)}
/>
))}
</div>
)}
</AnimatePresence>
</section>
{/* Detected Patterns Section */}
<section>
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<span className="text-2xl">📊</span>
Detected Patterns
{patterns.length > 0 && (
<span className="ml-2 px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded-full">
{patterns.length}
</span>
)}
</h3>
{patterns.length === 0 ? (
<div className="p-6 bg-gray-800/30 rounded-lg border border-gray-700/50 text-center">
<p className="text-gray-400">No patterns detected yet.</p>
</div>
) : (
<div className="grid gap-3">
{patterns.map((pattern) => (
<PatternCard
key={pattern.id}
pattern={pattern}
isSelected={selectedPattern === pattern.id}
onClick={() =>
setSelectedPattern(
selectedPattern === pattern.id ? null : pattern.id
)
}
/>
))}
</div>
)}
</section>
</div>
);
};
// === Sub-Components ===
interface RecommendationCardProps {
recommendation: WorkflowRecommendation;
onAccept: () => void;
onDismiss: () => void;
}
const RecommendationCard: React.FC<RecommendationCardProps> = ({
recommendation,
onAccept,
onDismiss,
}) => {
const confidencePercent = Math.round(recommendation.confidence * 100);
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return 'text-green-400';
if (confidence >= 0.6) return 'text-yellow-400';
return 'text-orange-400';
};
return (
<motion.div
layout
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className="p-4 bg-gray-800/50 rounded-lg border border-gray-700/50 hover:border-blue-500/30 transition-colors"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h4 className="text-white font-medium truncate">
{recommendation.pipeline_id}
</h4>
<span
className={`text-xs font-mono ${getConfidenceColor(
recommendation.confidence
)}`}
>
{confidencePercent}%
</span>
</div>
<p className="text-gray-400 text-sm mb-3">{recommendation.reason}</p>
{/* Suggested Inputs */}
{Object.keys(recommendation.suggested_inputs).length > 0 && (
<div className="mb-3">
<p className="text-xs text-gray-500 mb-1">Suggested inputs:</p>
<div className="flex flex-wrap gap-1">
{Object.entries(recommendation.suggested_inputs).map(
([key, value]) => (
<span
key={key}
className="px-2 py-0.5 bg-gray-700/50 text-gray-300 text-xs rounded"
>
{key}: {String(value).slice(0, 20)}
</span>
)
)}
</div>
</div>
)}
{/* Matched Patterns */}
{recommendation.patterns_matched.length > 0 && (
<div className="text-xs text-gray-500">
Based on {recommendation.patterns_matched.length} pattern(s)
</div>
)}
</div>
{/* Actions */}
<div className="flex gap-2 shrink-0">
<button
onClick={onAccept}
className="px-3 py-1.5 bg-blue-500 hover:bg-blue-600 text-white text-sm rounded transition-colors"
>
Accept
</button>
<button
onClick={onDismiss}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm rounded transition-colors"
>
Dismiss
</button>
</div>
</div>
{/* Confidence Bar */}
<div className="mt-3 h-1 bg-gray-700 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${confidencePercent}%` }}
className={`h-full ${
recommendation.confidence >= 0.8
? 'bg-green-500'
: recommendation.confidence >= 0.6
? 'bg-yellow-500'
: 'bg-orange-500'
}`}
/>
</div>
</motion.div>
);
};
interface PatternCardProps {
pattern: BehaviorPattern;
isSelected: boolean;
onClick: () => void;
}
const PatternCard: React.FC<PatternCardProps> = ({
pattern,
isSelected,
onClick,
}) => {
const getPatternTypeLabel = (type: PatternTypeVariant | string) => {
// Handle object format
const typeStr = typeof type === 'string' ? type : type.type;
switch (typeStr) {
case 'SkillCombination':
return { label: 'Skill Combo', icon: '⚡' };
case 'TemporalTrigger':
return { label: 'Time Trigger', icon: '⏰' };
case 'TaskPipelineMapping':
return { label: 'Task Mapping', icon: '🔄' };
case 'InputPattern':
return { label: 'Input Pattern', icon: '📝' };
default:
return { label: typeStr, icon: '📊' };
}
};
const { label, icon } = getPatternTypeLabel(pattern.pattern_type as PatternTypeVariant);
const confidencePercent = Math.round(pattern.confidence * 100);
return (
<motion.div
layout
onClick={onClick}
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
isSelected
? 'bg-purple-500/10 border-purple-500/50'
: 'bg-gray-800/30 border-gray-700/50 hover:border-gray-600'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-lg">{icon}</span>
<span className="text-white font-medium">{label}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400">
{pattern.frequency}x used
</span>
<span
className={`text-xs font-mono ${
pattern.confidence >= 0.6
? 'text-green-400'
: 'text-yellow-400'
}`}
>
{confidencePercent}%
</span>
</div>
</div>
<AnimatePresence>
{isSelected && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-3 pt-3 border-t border-gray-700/50 overflow-hidden"
>
<div className="space-y-2 text-sm">
<div>
<span className="text-gray-500">ID:</span>{' '}
<span className="text-gray-300 font-mono text-xs">
{pattern.id}
</span>
</div>
<div>
<span className="text-gray-500">First seen:</span>{' '}
<span className="text-gray-300">
{new Date(pattern.first_occurrence).toLocaleDateString()}
</span>
</div>
<div>
<span className="text-gray-500">Last seen:</span>{' '}
<span className="text-gray-300">
{new Date(pattern.last_occurrence).toLocaleDateString()}
</span>
</div>
{pattern.context.intent && (
<div>
<span className="text-gray-500">Intent:</span>{' '}
<span className="text-gray-300">{pattern.context.intent}</span>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
export default WorkflowRecommendations;