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:
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Shared API Types
|
||||
*/
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: string;
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
agent: {
|
||||
coreAvailable: boolean;
|
||||
};
|
||||
auth: {
|
||||
enabled: boolean;
|
||||
tokenCount: number;
|
||||
};
|
||||
stats: {
|
||||
sessions: number;
|
||||
websocket: { connections: number };
|
||||
sse: { connections: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
size: number;
|
||||
modified: string;
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
export interface FileListResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
parent: string | null;
|
||||
files: FileInfo[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileReadResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
path: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
modified: string;
|
||||
content: string;
|
||||
encoding: 'utf-8' | 'base64';
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileTreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
children?: FileTreeNode[];
|
||||
}
|
||||
|
||||
export interface FileTreeResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
path: string;
|
||||
tree: FileTreeNode[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
model: string;
|
||||
maxTokens: number;
|
||||
temperature: number;
|
||||
workdir: string;
|
||||
allowedPaths: string[];
|
||||
deniedPaths: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user