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

173 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}