feat(ui): add ButlerPanel — pain points, proposals, memory insights
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
- Add Butler types (PainPoint, Proposal, DelegationResult) to viking-client.ts - Add butler API functions: getButlerInsights, getButlerProposals, recordButlerPainPoint, generateButlerSolution, updateButlerProposalStatus, butlerDelegateTask - Create ButlerPanel with three sections: - InsightsSection: pain point cards with evidence chain, severity, confidence - ProposalsSection: solution cards with accept/reject actions - MemorySection: Viking memory entries per agent - Create useButlerInsights hook for data fetching - Add "管家" tab to RightPanel with ConciergeBell icon Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
122
desktop/src/components/ButlerPanel/InsightsSection.tsx
Normal file
122
desktop/src/components/ButlerPanel/InsightsSection.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { ChevronDown, ChevronUp, AlertTriangle, TrendingUp, Info } from 'lucide-react';
|
||||||
|
import type { ButlerPainPoint } from '../../lib/viking-client';
|
||||||
|
|
||||||
|
interface InsightsSectionProps {
|
||||||
|
painPoints: ButlerPainPoint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }: InsightsSectionProps) {
|
||||||
|
const [expandedId, setExpandedId] = useState<string | null>(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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
desktop/src/components/ButlerPanel/MemorySection.tsx
Normal file
71
desktop/src/components/ButlerPanel/MemorySection.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Brain, Loader2 } from 'lucide-react';
|
||||||
|
import { listVikingResources } from '../../lib/viking-client';
|
||||||
|
|
||||||
|
interface MemorySectionProps {
|
||||||
|
agentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemoryEntry {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
resourceType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MemorySection({ agentId }: MemorySectionProps) {
|
||||||
|
const [memories, setMemories] = useState<MemoryEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!agentId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
listVikingResources(`viking://agents/${agentId}/memories/`)
|
||||||
|
.then((entries) => {
|
||||||
|
setMemories(entries as MemoryEntry[]);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Memory path may not exist yet — show empty state
|
||||||
|
setMemories([]);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [agentId]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="w-5 h-5 text-gray-400 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memories.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Brain 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-2">
|
||||||
|
{memories.map((memory) => (
|
||||||
|
<div
|
||||||
|
key={memory.uri}
|
||||||
|
className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2"
|
||||||
|
>
|
||||||
|
<div className="text-sm text-gray-900 dark:text-gray-100 truncate">
|
||||||
|
{memory.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 dark:text-gray-500 truncate mt-0.5">
|
||||||
|
{memory.uri}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
desktop/src/components/ButlerPanel/ProposalsSection.tsx
Normal file
141
desktop/src/components/ButlerPanel/ProposalsSection.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { CheckCircle, XCircle, Clock, Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type { ButlerProposal } from '../../lib/viking-client';
|
||||||
|
import { updateButlerProposalStatus } from '../../lib/viking-client';
|
||||||
|
|
||||||
|
interface ProposalsSectionProps {
|
||||||
|
proposals: ButlerProposal[];
|
||||||
|
onStatusChange?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProposalStatusBadge({ status }: { status: ButlerProposal['status'] }) {
|
||||||
|
const config = {
|
||||||
|
pending: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300', icon: Clock, label: '待处理' },
|
||||||
|
accepted: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', icon: CheckCircle, label: '已接受' },
|
||||||
|
rejected: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-500 dark:text-gray-400', icon: XCircle, label: '已拒绝' },
|
||||||
|
completed: { bg: 'bg-emerald-100 dark:bg-emerald-900/30', text: 'text-emerald-700 dark:text-emerald-300', icon: CheckCircle, label: '已完成' },
|
||||||
|
};
|
||||||
|
const c = config[status];
|
||||||
|
const Icon = c.icon;
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium ${c.bg} ${c.text}`}>
|
||||||
|
<Icon className="w-3 h-3" />
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProposalsSection({ proposals, onStatusChange }: ProposalsSectionProps) {
|
||||||
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
|
const [updating, setUpdating] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleStatusUpdate = async (proposalId: string, status: string) => {
|
||||||
|
setUpdating(proposalId);
|
||||||
|
try {
|
||||||
|
await updateButlerProposalStatus(proposalId, status);
|
||||||
|
onStatusChange?.();
|
||||||
|
} catch (err) {
|
||||||
|
// Error handled silently - UI will not update
|
||||||
|
} finally {
|
||||||
|
setUpdating(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proposals.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Lightbulb 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">
|
||||||
|
{proposals.map((proposal) => {
|
||||||
|
const isExpanded = expandedId === proposal.id;
|
||||||
|
const isUpdating = updating === proposal.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={proposal.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 : proposal.id)}
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1 truncate">
|
||||||
|
{proposal.title}
|
||||||
|
</div>
|
||||||
|
<ProposalStatusBadge status={proposal.status} />
|
||||||
|
</div>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="px-3 pb-3 border-t border-gray-100 dark:border-gray-700 pt-2 space-y-3">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 whitespace-pre-line">
|
||||||
|
{proposal.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{proposal.steps.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-xs font-medium text-gray-600 dark:text-gray-300">步骤:</div>
|
||||||
|
{proposal.steps.map((step) => (
|
||||||
|
<div
|
||||||
|
key={step.index}
|
||||||
|
className="flex items-start gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 flex items-center justify-center font-medium">
|
||||||
|
{step.index}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{step.action}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 dark:text-gray-400">
|
||||||
|
{step.detail}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{proposal.status === 'pending' && (
|
||||||
|
<div className="flex gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-xs font-medium bg-emerald-500 text-white hover:bg-emerald-600 disabled:opacity-50"
|
||||||
|
onClick={() => handleStatusUpdate(proposal.id, 'accepted')}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-3.5 h-3.5" />
|
||||||
|
接受
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-xs font-medium bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 disabled:opacity-50"
|
||||||
|
onClick={() => handleStatusUpdate(proposal.id, 'rejected')}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
<XCircle className="w-3.5 h-3.5" />
|
||||||
|
拒绝
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
desktop/src/components/ButlerPanel/index.tsx
Normal file
60
desktop/src/components/ButlerPanel/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { useButlerInsights } from '../../hooks/useButlerInsights';
|
||||||
|
import { InsightsSection } from './InsightsSection';
|
||||||
|
import { ProposalsSection } from './ProposalsSection';
|
||||||
|
import { MemorySection } from './MemorySection';
|
||||||
|
|
||||||
|
interface ButlerPanelProps {
|
||||||
|
agentId: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
||||||
|
const { painPoints, proposals, loading, error, refresh } = useButlerInsights(agentId);
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">请先选择一个 Agent</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 px-3 py-2 text-xs text-red-700 dark:text-red-300">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<div className="w-5 h-5 border-2 border-gray-300 dark:border-gray-600 border-t-blue-500 rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Insights section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
我最近在关注
|
||||||
|
</h3>
|
||||||
|
<InsightsSection painPoints={painPoints} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Proposals section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
我提出的方案
|
||||||
|
</h3>
|
||||||
|
<ProposalsSection proposals={proposals} onStatusChange={refresh} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Memory section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
我记得关于您
|
||||||
|
</h3>
|
||||||
|
<MemorySection agentId={agentId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||||
Shield, Sparkles, List, Network, Dna, History,
|
Shield, Sparkles, List, Network, Dna, History,
|
||||||
ChevronDown, ChevronUp, RotateCcw, AlertCircle, Loader2,
|
ChevronDown, ChevronUp, RotateCcw, AlertCircle, Loader2,
|
||||||
|
ConciergeBell,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { ButlerPanel } from './ButlerPanel';
|
||||||
|
|
||||||
// === Helper to extract code blocks from markdown content ===
|
// === Helper to extract code blocks from markdown content ===
|
||||||
function extractCodeBlocksFromContent(content: string): CodeBlock[] {
|
function extractCodeBlocksFromContent(content: string): CodeBlock[] {
|
||||||
@@ -106,7 +108,7 @@ export function RightPanel() {
|
|||||||
const { messages, setCurrentAgent } = useChatStore();
|
const { messages, setCurrentAgent } = useChatStore();
|
||||||
const currentModel = useConversationStore((s) => s.currentModel);
|
const currentModel = useConversationStore((s) => s.currentModel);
|
||||||
const currentAgent = useConversationStore((s) => s.currentAgent);
|
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'evolution'>('status');
|
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'evolution' | 'butler'>('status');
|
||||||
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
||||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||||
@@ -316,6 +318,12 @@ export function RightPanel() {
|
|||||||
icon={<Dna className="w-4 h-4" />}
|
icon={<Dna className="w-4 h-4" />}
|
||||||
label="演化"
|
label="演化"
|
||||||
/>
|
/>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'butler'}
|
||||||
|
onClick={() => setActiveTab('butler')}
|
||||||
|
icon={<ConciergeBell className="w-4 h-4" />}
|
||||||
|
label="管家"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -377,7 +385,9 @@ export function RightPanel() {
|
|||||||
<AutonomyConfig />
|
<AutonomyConfig />
|
||||||
) : activeTab === 'evolution' ? (
|
) : activeTab === 'evolution' ? (
|
||||||
<IdentityChangeProposalPanel />
|
<IdentityChangeProposalPanel />
|
||||||
) : activeTab === 'agent' ? (
|
) : activeTab === 'butler' ? (
|
||||||
|
<ButlerPanel agentId={currentAgent?.id} />
|
||||||
|
) : activeTab === 'agent'? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={cardHover}
|
whileHover={cardHover}
|
||||||
|
|||||||
53
desktop/src/hooks/useButlerInsights.ts
Normal file
53
desktop/src/hooks/useButlerInsights.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
getButlerInsights,
|
||||||
|
getButlerProposals,
|
||||||
|
type ButlerPainPoint,
|
||||||
|
type ButlerProposal,
|
||||||
|
} from '../lib/viking-client';
|
||||||
|
|
||||||
|
interface ButlerInsightsState {
|
||||||
|
painPoints: ButlerPainPoint[];
|
||||||
|
proposals: ButlerProposal[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useButlerInsights(agentId: string | undefined): ButlerInsightsState {
|
||||||
|
const [painPoints, setPainPoints] = useState<ButlerPainPoint[]>([]);
|
||||||
|
const [proposals, setProposals] = useState<ButlerProposal[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
if (!agentId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
getButlerInsights(agentId).catch((err: unknown) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(msg);
|
||||||
|
return [] as ButlerPainPoint[];
|
||||||
|
}),
|
||||||
|
getButlerProposals(agentId).catch((err: unknown) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(msg);
|
||||||
|
return [] as ButlerProposal[];
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(([pains, props]) => {
|
||||||
|
setPainPoints(pains);
|
||||||
|
setProposals(props);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [agentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
return { painPoints, proposals, loading, error, refresh };
|
||||||
|
}
|
||||||
@@ -201,9 +201,116 @@ export async function extractAndStoreMemories(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Butler Insights Functions ===
|
||||||
|
|
||||||
|
export interface ButlerPainPoint {
|
||||||
|
id: string;
|
||||||
|
agent_id: string;
|
||||||
|
user_id: string;
|
||||||
|
summary: string;
|
||||||
|
category: string;
|
||||||
|
severity: 'low' | 'medium' | 'high';
|
||||||
|
evidence: Array<{ when: string; user_said: string; why_flagged: string }>;
|
||||||
|
occurrence_count: number;
|
||||||
|
first_seen: string;
|
||||||
|
last_seen: string;
|
||||||
|
confidence: number;
|
||||||
|
status: 'detected' | 'confirmed' | 'solving' | 'solved' | 'dismissed';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButlerProposalStep {
|
||||||
|
index: number;
|
||||||
|
action: string;
|
||||||
|
detail: string;
|
||||||
|
skill_hint: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButlerProposal {
|
||||||
|
id: string;
|
||||||
|
pain_point_id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
steps: ButlerProposalStep[];
|
||||||
|
status: 'pending' | 'accepted' | 'rejected' | 'completed';
|
||||||
|
evidence_chain: Array<{ when: string; user_said: string; why_flagged: string }>;
|
||||||
|
confidence_at_creation: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButlerDelegationTask {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
priority: number;
|
||||||
|
status: 'pending' | 'assigned' | 'in_progress' | 'completed' | 'failed';
|
||||||
|
assigned_expert: { id: string; name: string; role: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButlerDelegationResult {
|
||||||
|
request: string;
|
||||||
|
tasks: ButlerDelegationTask[];
|
||||||
|
success: boolean;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get butler insights data - pain points and proposals for the but agentId }
|
* Get butler pain points for an agent
|
||||||
|
*/
|
||||||
|
export async function getButlerInsights(agentId: string): Promise<ButlerPainPoint[]> {
|
||||||
return invoke<ButlerPainPoint[]>('butler_list_pain_points', { agentId });
|
return invoke<ButlerPainPoint[]>('butler_list_pain_points', { agentId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get butler proposals for an agent
|
||||||
|
*/
|
||||||
|
export async function getButlerProposals(agentId: string): Promise<ButlerProposal[]> {
|
||||||
|
return invoke<ButlerProposal[]>('butler_list_proposals', { agentId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a new pain point from conversation analysis
|
||||||
|
*/
|
||||||
|
export async function recordButlerPainPoint(
|
||||||
|
agentId: string,
|
||||||
|
userId: string,
|
||||||
|
summary: string,
|
||||||
|
category: string,
|
||||||
|
severity: string,
|
||||||
|
userSaid: string,
|
||||||
|
whyFlagged: string
|
||||||
|
): Promise<ButlerPainPoint> {
|
||||||
|
return invoke<ButlerPainPoint>('butler_record_pain_point', {
|
||||||
|
agentId,
|
||||||
|
userId,
|
||||||
|
summary,
|
||||||
|
category,
|
||||||
|
severity,
|
||||||
|
userSaid,
|
||||||
|
whyFlagged,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a solution for a high-confidence pain point
|
||||||
|
*/
|
||||||
|
export async function generateButlerSolution(painId: string): Promise<ButlerProposal> {
|
||||||
|
return invoke<ButlerProposal>('butler_generate_solution', { painId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the status of a proposal
|
||||||
|
*/
|
||||||
|
export async function updateButlerProposalStatus(
|
||||||
|
proposalId: string,
|
||||||
|
status: string
|
||||||
|
): Promise<void> {
|
||||||
|
return invoke<void>('butler_update_proposal_status', { proposalId, status });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Butler delegates a user request to expert agents
|
||||||
|
*/
|
||||||
|
export async function butlerDelegateTask(request: string): Promise<ButlerDelegationResult> {
|
||||||
|
return invoke<ButlerDelegationResult>('butler_delegate_task', { request });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user