feat(desktop): DeerFlow visual redesign + stream hang fix + intelligence client
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
DeerFlow frontend visual overhaul: - Card-style input box (white rounded card, textarea top, actions bottom) - Dropdown mode selector (闪速/思考/Pro/Ultra with icons+descriptions) - Colored quick-action chips (小惊喜/写作/研究/收集/学习) - Minimal top bar (title + token count + export) - Warm gray color system (#faf9f6 bg, #f5f4f1 sidebar, #e8e6e1 border) - DeerFlow-style sidebar (新对话/对话/智能体 nav) - Reasoning block, tool call chain, task progress visualization - Streaming text, model selector, suggestion chips components - Resizable artifact panel with drag handle - Virtualized message list for 100+ messages Bug fixes: - Stream hang: GatewayClient onclose code 1000 now calls onComplete - WebView2 textarea border: CSS !important override for UA styles - Gateway stream event handling (response/phase/tool_call types) Intelligence client: - Unified client with fallback drivers (compactor/heartbeat/identity/memory/reflection) - Gateway API types and type conversions
This commit is contained in:
121
desktop/src/components/ai/TokenMeter.tsx
Normal file
121
desktop/src/components/ai/TokenMeter.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TokenMeter — circular SVG gauge showing token usage
|
||||
// Inspired by DeerFlow's token usage display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface TokenMeterProps {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
model?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Color thresholds
|
||||
function getUsageColor(percent: number): string {
|
||||
if (percent >= 80) return '#ef4444'; // red
|
||||
if (percent >= 50) return '#eab308'; // yellow
|
||||
return '#22c55e'; // green
|
||||
}
|
||||
|
||||
// Format token count for display
|
||||
function formatTokens(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
||||
return String(n);
|
||||
}
|
||||
|
||||
export function TokenMeter({ inputTokens, outputTokens, model, className = '' }: TokenMeterProps) {
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const total = inputTokens + outputTokens;
|
||||
// Assume ~128K context window as budget for percentage calculation
|
||||
const budget = 128_000;
|
||||
const percent = Math.min(100, (total / budget) * 100);
|
||||
const color = getUsageColor(percent);
|
||||
|
||||
// SVG circular gauge parameters
|
||||
const size = 28;
|
||||
const strokeWidth = 3;
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (percent / 100) * circumference;
|
||||
|
||||
if (total === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
<button
|
||||
onClick={() => setShowDetail(!showDetail)}
|
||||
onMouseEnter={() => setShowDetail(true)}
|
||||
onMouseLeave={() => setShowDetail(false)}
|
||||
className="focus:outline-none"
|
||||
title="Token 用量"
|
||||
>
|
||||
<svg width={size} height={size} className="transform -rotate-90">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
/>
|
||||
{/* Usage arc */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-500"
|
||||
/>
|
||||
</svg>
|
||||
{/* Center text */}
|
||||
<span className="absolute inset-0 flex items-center justify-center text-[9px] font-medium text-gray-500 dark:text-gray-400">
|
||||
{percent >= 1 ? `${Math.round(percent)}` : '<1'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Hover detail card */}
|
||||
<AnimatePresence>
|
||||
{showDetail && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 4, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 4, scale: 0.95 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute bottom-full right-0 mb-2 w-44 p-3 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg z-50"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] text-gray-500 dark:text-gray-400">Input</span>
|
||||
<span className="text-[11px] font-medium text-gray-700 dark:text-gray-200">{formatTokens(inputTokens)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] text-gray-500 dark:text-gray-400">Output</span>
|
||||
<span className="text-[11px] font-medium text-gray-700 dark:text-gray-200">{formatTokens(outputTokens)}</span>
|
||||
</div>
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 pt-1.5 flex items-center justify-between">
|
||||
<span className="text-[11px] text-gray-500 dark:text-gray-400">Total</span>
|
||||
<span className="text-[11px] font-bold text-gray-800 dark:text-gray-100">{formatTokens(total)}</span>
|
||||
</div>
|
||||
{model && (
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 pt-1.5">
|
||||
<span className="text-[10px] text-gray-400 dark:text-gray-500 truncate block">{model}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user