feat(phase2): complete P1 tasks - Channels, Triggers, Skills CRUD and UI enhancements
Phase 2 P1 Tasks Completed: API Layer (gateway-client.ts, gatewayStore.ts): - Add Channels CRUD: getChannel, createChannel, updateChannel, deleteChannel - Add Triggers CRUD: getTrigger, createTrigger, updateTrigger, deleteTrigger - Add Skills CRUD: getSkill, createSkill, updateSkill, deleteSkill - Add Scheduled Tasks API: createScheduledTask, deleteScheduledTask, toggleScheduledTask - Add loadModels action for dynamic model list UI Components: - ModelsAPI.tsx: Dynamic model loading from API with loading/error states - SchedulerPanel.tsx: Full CreateJobModal with cron/interval/once scheduling - SecurityStatus.tsx: Loading states, error handling, retry functionality - WorkflowEditor.tsx: New workflow creation/editing modal (new file) - WorkflowHistory.tsx: Workflow execution history viewer (new file) - WorkflowList.tsx: Integrated editor and history access Configuration: - Add 4 Hands TOML configs: clip, collector, predictor, twitter Documentation (SYSTEM_ANALYSIS.md): - Update API coverage: 65% → 89% (53/62 endpoints) - Update UI completion: 85% → 92% - Mark Phase 2 P1 tasks as completed - Update technical debt cleanup status Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { useChatStore } from '../../store/chatStore';
|
||||
|
||||
interface ModelEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
const AVAILABLE_MODELS: ModelEntry[] = [
|
||||
{ id: 'glm-5', name: 'glm-5', provider: '智谱 AI' },
|
||||
{ id: 'qwen3.5-plus', name: 'qwen3.5-plus', provider: '通义千问' },
|
||||
{ id: 'kimi-k2.5', name: 'kimi-k2.5', provider: '月之暗面' },
|
||||
{ id: 'minimax-m2.5', name: 'MiniMax-M2.5', provider: 'MiniMax' },
|
||||
];
|
||||
|
||||
export function ModelsAPI() {
|
||||
const { connectionState, connect, disconnect, quickConfig, saveQuickConfig } = useGatewayStore();
|
||||
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());
|
||||
@@ -25,6 +24,13 @@ export function ModelsAPI() {
|
||||
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());
|
||||
@@ -45,6 +51,10 @@ export function ModelsAPI() {
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const handleRefreshModels = () => {
|
||||
loadModels();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
@@ -77,29 +87,115 @@ export function ModelsAPI() {
|
||||
<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>
|
||||
<span className="text-xs text-gray-400">切换后用于新的桌面对话请求</span>
|
||||
<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>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm">
|
||||
{AVAILABLE_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="text-xs text-gray-400 mt-1">{model.provider}</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>
|
||||
{/* 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>
|
||||
</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>
|
||||
@@ -141,4 +237,3 @@ export function ModelsAPI() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user