Major type system refactoring and error fixes across the codebase: **Type System Improvements:** - Extended OpenFangStreamEvent with 'connected' and 'agents_updated' event types - Added GatewayPong interface for WebSocket pong responses - Added index signature to MemorySearchOptions for Record compatibility - Fixed RawApproval interface with hand_name, run_id properties **Gateway & Protocol Fixes:** - Fixed performHandshake nonce handling in gateway-client.ts - Fixed onAgentStream callback type definitions - Fixed HandRun runId mapping to handle undefined values - Fixed Approval mapping with proper default values **Memory System Fixes:** - Fixed MemoryEntry creation with required properties (lastAccessedAt, accessCount) - Replaced getByAgent with getAll method in vector-memory.ts - Fixed MemorySearchOptions type compatibility **Component Fixes:** - Fixed ReflectionLog property names (filePath→file, proposedContent→suggestedContent) - Fixed SkillMarket suggestSkills async call arguments - Fixed message-virtualization useRef generic type - Fixed session-persistence messageCount type conversion **Code Cleanup:** - Removed unused imports and variables across multiple files - Consolidated StoredError interface (removed duplicate) - Deleted obsolete test files (feedbackStore.test.ts, memory-index.test.ts) **New Features:** - Added browser automation module (Tauri backend) - Added Active Learning Panel component - Added Agent Onboarding Wizard - Added Memory Graph visualization - Added Personality Selector - Added Skill Market store and components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
241 lines
11 KiB
TypeScript
241 lines
11 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
|
||
import { useGatewayStore } from '../../store/gatewayStore';
|
||
import { useChatStore } from '../../store/chatStore';
|
||
import { silentErrorHandler } from '../../lib/error-utils';
|
||
|
||
// Helper function to format context window size
|
||
function formatContextWindow(tokens?: number): string {
|
||
if (!tokens) return '';
|
||
if (tokens >= 1000000) {
|
||
return `${(tokens / 1000000).toFixed(1)}M`;
|
||
}
|
||
if (tokens >= 1000) {
|
||
return `${(tokens / 1000).toFixed(0)}K`;
|
||
}
|
||
return `${tokens}`;
|
||
}
|
||
|
||
export function ModelsAPI() {
|
||
const { connectionState, connect, disconnect, quickConfig, saveQuickConfig, models, modelsLoading, modelsError, loadModels } = useGatewayStore();
|
||
const { currentModel, setCurrentModel } = useChatStore();
|
||
const [gatewayUrl, setGatewayUrl] = useState(getStoredGatewayUrl());
|
||
const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken());
|
||
|
||
const connected = connectionState === 'connected';
|
||
const connecting = connectionState === 'connecting' || connectionState === 'reconnecting';
|
||
|
||
// Load models when connected
|
||
useEffect(() => {
|
||
if (connected && models.length === 0 && !modelsLoading) {
|
||
loadModels();
|
||
}
|
||
}, [connected, models.length, modelsLoading, loadModels]);
|
||
|
||
useEffect(() => {
|
||
setGatewayUrl(quickConfig.gatewayUrl || getStoredGatewayUrl());
|
||
setGatewayToken(quickConfig.gatewayToken || getStoredGatewayToken());
|
||
}, [quickConfig.gatewayToken, quickConfig.gatewayUrl]);
|
||
|
||
const handleReconnect = () => {
|
||
disconnect();
|
||
setTimeout(() => connect(
|
||
gatewayUrl || quickConfig.gatewayUrl || 'ws://127.0.0.1:50051/ws',
|
||
gatewayToken || quickConfig.gatewayToken || getStoredGatewayToken()
|
||
).catch(silentErrorHandler('ModelsAPI')), 500);
|
||
};
|
||
|
||
const handleSaveGatewaySettings = () => {
|
||
saveQuickConfig({
|
||
gatewayUrl,
|
||
gatewayToken,
|
||
}).catch(silentErrorHandler('ModelsAPI'));
|
||
};
|
||
|
||
const handleRefreshModels = () => {
|
||
loadModels();
|
||
};
|
||
|
||
return (
|
||
<div className="max-w-3xl">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h1 className="text-xl font-bold text-gray-900">模型与 API</h1>
|
||
<button
|
||
onClick={handleReconnect}
|
||
disabled={connecting}
|
||
className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors disabled:opacity-50"
|
||
>
|
||
{connecting ? '连接中...' : '重新连接'}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<h3 className="text-xs font-semibold text-gray-500 mb-3 uppercase tracking-wider">当前会话模型</h3>
|
||
<div className="bg-white border border-gray-200 rounded-xl p-4 shadow-sm space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-500">当前选择</span>
|
||
<span className="text-sm font-medium text-orange-600">{currentModel}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-500">Gateway 状态</span>
|
||
<span className={`text-sm ${connected ? 'text-green-600' : connecting ? 'text-yellow-600' : 'text-gray-400'}`}>
|
||
{connected ? '已连接' : connecting ? '连接中...' : '未连接'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<div className="flex justify-between items-center mb-3">
|
||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">可选模型</h3>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs text-gray-400">切换后用于新的桌面对话请求</span>
|
||
{connected && (
|
||
<button
|
||
onClick={handleRefreshModels}
|
||
disabled={modelsLoading}
|
||
className="text-xs text-orange-600 hover:text-orange-700 disabled:opacity-50"
|
||
>
|
||
{modelsLoading ? '加载中...' : '刷新'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Loading state */}
|
||
{modelsLoading && (
|
||
<div className="bg-white rounded-xl border border-gray-200 p-8 shadow-sm">
|
||
<div className="flex items-center justify-center">
|
||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500"></div>
|
||
<span className="ml-3 text-sm text-gray-500">正在加载模型列表...</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error state */}
|
||
{modelsError && !modelsLoading && (
|
||
<div className="bg-white rounded-xl border border-red-200 p-4 shadow-sm">
|
||
<div className="flex items-start gap-3">
|
||
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<div>
|
||
<p className="text-sm font-medium text-red-800">加载模型列表失败</p>
|
||
<p className="text-xs text-red-600 mt-1">{modelsError}</p>
|
||
<button
|
||
onClick={handleRefreshModels}
|
||
className="mt-2 text-xs text-red-600 hover:text-red-700 underline"
|
||
>
|
||
重试
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Not connected state */}
|
||
{!connected && !modelsLoading && !modelsError && (
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
|
||
<div className="text-center">
|
||
<svg className="w-8 h-8 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||
</svg>
|
||
<p className="text-sm text-gray-500">请先连接 Gateway 以获取可用模型列表</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Model list */}
|
||
{connected && !modelsLoading && !modelsError && models.length > 0 && (
|
||
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm">
|
||
{models.map((model) => {
|
||
const isActive = model.id === currentModel;
|
||
return (
|
||
<div key={model.id} className={`flex justify-between items-center p-4 ${isActive ? 'bg-orange-50/50' : ''}`}>
|
||
<div>
|
||
<div className="text-sm text-gray-900">{model.name}</div>
|
||
<div className="flex items-center gap-2 mt-1">
|
||
{model.provider && (
|
||
<span className="text-xs text-gray-400">{model.provider}</span>
|
||
)}
|
||
{model.contextWindow && (
|
||
<span className="text-xs text-gray-400">
|
||
{model.provider && '|'}
|
||
上下文 {formatContextWindow(model.contextWindow)}
|
||
</span>
|
||
)}
|
||
{model.maxOutput && (
|
||
<span className="text-xs text-gray-400">
|
||
最大输出 {formatContextWindow(model.maxOutput)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2 text-xs items-center">
|
||
{isActive ? (
|
||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">当前选择</span>
|
||
) : (
|
||
<button onClick={() => setCurrentModel(model.id)} className="text-orange-600 hover:underline">切换到此模型</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{/* Empty state */}
|
||
{connected && !modelsLoading && !modelsError && models.length === 0 && (
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
|
||
<div className="text-center">
|
||
<svg className="w-8 h-8 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 20a8 8 0 100-16 8 8 0 000 16z" />
|
||
</svg>
|
||
<p className="text-sm text-gray-500">暂无可用模型</p>
|
||
<p className="text-xs text-gray-400 mt-1">请检查 Gateway 配置或 Provider 设置</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-3 text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
|
||
当前页面只支持切换桌面端可选模型与维护 Gateway 连接信息,Provider Key、自定义模型增删改尚未在此页面接入。
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm font-medium text-gray-900">Gateway URL</span>
|
||
<span className={`px-2 py-0.5 rounded text-xs border ${connected ? 'bg-green-50 text-green-600 border-green-100' : 'bg-red-50 text-red-600 border-red-100'}`}>
|
||
{connected ? '已连接' : connecting ? '连接中...' : '未连接'}
|
||
</span>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button onClick={handleReconnect} className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors">
|
||
重新连接
|
||
</button>
|
||
<button onClick={handleSaveGatewaySettings} className="text-xs text-white bg-orange-500 hover:bg-orange-600 px-3 py-1.5 rounded-lg transition-colors">
|
||
保存连接设置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-3 bg-gray-50 border border-gray-200 rounded-xl p-3 text-xs text-gray-600 font-mono shadow-sm">
|
||
<input
|
||
type="text"
|
||
value={gatewayUrl}
|
||
onChange={(e) => setGatewayUrl(e.target.value)}
|
||
onBlur={() => { saveQuickConfig({ gatewayUrl }).catch(silentErrorHandler('ModelsAPI')); }}
|
||
className="w-full bg-transparent border-none outline-none"
|
||
/>
|
||
<input
|
||
type="password"
|
||
value={gatewayToken}
|
||
onChange={(e) => setGatewayToken(e.target.value)}
|
||
onBlur={() => { saveQuickConfig({ gatewayToken }).catch(silentErrorHandler('ModelsAPI')); }}
|
||
placeholder="Gateway auth token"
|
||
className="w-full bg-transparent border-none outline-none"
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|