feat(ui): 添加外部服务配置面板 (ServicesPanel)
- 新增 ServicesPanel 组件,用于配置 Tavily 等第三方服务的 API Key - 添加 Services API 客户端方法 (listServices, updateService, deleteService) - 在工具栏菜单中添加 "External Services" 入口 - 支持 API Key 的配置、启用/禁用和删除
This commit is contained in:
@@ -1215,3 +1215,62 @@ export async function refreshProjectTokenStats(projectId: string): Promise<{
|
|||||||
}> {
|
}> {
|
||||||
return request('POST', `/stats/projects/${encodeURIComponent(projectId)}/refresh`);
|
return request('POST', `/stats/projects/${encodeURIComponent(projectId)}/refresh`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Services API ============
|
||||||
|
|
||||||
|
/** 服务列表项 */
|
||||||
|
export interface ServiceListItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
website: string;
|
||||||
|
enabled: boolean;
|
||||||
|
hasApiKey: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有服务列表
|
||||||
|
*/
|
||||||
|
export async function listServices(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data: ServiceListItem[];
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('GET', '/services');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个服务详情
|
||||||
|
*/
|
||||||
|
export async function getService(id: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: ServiceListItem;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('GET', `/services/${encodeURIComponent(id)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新服务配置
|
||||||
|
*/
|
||||||
|
export async function updateService(
|
||||||
|
id: string,
|
||||||
|
config: { apiKey?: string; enabled?: boolean }
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('PUT', `/services/${encodeURIComponent(id)}`, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除服务配置(移除 API Key)
|
||||||
|
*/
|
||||||
|
export async function deleteService(id: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('DELETE', `/services/${encodeURIComponent(id)}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,452 @@
|
|||||||
|
/**
|
||||||
|
* ServicesPanel Component
|
||||||
|
*
|
||||||
|
* Third-party services configuration panel (e.g., Tavily for web search)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
RefreshCw,
|
||||||
|
Globe,
|
||||||
|
Key,
|
||||||
|
Check,
|
||||||
|
Loader2,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
ExternalLink,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { cn } from '../utils/cn';
|
||||||
|
import { modalOverlay, modalContent, smoothTransition } from '../utils/animations';
|
||||||
|
import { Button } from '../primitives/Button';
|
||||||
|
import { Input } from '../primitives/Input';
|
||||||
|
import { Switch } from '../primitives/Switch';
|
||||||
|
import { Skeleton } from './Skeleton';
|
||||||
|
import {
|
||||||
|
listServices,
|
||||||
|
updateService,
|
||||||
|
deleteService,
|
||||||
|
type ServiceListItem,
|
||||||
|
} from '../api/client.js';
|
||||||
|
|
||||||
|
interface ServicesPanelProps {
|
||||||
|
onClose: () => void;
|
||||||
|
/** Enable responsive layout */
|
||||||
|
responsive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServicesPanel({ onClose, responsive = false }: ServicesPanelProps) {
|
||||||
|
// Data state
|
||||||
|
const [services, setServices] = useState<ServiceListItem[]>([]);
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [savingService, setSavingService] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Edit state
|
||||||
|
const [editingService, setEditingService] = useState<string | null>(null);
|
||||||
|
const [apiKeyInput, setApiKeyInput] = useState('');
|
||||||
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
|
|
||||||
|
// Load services list
|
||||||
|
const loadServices = useCallback(async (showToast = false) => {
|
||||||
|
try {
|
||||||
|
const result = await listServices();
|
||||||
|
if (result.success) {
|
||||||
|
setServices(result.data);
|
||||||
|
if (showToast) {
|
||||||
|
toast.success('Services refreshed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to load services');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to load services');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
loadServices().finally(() => setLoading(false));
|
||||||
|
}, [loadServices]);
|
||||||
|
|
||||||
|
// Refresh
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
await loadServices(true);
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start editing a service
|
||||||
|
const handleStartEdit = async (serviceId: string) => {
|
||||||
|
setEditingService(serviceId);
|
||||||
|
setApiKeyInput('');
|
||||||
|
setShowApiKey(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save service config
|
||||||
|
const handleSaveService = async (serviceId: string, enabled: boolean) => {
|
||||||
|
setSavingService(serviceId);
|
||||||
|
try {
|
||||||
|
const payload: { apiKey?: string; enabled?: boolean } = { enabled };
|
||||||
|
if (apiKeyInput) {
|
||||||
|
payload.apiKey = apiKeyInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateService(serviceId, payload);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`${serviceId} configuration saved`);
|
||||||
|
setEditingService(null);
|
||||||
|
setApiKeyInput('');
|
||||||
|
loadServices();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to save configuration');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to save configuration');
|
||||||
|
} finally {
|
||||||
|
setSavingService(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle service enabled status
|
||||||
|
const handleToggleEnabled = async (service: ServiceListItem) => {
|
||||||
|
setSavingService(service.id);
|
||||||
|
try {
|
||||||
|
const result = await updateService(service.id, { enabled: !service.enabled });
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`${service.name} ${!service.enabled ? 'enabled' : 'disabled'}`);
|
||||||
|
loadServices();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to update service');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to update service');
|
||||||
|
} finally {
|
||||||
|
setSavingService(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete service config (remove API key)
|
||||||
|
const handleDeleteConfig = async (serviceId: string) => {
|
||||||
|
if (!confirm(`Are you sure you want to remove the API key for "${serviceId}"?`)) return;
|
||||||
|
|
||||||
|
setSavingService(serviceId);
|
||||||
|
try {
|
||||||
|
const result = await deleteService(serviceId);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`${serviceId} configuration removed`);
|
||||||
|
loadServices();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to remove configuration');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to remove configuration');
|
||||||
|
} finally {
|
||||||
|
setSavingService(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading skeleton
|
||||||
|
const LoadingSkeleton = () => (
|
||||||
|
<div className="space-y-3 p-4">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<div key={i} className="flex items-center gap-3 p-4 bg-surface-base/50 rounded-lg">
|
||||||
|
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Skeleton className="h-4 w-32" />
|
||||||
|
<Skeleton className="h-3 w-48" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-6 w-12 rounded-full" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Service item component
|
||||||
|
const ServiceItem = ({ service }: { service: ServiceListItem }) => {
|
||||||
|
const isEditing = editingService === service.id;
|
||||||
|
const isSaving = savingService === service.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-surface-base/50 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Service Header */}
|
||||||
|
<div className="flex items-center gap-4 p-4">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary-500/10 flex items-center justify-center">
|
||||||
|
<Globe size={20} className="text-primary-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-fg-secondary">{service.name}</span>
|
||||||
|
<a
|
||||||
|
href={service.website}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-fg-subtle hover:text-primary-400 transition-colors"
|
||||||
|
title="Visit website"
|
||||||
|
>
|
||||||
|
<ExternalLink size={12} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-fg-subtle truncate">{service.description}</p>
|
||||||
|
<div className="text-xs text-fg-subtle flex items-center gap-2 mt-1">
|
||||||
|
{service.hasApiKey ? (
|
||||||
|
<span className="text-green-400 flex items-center gap-1">
|
||||||
|
<Key size={10} />
|
||||||
|
API Key configured
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-yellow-400 flex items-center gap-1">
|
||||||
|
<Key size={10} />
|
||||||
|
No API Key
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Enable Toggle */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-fg-muted">
|
||||||
|
{service.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={service.enabled}
|
||||||
|
onCheckedChange={() => handleToggleEnabled(service)}
|
||||||
|
disabled={isSaving || !service.hasApiKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Configure Button */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleStartEdit(service.id)}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 size={14} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Key size={14} className="mr-1" />
|
||||||
|
)}
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edit Panel */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isEditing && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-4 pb-4 pt-2 border-t border-line/50 space-y-4">
|
||||||
|
{/* API Key Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-fg-muted flex items-center gap-2">
|
||||||
|
<Key size={14} />
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
type={showApiKey ? 'text' : 'password'}
|
||||||
|
value={apiKeyInput}
|
||||||
|
onChange={(e) => setApiKeyInput(e.target.value)}
|
||||||
|
placeholder={service.hasApiKey ? '••••••••••••••••' : 'Enter API key'}
|
||||||
|
className="pr-10"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowApiKey(!showApiKey)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-fg-subtle hover:text-fg-secondary"
|
||||||
|
>
|
||||||
|
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-fg-subtle">
|
||||||
|
Get your API key from{' '}
|
||||||
|
<a
|
||||||
|
href={service.website}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-400 hover:underline"
|
||||||
|
>
|
||||||
|
{service.website}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
{service.hasApiKey && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteConfig(service.id)}
|
||||||
|
className="text-red-400 hover:text-red-300"
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Remove API Key
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingService(null);
|
||||||
|
setApiKeyInput('');
|
||||||
|
}}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSaveService(service.id, service.enabled)}
|
||||||
|
disabled={isSaving || (!apiKeyInput && !service.hasApiKey)}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 size={14} className="animate-spin mr-1" />
|
||||||
|
) : (
|
||||||
|
<Check size={14} className="mr-1" />
|
||||||
|
)}
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
variants={modalOverlay}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 bg-black/50 flex z-50',
|
||||||
|
responsive ? 'items-end md:items-center justify-center' : 'items-center justify-center'
|
||||||
|
)}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={modalContent}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
transition={smoothTransition}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className={cn(
|
||||||
|
'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col',
|
||||||
|
responsive
|
||||||
|
? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg'
|
||||||
|
: 'rounded-lg w-full max-w-lg mx-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between border-b border-line',
|
||||||
|
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{responsive && (
|
||||||
|
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
|
||||||
|
)}
|
||||||
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Globe size={20} className="text-primary-400" />
|
||||||
|
External Services
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-fg-subtle">
|
||||||
|
Configure API keys for third-party services
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={refreshing}
|
||||||
|
title="Refresh"
|
||||||
|
className={cn(responsive && 'min-w-[44px] min-h-[44px]')}
|
||||||
|
>
|
||||||
|
<RefreshCw size={18} className={cn(refreshing && 'animate-spin')} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onClose}
|
||||||
|
className={cn(responsive && 'min-w-[44px] min-h-[44px]')}
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services List */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{loading ? (
|
||||||
|
<LoadingSkeleton />
|
||||||
|
) : services.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
|
||||||
|
<Globe size={48} className="mb-4 opacity-50" />
|
||||||
|
<p className="text-center">No services available</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className={cn('space-y-3', responsive ? 'p-4' : 'p-4')}
|
||||||
|
>
|
||||||
|
{services.map((service) => (
|
||||||
|
<ServiceItem key={service.id} service={service} />
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'border-t border-line text-xs text-fg-subtle text-center',
|
||||||
|
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Config stored in{' '}
|
||||||
|
<code className="font-mono bg-surface-base px-1 rounded">~/.ai-assistant/providers.json</code>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -105,6 +105,12 @@ export {
|
|||||||
stopLSPServer,
|
stopLSPServer,
|
||||||
getRunningLSPServers,
|
getRunningLSPServers,
|
||||||
getLSPDiagnostics,
|
getLSPDiagnostics,
|
||||||
|
// Services API
|
||||||
|
listServices,
|
||||||
|
getService,
|
||||||
|
updateService,
|
||||||
|
deleteService,
|
||||||
|
type ServiceListItem,
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -235,6 +241,7 @@ export { AgentEditor } from './components/AgentEditor.js';
|
|||||||
export { AgentDefaultsEditor } from './components/AgentDefaultsEditor.js';
|
export { AgentDefaultsEditor } from './components/AgentDefaultsEditor.js';
|
||||||
export { ProvidersPanel } from './components/ProvidersPanel.js';
|
export { ProvidersPanel } from './components/ProvidersPanel.js';
|
||||||
export { ProviderEditor } from './components/ProviderEditor.js';
|
export { ProviderEditor } from './components/ProviderEditor.js';
|
||||||
|
export { ServicesPanel } from './components/ServicesPanel.js';
|
||||||
export { CheckpointPanel } from './components/CheckpointPanel.js';
|
export { CheckpointPanel } from './components/CheckpointPanel.js';
|
||||||
export { CheckpointDiffViewer } from './components/CheckpointDiffViewer.js';
|
export { CheckpointDiffViewer } from './components/CheckpointDiffViewer.js';
|
||||||
export { RestoreDialog } from './components/RestoreDialog.js';
|
export { RestoreDialog } from './components/RestoreDialog.js';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
AgentsPanel,
|
AgentsPanel,
|
||||||
CheckpointPanel,
|
CheckpointPanel,
|
||||||
ProvidersPanel,
|
ProvidersPanel,
|
||||||
|
ServicesPanel,
|
||||||
LSPPanel,
|
LSPPanel,
|
||||||
DiagnosticsPanel,
|
DiagnosticsPanel,
|
||||||
SessionPanel,
|
SessionPanel,
|
||||||
@@ -37,6 +38,7 @@ export function App() {
|
|||||||
const [showAgents, setShowAgents] = useState(false);
|
const [showAgents, setShowAgents] = useState(false);
|
||||||
const [showCheckpoints, setShowCheckpoints] = useState(false);
|
const [showCheckpoints, setShowCheckpoints] = useState(false);
|
||||||
const [showProviders, setShowProviders] = useState(false);
|
const [showProviders, setShowProviders] = useState(false);
|
||||||
|
const [showServices, setShowServices] = useState(false);
|
||||||
const [showLSP, setShowLSP] = useState(false);
|
const [showLSP, setShowLSP] = useState(false);
|
||||||
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
||||||
const [showSessions, setShowSessions] = useState(false);
|
const [showSessions, setShowSessions] = useState(false);
|
||||||
@@ -194,6 +196,7 @@ export function App() {
|
|||||||
onOpenAgents={() => setShowAgents(true)}
|
onOpenAgents={() => setShowAgents(true)}
|
||||||
onOpenCheckpoints={() => setShowCheckpoints(true)}
|
onOpenCheckpoints={() => setShowCheckpoints(true)}
|
||||||
onOpenProviders={() => setShowProviders(true)}
|
onOpenProviders={() => setShowProviders(true)}
|
||||||
|
onOpenServices={() => setShowServices(true)}
|
||||||
onOpenLSP={() => setShowLSP(true)}
|
onOpenLSP={() => setShowLSP(true)}
|
||||||
onOpenDiagnostics={() => setShowDiagnostics(true)}
|
onOpenDiagnostics={() => setShowDiagnostics(true)}
|
||||||
onOpenSessions={() => setShowSessions(true)}
|
onOpenSessions={() => setShowSessions(true)}
|
||||||
@@ -236,6 +239,9 @@ export function App() {
|
|||||||
{/* Providers 面板 */}
|
{/* Providers 面板 */}
|
||||||
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} responsive />}
|
{showProviders && <ProvidersPanel onClose={() => setShowProviders(false)} responsive />}
|
||||||
|
|
||||||
|
{/* Services 面板 */}
|
||||||
|
{showServices && <ServicesPanel onClose={() => setShowServices(false)} responsive />}
|
||||||
|
|
||||||
{/* LSP 面板 */}
|
{/* LSP 面板 */}
|
||||||
{showLSP && (
|
{showLSP && (
|
||||||
<LSPPanel
|
<LSPPanel
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare } from 'lucide-react';
|
import { MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare, Globe } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +33,7 @@ interface ChatPageProps {
|
|||||||
onOpenAgents?: () => void;
|
onOpenAgents?: () => void;
|
||||||
onOpenCheckpoints?: () => void;
|
onOpenCheckpoints?: () => void;
|
||||||
onOpenProviders?: () => void;
|
onOpenProviders?: () => void;
|
||||||
|
onOpenServices?: () => void;
|
||||||
onOpenLSP?: () => void;
|
onOpenLSP?: () => void;
|
||||||
onOpenDiagnostics?: () => void;
|
onOpenDiagnostics?: () => void;
|
||||||
onOpenSessions?: () => void;
|
onOpenSessions?: () => void;
|
||||||
@@ -61,6 +62,7 @@ export function ChatPage({
|
|||||||
onOpenAgents,
|
onOpenAgents,
|
||||||
onOpenCheckpoints,
|
onOpenCheckpoints,
|
||||||
onOpenProviders,
|
onOpenProviders,
|
||||||
|
onOpenServices,
|
||||||
onOpenLSP,
|
onOpenLSP,
|
||||||
onOpenDiagnostics,
|
onOpenDiagnostics,
|
||||||
onOpenSessions,
|
onOpenSessions,
|
||||||
@@ -197,6 +199,7 @@ export function ChatPage({
|
|||||||
items={[
|
items={[
|
||||||
{ icon: History, label: 'Checkpoints', onClick: onOpenCheckpoints },
|
{ icon: History, label: 'Checkpoints', onClick: onOpenCheckpoints },
|
||||||
{ icon: Server, label: 'Model Providers', onClick: onOpenProviders },
|
{ icon: Server, label: 'Model Providers', onClick: onOpenProviders },
|
||||||
|
{ icon: Globe, label: 'External Services', onClick: onOpenServices },
|
||||||
{ icon: Bot, label: 'Agent Presets', onClick: onOpenAgents },
|
{ icon: Bot, label: 'Agent Presets', onClick: onOpenAgents },
|
||||||
{ icon: Zap, label: 'Hooks', onClick: onOpenHooks },
|
{ icon: Zap, label: 'Hooks', onClick: onOpenHooks },
|
||||||
{ icon: Plug, label: 'MCP Servers', onClick: onOpenMCP },
|
{ icon: Plug, label: 'MCP Servers', onClick: onOpenMCP },
|
||||||
|
|||||||
Reference in New Issue
Block a user