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
5 fixes from focused audit: - Connect analyze_for_pain_signals() to post_conversation_hook (pain points now auto-created) - Add "generate solution" button in InsightsSection for high-confidence pain points (>=0.7) - Fix Memory URI mismatch: viking://agents/ → viking://agent/ (singular) - Remove duplicate .then() chain in useButlerInsights (was destructuring undefined) - Update stale director.rs doc comment (multi-agent now enabled by default)
154 lines
6.7 KiB
TypeScript
154 lines
6.7 KiB
TypeScript
import { useState } from 'react';
|
|
import { ChevronDown, ChevronUp, AlertTriangle, TrendingUp, Info, Lightbulb, Loader2 } from 'lucide-react';
|
|
import type { ButlerPainPoint } from '../../lib/viking-client';
|
|
import { generateButlerSolution } from '../../lib/viking-client';
|
|
|
|
interface InsightsSectionProps {
|
|
painPoints: ButlerPainPoint[];
|
|
onGenerate?: () => void;
|
|
}
|
|
|
|
function SeverityBadge({ severity }: { severity: ButlerPainPoint['severity'] }) {
|
|
const config = {
|
|
low: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', label: '低' },
|
|
medium: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300', label: '中' },
|
|
high: { bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300', label: '高' },
|
|
};
|
|
const c = config[severity];
|
|
return <span className={`px-1.5 py-0.5 rounded text-xs font-medium ${c.bg} ${c.text}`}>{c.label}</span>;
|
|
}
|
|
|
|
function StatusBadge({ status }: { status: ButlerPainPoint['status'] }) {
|
|
const config = {
|
|
detected: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-600 dark:text-gray-400', label: '已检测' },
|
|
confirmed: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300', label: '已确认' },
|
|
solving: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', label: '解决中' },
|
|
solved: { bg: 'bg-emerald-100 dark:bg-emerald-900/30', text: 'text-emerald-700 dark:text-emerald-300', label: '已解决' },
|
|
dismissed: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-500 dark:text-gray-500', label: '已忽略' },
|
|
};
|
|
const c = config[status];
|
|
return <span className={`px-1.5 py-0.5 rounded text-xs ${c.bg} ${c.text}`}>{c.label}</span>;
|
|
}
|
|
|
|
export function InsightsSection({ painPoints, onGenerate }: InsightsSectionProps) {
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [generating, setGenerating] = useState<string | null>(null);
|
|
|
|
const handleGenerateSolution = async (painId: string) => {
|
|
setGenerating(painId);
|
|
try {
|
|
await generateButlerSolution(painId);
|
|
onGenerate?.();
|
|
} catch {
|
|
onGenerate?.();
|
|
} finally {
|
|
setGenerating(null);
|
|
}
|
|
};
|
|
|
|
if (painPoints.length === 0) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<Info className="w-8 h-8 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">暂无洞察</p>
|
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
管家会在对话中自动发现您遇到的反复困难
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{painPoints.map((pp) => {
|
|
const isExpanded = expandedId === pp.id;
|
|
return (
|
|
<div
|
|
key={pp.id}
|
|
className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden"
|
|
>
|
|
<button
|
|
className="w-full px-3 py-2.5 flex items-start gap-2 text-left hover:bg-gray-50 dark:hover:bg-gray-750"
|
|
onClick={() => setExpandedId(isExpanded ? null : pp.id)}
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<AlertTriangle className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />
|
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
{pp.summary}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<SeverityBadge severity={pp.severity} />
|
|
<StatusBadge status={pp.status} />
|
|
<span className="text-xs text-gray-400 dark:text-gray-500">
|
|
{pp.occurrence_count} 次
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<div className="flex items-center gap-1">
|
|
<TrendingUp className="w-3 h-3 text-gray-400" />
|
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
{Math.round(pp.confidence * 100)}%
|
|
</span>
|
|
</div>
|
|
{isExpanded ? (
|
|
<ChevronUp className="w-4 h-4 text-gray-400" />
|
|
) : (
|
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<div className="px-3 pb-3 border-t border-gray-100 dark:border-gray-700 pt-2 space-y-2">
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
分类: <span className="text-gray-700 dark:text-gray-300">{pp.category}</span>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-xs font-medium text-gray-600 dark:text-gray-300">证据链:</div>
|
|
{pp.evidence.map((e, i) => (
|
|
<div
|
|
key={i}
|
|
className="text-xs bg-gray-50 dark:bg-gray-750 rounded p-2"
|
|
>
|
|
<div className="text-gray-700 dark:text-gray-300 italic">
|
|
“{e.user_said}”
|
|
</div>
|
|
<div className="text-gray-400 dark:text-gray-500 mt-0.5">
|
|
{e.why_flagged}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs text-gray-400 dark:text-gray-500">
|
|
<span>首次: {new Date(pp.first_seen).toLocaleDateString()}</span>
|
|
<span>|</span>
|
|
<span>最近: {new Date(pp.last_seen).toLocaleDateString()}</span>
|
|
</div>
|
|
{(pp.status === 'confirmed' || pp.status === 'detected') && pp.confidence >= 0.7 && (
|
|
<div className="pt-1">
|
|
<button
|
|
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-xs font-medium bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50"
|
|
onClick={() => handleGenerateSolution(pp.id)}
|
|
disabled={generating === pp.id}
|
|
>
|
|
{generating === pp.id ? (
|
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
) : (
|
|
<Lightbulb className="w-3.5 h-3.5" />
|
|
)}
|
|
生成解决方案
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|