feat: 添加配置面板组件
- 新增 ConfigPanel 组件,支持模型选择、参数调整 - 添加配置 API (getConfig/updateConfig) - 在 App.tsx 中集成配置按钮和面板
This commit is contained in:
+47
-17
@@ -6,12 +6,14 @@ import { useState, useEffect } from 'react';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import { ChatPage } from './pages/Chat';
|
||||
import { FileBrowser } from './components/FileBrowser';
|
||||
import { ConfigPanel } from './components/ConfigPanel';
|
||||
import { listSessions, createSession, type Session } from './api/client';
|
||||
|
||||
export function App() {
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const [showFileBrowser, setShowFileBrowser] = useState(false);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
|
||||
// 初始化:加载或创建会话
|
||||
useEffect(() => {
|
||||
@@ -64,23 +66,48 @@ export function App() {
|
||||
onCreateSession={handleCreateSession}
|
||||
/>
|
||||
|
||||
{/* 文件浏览器切换按钮 */}
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(!showFileBrowser)}
|
||||
className={`absolute top-3 right-4 z-10 p-2 rounded-lg transition-colors ${
|
||||
showFileBrowser ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/* 工具栏按钮 */}
|
||||
<div className="absolute top-3 right-4 z-10 flex gap-2">
|
||||
{/* 配置按钮 */}
|
||||
<button
|
||||
onClick={() => setShowConfig(true)}
|
||||
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors"
|
||||
title="Settings"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* 文件浏览器切换按钮 */}
|
||||
<button
|
||||
onClick={() => setShowFileBrowser(!showFileBrowser)}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
showFileBrowser ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex">
|
||||
{/* 聊天区域 */}
|
||||
@@ -105,6 +132,9 @@ export function App() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 配置面板 */}
|
||||
{showConfig && <ConfigPanel onClose={() => setShowConfig(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,3 +161,21 @@ export async function getFileTree(path: string = '.', depth: number = 3): Promis
|
||||
const params = new URLSearchParams({ path, depth: String(depth) });
|
||||
return request('GET', `/files/tree?${params}`);
|
||||
}
|
||||
|
||||
// Config
|
||||
export interface ServerConfig {
|
||||
model: string;
|
||||
maxTokens: number;
|
||||
temperature: number;
|
||||
workdir: string;
|
||||
allowedPaths: string[];
|
||||
deniedPaths: string[];
|
||||
}
|
||||
|
||||
export async function getConfig(): Promise<{ success: boolean; data: ServerConfig }> {
|
||||
return request('GET', '/config');
|
||||
}
|
||||
|
||||
export async function updateConfig(config: Partial<ServerConfig>): Promise<{ success: boolean; data: ServerConfig }> {
|
||||
return request('PATCH', '/config', config);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* ConfigPanel Component
|
||||
*
|
||||
* 配置面板组件
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getConfig, updateConfig, type ServerConfig } from '../api/client';
|
||||
|
||||
interface ConfigPanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// 可用的模型列表
|
||||
const AVAILABLE_MODELS = [
|
||||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
|
||||
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' },
|
||||
{ id: 'claude-3-opus-20240229', name: 'Claude 3 Opus' },
|
||||
{ id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku' },
|
||||
];
|
||||
|
||||
export function ConfigPanel({ onClose }: ConfigPanelProps) {
|
||||
const [config, setConfig] = useState<ServerConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
model: '',
|
||||
maxTokens: 8192,
|
||||
temperature: 0.7,
|
||||
workdir: '',
|
||||
});
|
||||
|
||||
// 加载配置
|
||||
useEffect(() => {
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await getConfig();
|
||||
setConfig(response.data);
|
||||
setFormData({
|
||||
model: response.data.model,
|
||||
maxTokens: response.data.maxTokens,
|
||||
temperature: response.data.temperature,
|
||||
workdir: response.data.workdir,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load config');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await updateConfig(formData);
|
||||
setConfig(response.data);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 2000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save config');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置为默认值
|
||||
const handleReset = () => {
|
||||
if (config) {
|
||||
setFormData({
|
||||
model: config.model,
|
||||
maxTokens: config.maxTokens,
|
||||
temperature: config.temperature,
|
||||
workdir: config.workdir,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="text-gray-400">Loading configuration...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-800 rounded-lg w-full max-w-lg mx-4 max-h-[90vh] overflow-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700">
|
||||
<h2 className="text-lg font-semibold">Configuration</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-700 rounded transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-900/50 border border-red-700 rounded-lg text-red-300 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success message */}
|
||||
{success && (
|
||||
<div className="p-3 bg-green-900/50 border border-green-700 rounded-lg text-green-300 text-sm">
|
||||
Configuration saved successfully!
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
value={formData.model}
|
||||
onChange={(e) => setFormData({ ...formData, model: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
{AVAILABLE_MODELS.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Select the AI model to use for conversations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Max Tokens */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Max Tokens: {formData.maxTokens}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1024"
|
||||
max="32768"
|
||||
step="1024"
|
||||
value={formData.maxTokens}
|
||||
onChange={(e) => setFormData({ ...formData, maxTokens: parseInt(e.target.value) })}
|
||||
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>1K</span>
|
||||
<span>8K</span>
|
||||
<span>16K</span>
|
||||
<span>32K</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Maximum number of tokens in the response
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Temperature */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Temperature: {formData.temperature.toFixed(2)}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={formData.temperature}
|
||||
onChange={(e) => setFormData({ ...formData, temperature: parseFloat(e.target.value) })}
|
||||
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>Precise (0)</span>
|
||||
<span>Balanced (0.5)</span>
|
||||
<span>Creative (1)</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Controls randomness in responses
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Working Directory */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Working Directory
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.workdir}
|
||||
onChange={(e) => setFormData({ ...formData, workdir: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500 font-mono text-sm"
|
||||
placeholder="/path/to/project"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Root directory for file operations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Server Info (Read-only) */}
|
||||
{config && (
|
||||
<div className="pt-4 border-t border-gray-700">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-3">Server Information</h3>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Allowed Paths:</span>
|
||||
<span className="ml-2 text-gray-300">
|
||||
{config.allowedPaths.length || 'All'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Denied Paths:</span>
|
||||
<span className="ml-2 text-gray-300">
|
||||
{config.deniedPaths.length || 'None'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-700 bg-gray-800/50">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-4 py-2 text-sm text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user