From 80cadd11587e2ffb8a32ded7ac442a758e12d4dc Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 7 Apr 2026 09:30:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20add=20ButlerPanel=20=E2=80=94=20pai?= =?UTF-8?q?n=20points,=20proposals,=20memory=20insights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../ButlerPanel/InsightsSection.tsx | 122 +++++++++++++++ .../components/ButlerPanel/MemorySection.tsx | 71 +++++++++ .../ButlerPanel/ProposalsSection.tsx | 141 ++++++++++++++++++ desktop/src/components/ButlerPanel/index.tsx | 60 ++++++++ desktop/src/components/RightPanel.tsx | 14 +- desktop/src/hooks/useButlerInsights.ts | 53 +++++++ desktop/src/lib/viking-client.ts | 109 +++++++++++++- 7 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 desktop/src/components/ButlerPanel/InsightsSection.tsx create mode 100644 desktop/src/components/ButlerPanel/MemorySection.tsx create mode 100644 desktop/src/components/ButlerPanel/ProposalsSection.tsx create mode 100644 desktop/src/components/ButlerPanel/index.tsx create mode 100644 desktop/src/hooks/useButlerInsights.ts diff --git a/desktop/src/components/ButlerPanel/InsightsSection.tsx b/desktop/src/components/ButlerPanel/InsightsSection.tsx new file mode 100644 index 0000000..4601a31 --- /dev/null +++ b/desktop/src/components/ButlerPanel/InsightsSection.tsx @@ -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 {c.label}; +} + +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 {c.label}; +} + +export function InsightsSection({ painPoints }: InsightsSectionProps) { + const [expandedId, setExpandedId] = useState(null); + + if (painPoints.length === 0) { + return ( +
+ +

暂无洞察

+

+ 管家会在对话中自动发现您遇到的反复困难 +

+
+ ); + } + + return ( +
+ {painPoints.map((pp) => { + const isExpanded = expandedId === pp.id; + return ( +
+ + + {isExpanded && ( +
+
+ 分类: {pp.category} +
+
+
证据链:
+ {pp.evidence.map((e, i) => ( +
+
+ “{e.user_said}” +
+
+ {e.why_flagged} +
+
+ ))} +
+
+ 首次: {new Date(pp.first_seen).toLocaleDateString()} + | + 最近: {new Date(pp.last_seen).toLocaleDateString()} +
+
+ )} +
+ ); + })} +
+ ); +} diff --git a/desktop/src/components/ButlerPanel/MemorySection.tsx b/desktop/src/components/ButlerPanel/MemorySection.tsx new file mode 100644 index 0000000..ce7dea7 --- /dev/null +++ b/desktop/src/components/ButlerPanel/MemorySection.tsx @@ -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([]); + 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 ( +
+ +
+ ); + } + + if (memories.length === 0) { + return ( +
+ +

暂无记忆

+

+ 管家会在对话中自动积累对您的了解 +

+
+ ); + } + + return ( +
+ {memories.map((memory) => ( +
+
+ {memory.name} +
+
+ {memory.uri} +
+
+ ))} +
+ ); +} diff --git a/desktop/src/components/ButlerPanel/ProposalsSection.tsx b/desktop/src/components/ButlerPanel/ProposalsSection.tsx new file mode 100644 index 0000000..3dc310d --- /dev/null +++ b/desktop/src/components/ButlerPanel/ProposalsSection.tsx @@ -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 ( + + + {c.label} + + ); +} + +export function ProposalsSection({ proposals, onStatusChange }: ProposalsSectionProps) { + const [expandedId, setExpandedId] = useState(null); + const [updating, setUpdating] = useState(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 ( +
+ +

暂无方案

+

+ 当管家发现高置信度痛点时,会自动生成解决方案 +

+
+ ); + } + + return ( +
+ {proposals.map((proposal) => { + const isExpanded = expandedId === proposal.id; + const isUpdating = updating === proposal.id; + + return ( +
+ + + {isExpanded && ( +
+

+ {proposal.description} +

+ + {proposal.steps.length > 0 && ( +
+
步骤:
+ {proposal.steps.map((step) => ( +
+ + {step.index} + +
+
+ {step.action} +
+
+ {step.detail} +
+
+
+ ))} +
+ )} + + {proposal.status === 'pending' && ( +
+ + +
+ )} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/desktop/src/components/ButlerPanel/index.tsx b/desktop/src/components/ButlerPanel/index.tsx new file mode 100644 index 0000000..ee2c500 --- /dev/null +++ b/desktop/src/components/ButlerPanel/index.tsx @@ -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 ( +
+

请先选择一个 Agent

+
+ ); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + + {loading && ( +
+
+
+ )} + + {/* Insights section */} +
+

+ 我最近在关注 +

+ +
+ + {/* Proposals section */} +
+

+ 我提出的方案 +

+ +
+ + {/* Memory section */} +
+

+ 我记得关于您 +

+ +
+
+ ); +} diff --git a/desktop/src/components/RightPanel.tsx b/desktop/src/components/RightPanel.tsx index 06236a1..3d7c1eb 100644 --- a/desktop/src/components/RightPanel.tsx +++ b/desktop/src/components/RightPanel.tsx @@ -12,7 +12,9 @@ import { MessageSquare, Cpu, FileText, User, Activity, Brain, Shield, Sparkles, List, Network, Dna, History, ChevronDown, ChevronUp, RotateCcw, AlertCircle, Loader2, + ConciergeBell, } from 'lucide-react'; +import { ButlerPanel } from './ButlerPanel'; // === Helper to extract code blocks from markdown content === function extractCodeBlocksFromContent(content: string): CodeBlock[] { @@ -106,7 +108,7 @@ export function RightPanel() { const { messages, setCurrentAgent } = useChatStore(); const currentModel = useConversationStore((s) => s.currentModel); 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 [isEditingAgent, setIsEditingAgent] = useState(false); const [agentDraft, setAgentDraft] = useState(null); @@ -316,6 +318,12 @@ export function RightPanel() { icon={} label="演化" /> + setActiveTab('butler')} + icon={} + label="管家" + />
@@ -377,7 +385,9 @@ export function RightPanel() { ) : activeTab === 'evolution' ? ( - ) : activeTab === 'agent' ? ( + ) : activeTab === 'butler' ? ( + + ) : activeTab === 'agent'? (
void; +} + +export function useButlerInsights(agentId: string | undefined): ButlerInsightsState { + const [painPoints, setPainPoints] = useState([]); + const [proposals, setProposals] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 }; +} diff --git a/desktop/src/lib/viking-client.ts b/desktop/src/lib/viking-client.ts index 858fe04..5476ffd 100644 --- a/desktop/src/lib/viking-client.ts +++ b/desktop/src/lib/viking-client.ts @@ -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 { return invoke('butler_list_pain_points', { agentId }); } +/** + * Get butler proposals for an agent + */ +export async function getButlerProposals(agentId: string): Promise { + return invoke('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 { + return invoke('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 { + return invoke('butler_generate_solution', { painId }); +} + +/** + * Update the status of a proposal + */ +export async function updateButlerProposalStatus( + proposalId: string, + status: string +): Promise { + return invoke('butler_update_proposal_status', { proposalId, status }); +} + +/** + * Butler delegates a user request to expert agents + */ +export async function butlerDelegateTask(request: string): Promise { + return invoke('butler_delegate_task', { request }); +}