Add initial project structure with essential files

- Created .gitignore to exclude build artifacts and dependencies.
- Added index.html as the main entry point for the application.
- Included LICENSE file with Apache 2.0 terms.
- Initialized package.json and package-lock.json for project dependencies.
- Added pnpm-lock.yaml for package management.
- Created QUICKSTART.md for setup instructions.
- Added README.md and README.zh-CN.md for project documentation in English and Chinese.
This commit is contained in:
pftom
2026-04-28 12:25:59 +08:00
commit a98096a042
258 changed files with 67862 additions and 0 deletions
+290
View File
@@ -0,0 +1,290 @@
import { useEffect, useMemo, useState } from 'react';
import { LOCALE_LABEL, LOCALES, useI18n } from '../i18n';
import type { Locale } from '../i18n';
import { AgentIcon } from './AgentIcon';
import type { AgentInfo, AppConfig, ExecMode } from '../types';
interface Props {
initial: AppConfig;
agents: AgentInfo[];
daemonLive: boolean;
welcome?: boolean;
onSave: (cfg: AppConfig) => void;
onClose: () => void;
onRefreshAgents: () => void;
}
const SUGGESTED_MODELS = [
'claude-opus-4-5',
'claude-sonnet-4-5',
'claude-haiku-4-5',
];
export function SettingsDialog({
initial,
agents,
daemonLive,
welcome,
onSave,
onClose,
onRefreshAgents,
}: Props) {
const { t, locale, setLocale } = useI18n();
const [cfg, setCfg] = useState<AppConfig>(initial);
const [showApiKey, setShowApiKey] = useState(false);
// If the daemon goes offline mid-edit, force API mode so the UI doesn't
// pretend Local CLI is selectable.
useEffect(() => {
if (!daemonLive && cfg.mode === 'daemon') {
setCfg((c) => ({ ...c, mode: 'api' }));
}
}, [daemonLive, cfg.mode]);
const installedCount = useMemo(
() => agents.filter((a) => a.available).length,
[agents],
);
const setMode = (mode: ExecMode) => setCfg((c) => ({ ...c, mode }));
const canSave =
cfg.mode === 'daemon'
? Boolean(cfg.agentId && agents.find((a) => a.id === cfg.agentId)?.available)
: Boolean(cfg.apiKey.trim() && cfg.model.trim() && cfg.baseUrl.trim());
return (
<div className="modal-backdrop" onClick={onClose}>
<div
className="modal modal-settings"
role="dialog"
aria-modal="true"
onClick={(e) => e.stopPropagation()}
>
<header className="modal-head">
{welcome ? (
<>
<span className="kicker">{t('settings.welcomeKicker')}</span>
<h2>{t('settings.welcomeTitle')}</h2>
<p className="subtitle">{t('settings.welcomeSubtitle')}</p>
</>
) : (
<>
<span className="kicker">{t('settings.kicker')}</span>
<h2>{t('settings.title')}</h2>
<p className="subtitle">{t('settings.subtitle')}</p>
</>
)}
</header>
<div
className="seg-control"
role="tablist"
aria-label={t('settings.modeAria')}
>
<button
type="button"
role="tab"
aria-selected={cfg.mode === 'daemon'}
className={'seg-btn' + (cfg.mode === 'daemon' ? ' active' : '')}
disabled={!daemonLive}
onClick={() => setMode('daemon')}
title={
daemonLive
? t('settings.modeDaemonHelp')
: t('settings.modeDaemonOffline')
}
>
<span className="seg-title">{t('settings.modeDaemon')}</span>
<span className="seg-meta">
{daemonLive
? t('settings.modeDaemonInstalledMeta', { count: installedCount })
: t('settings.modeDaemonOfflineMeta')}
</span>
</button>
<button
type="button"
role="tab"
aria-selected={cfg.mode === 'api'}
className={'seg-btn' + (cfg.mode === 'api' ? ' active' : '')}
onClick={() => setMode('api')}
>
<span className="seg-title">{t('settings.modeApi')}</span>
<span className="seg-meta">{t('settings.modeApiMeta')}</span>
</button>
</div>
{cfg.mode === 'daemon' ? (
<section className="settings-section">
<div className="section-head">
<div>
<h3>{t('settings.codeAgent')}</h3>
<p className="hint">{t('settings.codeAgentHint')}</p>
</div>
<button
type="button"
className="ghost icon-btn"
onClick={onRefreshAgents}
title={t('settings.rescanTitle')}
>
{t('settings.rescan')}
</button>
</div>
{agents.length === 0 ? (
<div className="empty-card">
{t('settings.noAgentsDetected')}
</div>
) : (
<div className="agent-grid">
{agents.map((a) => {
const active = cfg.agentId === a.id;
return (
<button
type="button"
key={a.id}
className={
'agent-card' +
(active ? ' active' : '') +
(a.available ? '' : ' disabled')
}
onClick={() =>
a.available && setCfg((c) => ({ ...c, agentId: a.id }))
}
disabled={!a.available}
aria-pressed={active}
>
<AgentIcon id={a.id} size={40} />
<div className="agent-card-body">
<div className="agent-card-name">{a.name}</div>
<div className="agent-card-meta">
{a.available ? (
a.version ? (
<span title={a.path ?? ''}>{a.version}</span>
) : (
<span title={a.path ?? ''}>
{t('common.installed')}
</span>
)
) : (
<span className="muted">
{t('common.notInstalled')}
</span>
)}
</div>
</div>
{a.available ? (
<span
className={'status-dot' + (active ? ' active' : '')}
aria-hidden="true"
/>
) : null}
</button>
);
})}
</div>
)}
</section>
) : (
<section className="settings-section">
<div className="section-head">
<h3>{t('settings.apiSection')}</h3>
</div>
<label className="field">
<span className="field-label">{t('settings.apiKey')}</span>
<div className="field-row">
<input
type={showApiKey ? 'text' : 'password'}
placeholder="sk-ant-..."
value={cfg.apiKey}
onChange={(e) => setCfg({ ...cfg, apiKey: e.target.value })}
autoFocus
/>
<button
type="button"
className="ghost icon-btn"
onClick={() => setShowApiKey((v) => !v)}
title={
showApiKey ? t('settings.hideKey') : t('settings.showKey')
}
>
{showApiKey ? t('settings.hide') : t('settings.show')}
</button>
</div>
</label>
<label className="field">
<span className="field-label">{t('settings.model')}</span>
<input
type="text"
value={cfg.model}
list="suggested-models"
onChange={(e) => setCfg({ ...cfg, model: e.target.value })}
/>
<datalist id="suggested-models">
{SUGGESTED_MODELS.map((m) => (
<option value={m} key={m} />
))}
</datalist>
</label>
<label className="field">
<span className="field-label">{t('settings.baseUrl')}</span>
<input
type="text"
value={cfg.baseUrl}
onChange={(e) => setCfg({ ...cfg, baseUrl: e.target.value })}
/>
</label>
<p className="hint">{t('settings.apiHint')}</p>
</section>
)}
<section className="settings-section">
<div className="section-head">
<div>
<h3>{t('settings.language')}</h3>
<p className="hint">{t('settings.languageHint')}</p>
</div>
</div>
<div
className="seg-control"
role="tablist"
aria-label={t('settings.language')}
>
{LOCALES.map((code) => {
const active = locale === code;
return (
<button
key={code}
type="button"
role="tab"
aria-selected={active}
className={'seg-btn' + (active ? ' active' : '')}
onClick={() => setLocale(code as Locale)}
>
<span className="seg-title">{LOCALE_LABEL[code]}</span>
<span className="seg-meta">{code}</span>
</button>
);
})}
</div>
</section>
<footer className="modal-foot">
<button type="button" className="ghost" onClick={onClose}>
{welcome ? t('settings.skipForNow') : t('common.cancel')}
</button>
<button
type="button"
className="primary"
disabled={!canSave}
onClick={() => {
onSave(cfg);
onClose();
}}
>
{welcome ? t('settings.getStarted') : t('common.save')}
</button>
</footer>
</div>
</div>
);
}