Compare commits
3 Commits
b3f7328778
...
730d50bc63
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
730d50bc63 | ||
|
|
ce10befff1 | ||
|
|
f5c6abf03f |
@@ -1,4 +1,6 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useButlerInsights } from '../../hooks/useButlerInsights';
|
import { useButlerInsights } from '../../hooks/useButlerInsights';
|
||||||
|
import { useChatStore } from '../../store/chatStore';
|
||||||
import { InsightsSection } from './InsightsSection';
|
import { InsightsSection } from './InsightsSection';
|
||||||
import { ProposalsSection } from './ProposalsSection';
|
import { ProposalsSection } from './ProposalsSection';
|
||||||
import { MemorySection } from './MemorySection';
|
import { MemorySection } from './MemorySection';
|
||||||
@@ -9,6 +11,21 @@ interface ButlerPanelProps {
|
|||||||
|
|
||||||
export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
||||||
const { painPoints, proposals, loading, error, refresh } = useButlerInsights(agentId);
|
const { painPoints, proposals, loading, error, refresh } = useButlerInsights(agentId);
|
||||||
|
const messageCount = useChatStore((s) => s.messages.length);
|
||||||
|
const [analyzing, setAnalyzing] = useState(false);
|
||||||
|
|
||||||
|
const hasData = (painPoints?.length ?? 0) > 0 || (proposals?.length ?? 0) > 0;
|
||||||
|
const canAnalyze = messageCount >= 2;
|
||||||
|
|
||||||
|
const handleAnalyze = async () => {
|
||||||
|
if (!canAnalyze || analyzing) return;
|
||||||
|
setAnalyzing(true);
|
||||||
|
try {
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
setAnalyzing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
return (
|
return (
|
||||||
@@ -32,23 +49,51 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Insights section */}
|
{!loading && !hasData && (
|
||||||
<div>
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
<div className="text-gray-400 dark:text-gray-500 mb-3">
|
||||||
我最近在关注
|
<svg className="w-12 h-12 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</h3>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
|
||||||
<InsightsSection painPoints={painPoints} onGenerate={refresh} />
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
|
||||||
|
管家正在了解您,多轮对话后会自动发现您的需求
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleAnalyze}
|
||||||
|
disabled={!canAnalyze || analyzing}
|
||||||
|
className={`text-xs px-3 py-1.5 rounded-lg transition-colors ${
|
||||||
|
canAnalyze && !analyzing
|
||||||
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{analyzing ? '分析中...' : canAnalyze ? '立即分析对话' : '至少需要 2 条对话才能分析'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Proposals section */}
|
{(hasData || loading) && (
|
||||||
<div>
|
<>
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
{/* Insights section */}
|
||||||
我提出的方案
|
<div>
|
||||||
</h3>
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
<ProposalsSection proposals={proposals} onStatusChange={refresh} />
|
我最近在关注
|
||||||
</div>
|
</h3>
|
||||||
|
<InsightsSection painPoints={painPoints} onGenerate={refresh} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Memory section */}
|
{/* 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 (always show) */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
我记得关于您
|
我记得关于您
|
||||||
|
|||||||
@@ -275,30 +275,81 @@ function HistoryItem({
|
|||||||
onRestore: () => void;
|
onRestore: () => void;
|
||||||
isRestoring: boolean;
|
isRestoring: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
const timeAgo = getTimeAgo(snapshot.timestamp);
|
const timeAgo = getTimeAgo(snapshot.timestamp);
|
||||||
|
|
||||||
|
// Determine which files changed
|
||||||
|
const fileEntries: Array<{ key: string; label: string; color: string; content: string }> = [];
|
||||||
|
if (snapshot.files?.soul) {
|
||||||
|
fileEntries.push({ key: 'soul', label: 'Soul', color: 'text-purple-600 dark:text-purple-400', content: snapshot.files.soul });
|
||||||
|
}
|
||||||
|
if (snapshot.files?.instructions) {
|
||||||
|
fileEntries.push({ key: 'instructions', label: 'Instructions', color: 'text-blue-600 dark:text-blue-400', content: snapshot.files.instructions });
|
||||||
|
}
|
||||||
|
if (snapshot.files?.user_profile) {
|
||||||
|
fileEntries.push({ key: 'user_profile', label: 'Profile', color: 'text-emerald-600 dark:text-emerald-400', content: snapshot.files.user_profile });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
|
<div className="rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
|
||||||
<div className="w-8 h-8 rounded-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">
|
<div
|
||||||
<History className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
className="flex items-start gap-3 p-3 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70"
|
||||||
</div>
|
onClick={() => setExpanded(!expanded)}
|
||||||
<div className="flex-1 min-w-0">
|
>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="w-8 h-8 rounded-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">{timeAgo}</span>
|
<History className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||||
<Button
|
</div>
|
||||||
variant="ghost"
|
<div className="flex-1 min-w-0">
|
||||||
size="sm"
|
<div className="flex items-center justify-between gap-2">
|
||||||
onClick={onRestore}
|
<span className="text-xs text-gray-500 dark:text-gray-400">{timeAgo}</span>
|
||||||
disabled={isRestoring}
|
<div className="flex items-center gap-2">
|
||||||
className="text-xs text-gray-500 hover:text-orange-600"
|
{fileEntries.length > 0 && (
|
||||||
>
|
<span className="text-xs text-gray-400">
|
||||||
恢复
|
{fileEntries.length} 个文件
|
||||||
</Button>
|
</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onRestore(); }}
|
||||||
|
disabled={isRestoring}
|
||||||
|
className="text-xs text-gray-500 hover:text-orange-600"
|
||||||
|
>
|
||||||
|
恢复
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1">
|
||||||
|
{snapshot.reason || '自动快照'}
|
||||||
|
</p>
|
||||||
|
{fileEntries.length > 0 && (
|
||||||
|
<div className="flex gap-1 mt-1.5">
|
||||||
|
{fileEntries.map(({ key, label, color }) => (
|
||||||
|
<span
|
||||||
|
key={key}
|
||||||
|
className={`text-[10px] px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-700 ${color}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1 truncate">
|
|
||||||
{snapshot.reason || '自动快照'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{expanded && fileEntries.length > 0 && (
|
||||||
|
<div className="px-3 pb-3 pt-1 border-t border-gray-200 dark:border-gray-700 space-y-2">
|
||||||
|
{fileEntries.map(({ key, label, color, content }) => (
|
||||||
|
<div key={key}>
|
||||||
|
<div className={`text-xs font-medium ${color} mb-1`}>{label}</div>
|
||||||
|
<pre className="text-xs text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-32 overflow-y-auto bg-white dark:bg-gray-900 p-2 rounded">
|
||||||
|
{content.slice(0, 500)}
|
||||||
|
{content.length > 500 ? '...' : ''}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,19 +398,31 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 消息统计 */}
|
{/* 消息统计 — butler tab 时显示管家专用摘要 */}
|
||||||
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between text-xs">
|
{activeTab === 'butler' ? (
|
||||||
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between text-xs">
|
||||||
<BarChart3 className="w-3.5 h-3.5" />
|
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
||||||
<span>{messageCount} 条消息</span>
|
<span className="font-medium">管家模式</span>
|
||||||
<span className="text-gray-300 dark:text-gray-600">|</span>
|
</div>
|
||||||
<span>{userMsgCount} 用户 / {assistantMsgCount} 助手</span>
|
<div className={`flex items-center gap-1 ${connected ? 'text-emerald-500' : 'text-gray-400'}`}>
|
||||||
|
{connected ? <Wifi className="w-3.5 h-3.5" /> : <WifiOff className="w-3.5 h-3.5" />}
|
||||||
|
<span>{runtimeSummary}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex items-center gap-1 ${connected ? 'text-emerald-500' : 'text-gray-400'}`}>
|
) : (
|
||||||
{connected ? <Wifi className="w-3.5 h-3.5" /> : <WifiOff className="w-3.5 h-3.5" />}
|
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between text-xs">
|
||||||
<span>{runtimeSummary}</span>
|
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
||||||
|
<BarChart3 className="w-3.5 h-3.5" />
|
||||||
|
<span>{messageCount} 条消息</span>
|
||||||
|
<span className="text-gray-300 dark:text-gray-600">|</span>
|
||||||
|
<span>{userMsgCount} 用户 / {assistantMsgCount} 助手</span>
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center gap-1 ${connected ? 'text-emerald-500' : 'text-gray-400'}`}>
|
||||||
|
{connected ? <Wifi className="w-3.5 h-3.5" /> : <WifiOff className="w-3.5 h-3.5" />}
|
||||||
|
<span>{runtimeSummary}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
||||||
{activeTab === 'memory' ? (
|
{activeTab === 'memory' ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user