feat(ui): 创建共享 UI 组件包

将 web 和 desktop 的重复代码抽取到 @ai-assistant/ui 包:

- 添加可配置的 API 客户端 (configureApiClient)
- 迁移共享组件: ChatMessage, ChatInput, Sidebar, FileBrowser, ConfigPanel
- 迁移共享 hook: useChat
- 添加 responsive prop 支持响应式布局
- 更新 web/desktop 依赖并删除重复代码
This commit is contained in:
2025-12-12 15:52:53 +08:00
parent 563224fa73
commit 68ab6a2016
30 changed files with 711 additions and 1388 deletions
+165
View File
@@ -0,0 +1,165 @@
/**
* Configurable API Client
*/
import type {
Session,
Message,
HealthStatus,
FileListResponse,
FileReadResponse,
FileTreeResponse,
ServerConfig,
} from './types.js';
// Re-export types
export type {
Session,
Message,
HealthStatus,
FileInfo,
FileListResponse,
FileReadResponse,
FileTreeNode,
FileTreeResponse,
ServerConfig,
} from './types.js';
// API Configuration
interface ApiConfig {
baseUrl: string;
wsBaseUrl: () => string;
healthUrl: () => string;
}
let apiConfig: ApiConfig = {
baseUrl: '/api',
wsBaseUrl: () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.host}/api`;
},
healthUrl: () => '/health',
};
/**
* Configure API client for different environments
*/
export function configureApiClient(config: {
baseUrl: string;
wsBaseUrl: string | (() => string);
healthUrl?: string | (() => string);
}) {
apiConfig = {
baseUrl: config.baseUrl,
wsBaseUrl:
typeof config.wsBaseUrl === 'function'
? config.wsBaseUrl
: () => config.wsBaseUrl as string,
healthUrl: config.healthUrl
? typeof config.healthUrl === 'function'
? config.healthUrl
: () => config.healthUrl as string
: () => '/health',
};
}
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const response = await fetch(`${apiConfig.baseUrl}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error(error.error || `HTTP ${response.status}`);
}
return response.json();
}
// Health
export async function getHealth(): Promise<HealthStatus> {
const healthUrl = apiConfig.healthUrl();
const response = await fetch(healthUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// Sessions
export async function listSessions(): Promise<{ success: boolean; data: Session[] }> {
return request('GET', '/sessions');
}
export async function createSession(name?: string): Promise<{ success: boolean; data: Session }> {
return request('POST', '/sessions', { name });
}
export async function getSession(id: string): Promise<{ success: boolean; data: Session }> {
return request('GET', `/sessions/${id}`);
}
export async function deleteSession(id: string): Promise<{ success: boolean }> {
return request('DELETE', `/sessions/${id}`);
}
// Messages
export async function getMessages(
sessionId: string
): Promise<{ success: boolean; data: Message[] }> {
return request('GET', `/sessions/${sessionId}/messages`);
}
export async function sendMessage(
sessionId: string,
content: string
): Promise<{ success: boolean; data: Message }> {
return request('POST', `/sessions/${sessionId}/messages`, { content });
}
// WebSocket
export function createWebSocket(sessionId: string): WebSocket {
const wsBase = apiConfig.wsBaseUrl();
return new WebSocket(`${wsBase}/ws/${sessionId}`);
}
// Files
export async function getWorkingDirectory(): Promise<{
success: boolean;
data: { workingDirectory: string; separator: string };
}> {
return request('GET', '/files');
}
export async function listFiles(
path: string = '.',
showHidden: boolean = false
): Promise<FileListResponse> {
const params = new URLSearchParams({ path });
if (showHidden) params.set('hidden', 'true');
return request('GET', `/files/list?${params}`);
}
export async function readFile(path: string): Promise<FileReadResponse> {
return request('GET', `/files/read?path=${encodeURIComponent(path)}`);
}
export async function getFileTree(path: string = '.', depth: number = 3): Promise<FileTreeResponse> {
const params = new URLSearchParams({ path, depth: String(depth) });
return request('GET', `/files/tree?${params}`);
}
// Config
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);
}