hololake-system-architecture/product-source/guanghu-knowledge-base/src/components/HumanSettings.tsx

173 lines
8.6 KiB
TypeScript
Raw Normal View History

import { useEffect, useState } from 'react';
export interface HumanPreferences {
language: 'system' | 'zh-CN' | 'en';
font: 'system' | 'serif' | 'accessible';
readingSize: number;
appearance: 'lake-night' | 'mist-light' | 'deep-night';
}
export const DEFAULT_HUMAN_PREFERENCES: HumanPreferences = {
language: 'system',
font: 'system',
readingSize: 17,
appearance: 'lake-night',
};
const STORAGE_KEY = 'hololake.human-preferences.v1';
export function loadHumanPreferences(): HumanPreferences {
try {
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
const appearance = stored.appearance === 'eternal-lake' ? 'lake-night' : stored.appearance;
return { ...DEFAULT_HUMAN_PREFERENCES, ...stored, appearance: appearance || DEFAULT_HUMAN_PREFERENCES.appearance };
} catch {
return DEFAULT_HUMAN_PREFERENCES;
}
}
interface Props {
open: boolean;
preferences: HumanPreferences;
onClose: () => void;
onChange: (preferences: HumanPreferences) => void;
onManageServer: () => void;
onAgentChanged?: () => void;
}
export function HumanSettings({ open, preferences, onClose, onChange, onManageServer, onAgentChanged }: Props) {
const [draft, setDraft] = useState(preferences);
const [modelBaseUrl, setModelBaseUrl] = useState('https://api.openai.com/v1');
const [modelName, setModelName] = useState('gpt-4o');
const [modelKey, setModelKey] = useState('');
const [modelConfigured, setModelConfigured] = useState(false);
const [modelOperational, setModelOperational] = useState(false);
const [modelBusy, setModelBusy] = useState(false);
const [modelMessage, setModelMessage] = useState('');
useEffect(() => {
if (!open) return;
setDraft(preferences);
const agent = (window as any).hololake?.agent;
if (!agent?.getConfig) return;
agent.getConfig().then((config: any) => {
setModelBaseUrl(config.baseUrl || 'https://api.openai.com/v1');
setModelName(config.model || 'gpt-4o');
setModelConfigured(Boolean(config.configured));
setModelOperational(Boolean(config.operational));
setModelMessage(config.operational ? '模型服务已验证可用' : config.configured ? '密钥已保存,尚未通过连通验证' : '尚未配置模型服务');
}).catch(() => setModelMessage('无法读取模型配置'));
}, [open, preferences]);
async function saveAndVerifyModel() {
const agent = (window as any).hololake?.agent;
if (!agent?.saveConfig || !agent?.testConfig) {
setModelMessage('模型配置只在 HoloLake 桌面 App 中提供');
return;
}
setModelBusy(true);
setModelMessage('正在保存并验证模型服务…');
try {
await agent.saveConfig({ baseUrl: modelBaseUrl, model: modelName, apiKey: modelKey || undefined });
setModelKey('');
setModelConfigured(true);
await agent.testConfig();
setModelOperational(true);
setModelMessage('模型服务已验证可用');
onAgentChanged?.();
} catch (error) {
setModelOperational(false);
setModelMessage(error instanceof Error ? error.message : String(error));
onAgentChanged?.();
} finally {
setModelBusy(false);
}
}
if (!open) return null;
function save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(draft));
onChange(draft);
onClose();
}
return (
<div className="human-settings-backdrop" role="presentation" onMouseDown={event => {
if (event.target === event.currentTarget) onClose();
}}>
<section className="human-settings" role="dialog" aria-modal="true" aria-labelledby="human-settings-title">
<header className="human-settings-header">
<div><small></small><h2 id="human-settings-title"></h2></div>
<button className="icon-button" onClick={onClose} aria-label="关闭设置">×</button>
</header>
<div className="human-settings-body">
<section className="settings-section">
<div className="settings-section-copy"><strong></strong><small></small></div>
<select value={draft.language} onChange={event => setDraft({ ...draft, language: event.target.value as HumanPreferences['language'] })}>
<option value="system"></option>
<option value="zh-CN"></option>
<option value="en">EnglishBeta</option>
</select>
</section>
<section className="settings-model-card">
<div className="settings-model-heading">
<div><strong></strong><small></small></div>
<span className={modelOperational ? 'verified' : modelConfigured ? 'pending' : ''}>{modelOperational ? '可用' : modelConfigured ? '待验证' : '未配置'}</span>
</div>
<div className="settings-model-fields">
<label><span></span><input value={modelBaseUrl} onChange={event => setModelBaseUrl(event.target.value)} placeholder="https://api.example.com/v1" /></label>
<label><span></span><input value={modelName} onChange={event => setModelName(event.target.value)} placeholder="模型名称" /></label>
<label><span></span><input type="password" value={modelKey} onChange={event => setModelKey(event.target.value)} placeholder={modelConfigured ? '留空则保留现有密钥' : '仅保存在此设备的加密存储'} /></label>
</div>
<div className="settings-model-actions">
<small className={modelOperational ? 'success' : ''}>{modelMessage}</small>
<button className="secondary-button" type="button" onClick={saveAndVerifyModel} disabled={modelBusy || !modelBaseUrl.trim() || !modelName.trim()}>{modelBusy ? '正在验证…' : '保存并验证'}</button>
</div>
</section>
<section className="settings-section">
<div className="settings-section-copy"><strong></strong><small></small></div>
<select value={draft.font} onChange={event => setDraft({ ...draft, font: event.target.value as HumanPreferences['font'] })}>
<option value="system"></option>
<option value="serif"></option>
<option value="accessible"></option>
</select>
</section>
<section className="settings-section reading-size-setting">
<div className="settings-section-copy"><strong></strong><small>{draft.readingSize}px · </small></div>
<div className="reading-size-control">
<button type="button" aria-label="减小正文字号" onClick={() => setDraft({ ...draft, readingSize: Math.max(14, draft.readingSize - 1) })}>A</button>
<input type="range" min="14" max="24" step="1" value={draft.readingSize} onChange={event => setDraft({ ...draft, readingSize: Number(event.target.value) })} />
<button type="button" aria-label="增大正文字号" onClick={() => setDraft({ ...draft, readingSize: Math.min(24, draft.readingSize + 1) })}>A</button>
</div>
</section>
<section className="settings-section">
<div className="settings-section-copy"><strong></strong><small></small></div>
<select value={draft.appearance} onChange={event => setDraft({ ...draft, appearance: event.target.value as HumanPreferences['appearance'] })}>
<option value="lake-night"></option>
<option value="mist-light"></option>
<option value="deep-night"></option>
</select>
</section>
<section className="settings-account-card">
<div><small></small><strong> · </strong><p>访</p></div>
<button className="secondary-button" onClick={onManageServer}></button>
</section>
</div>
<footer className="human-settings-footer">
<button className="secondary-button" onClick={() => setDraft(DEFAULT_HUMAN_PREFERENCES)}></button>
<span />
<button className="secondary-button" onClick={onClose}></button>
<button className="primary-button" onClick={save}></button>
</footer>
</section>
</div>
);
}