Files
bilin/src/renderer/components/settings/SettingsPage.tsx
T
bing 4adafa1936 feat: ship v1.0.0 with character graph and writing analytics
Add P7 Cytoscape relationship graph with setting CRUD, P9 cockpit analytics driven by writing_sessions, chapter POV selection, and full test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 10:48:31 +08:00

240 lines
9.1 KiB
TypeScript

import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { ThemeId, Language, PomodoroDuration } from '@shared/types'
import { useSettingsStore } from '@renderer/stores/useSettingsStore'
import { ShortcutEditor } from '@renderer/components/settings/ShortcutEditor'
import { AiSettingsPage } from '@renderer/components/settings/AiSettingsPage'
const THEMES: { id: ThemeId; key: string }[] = [
{ id: 'default', key: 'theme.default' },
{ id: 'bamboo', key: 'theme.bamboo' },
{ id: 'moonlit', key: 'theme.moonlit' },
{ id: 'ricepaper', key: 'theme.ricepaper' },
{ id: 'mist', key: 'theme.mist' },
{ id: 'teagarden', key: 'theme.teagarden' },
{ id: 'twilight', key: 'theme.twilight' }
]
export function SettingsPage(): React.JSX.Element {
const { t } = useTranslation()
const settings = useSettingsStore()
const [section, setSection] = useState<'general' | 'ai' | 'shortcuts' | 'about'>('general')
return (
<div id="settings-page">
<div className="settings-sidebar">
<div
className={`settings-nav-item ${section === 'general' ? 'active' : ''}`}
onClick={() => setSection('general')}
role="button"
tabIndex={0}
>
{t('settings.general')}
</div>
<div
className={`settings-nav-item ${section === 'ai' ? 'active' : ''}`}
onClick={() => setSection('ai')}
role="button"
tabIndex={0}
data-testid="settings-nav-ai"
>
{t('settings.ai')}
</div>
<div
className={`settings-nav-item ${section === 'shortcuts' ? 'active' : ''}`}
onClick={() => setSection('shortcuts')}
role="button"
tabIndex={0}
>
{t('settings.shortcuts')}
</div>
<div
className={`settings-nav-item ${section === 'about' ? 'active' : ''}`}
onClick={() => setSection('about')}
role="button"
tabIndex={0}
>
{t('settings.about')}
</div>
</div>
<div className="settings-content">
<h2 className="settings-heading">{t('settings.title')}</h2>
{section === 'general' && (
<>
<div className="setting-item">
<span>{t('settings.penName')}</span>
<input
data-testid="settings-pen-name"
className="form-control form-control--narrow"
value={settings.penName}
onChange={(e) => void settings.update({ penName: e.target.value })}
/>
</div>
<div className="setting-item">
<span>{t('settings.theme')}</span>
<select
data-testid="settings-theme"
className="form-control"
value={settings.theme}
onChange={(e) => void settings.update({ theme: e.target.value as ThemeId })}
>
{THEMES.map((th) => (
<option key={th.id} value={th.id}>
{t(th.key)}
</option>
))}
</select>
</div>
<div className="setting-item">
<span>{t('settings.language')}</span>
<select
data-testid="settings-language"
className="form-control"
value={settings.language}
onChange={(e) => void settings.update({ language: e.target.value as Language })}
>
<option value="zh-CN"></option>
<option value="en">English</option>
</select>
</div>
<div className="setting-item">
<span>{t('settings.updateSchedule')}</span>
<select
data-testid="settings-update-schedule"
className="form-control"
value={settings.updateSchedule}
onChange={(e) =>
void settings.update({
updateSchedule: e.target.value as typeof settings.updateSchedule
})
}
>
<option value="none">{t('settings.updateSchedule.none')}</option>
<option value="daily">{t('settings.updateSchedule.daily')}</option>
<option value="alternate">{t('settings.updateSchedule.alternate')}</option>
<option value="weekly">{t('settings.updateSchedule.weekly')}</option>
</select>
</div>
<div className="setting-item">
<span>{t('settings.stockBufferThreshold')}</span>
<input
type="number"
min={1}
max={20}
data-testid="settings-stock-threshold"
className="form-control form-control--narrow"
value={settings.stockBufferThreshold}
onChange={(e) =>
void settings.update({
stockBufferThreshold: Math.min(20, Math.max(1, Number(e.target.value) || 3))
})
}
/>
</div>
<div className="setting-item">
<span>{t('settings.dailyWordGoal')}</span>
<input
type="number"
min={0}
data-testid="settings-daily-word-goal"
className="form-control form-control--narrow"
value={settings.dailyWordGoal}
onChange={(e) =>
void settings.update({
dailyWordGoal: Math.max(0, Number(e.target.value) || 0)
})
}
/>
</div>
<div className="setting-item">
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
type="checkbox"
data-testid="settings-knowledge-auto-extract"
checked={settings.knowledgeAutoExtract !== false}
onChange={(e) =>
void settings.update({ knowledgeAutoExtract: e.target.checked })
}
/>
{t('settings.knowledgeAutoExtract')}
</label>
</div>
<div className="setting-item">
<span>{t('settings.pomodoroDuration')}</span>
<select
data-testid="settings-pomodoro-duration"
className="form-control form-control--narrow"
value={settings.pomodoroDurationMinutes ?? 25}
onChange={(e) =>
void settings.update({
pomodoroDurationMinutes: Number(e.target.value) as PomodoroDuration
})
}
>
<option value={15}>15</option>
<option value={25}>25</option>
<option value={45}>45</option>
</select>
</div>
<div className="setting-item">
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
type="checkbox"
data-testid="settings-system-notifications"
checked={settings.enableSystemNotifications === true}
onChange={(e) =>
void settings.update({ enableSystemNotifications: e.target.checked })
}
/>
{t('settings.systemNotifications')}
</label>
</div>
<div className="setting-item">
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
type="checkbox"
data-testid="settings-goal-notifications"
checked={settings.enableGoalNotifications !== false}
onChange={(e) =>
void settings.update({ enableGoalNotifications: e.target.checked })
}
/>
{t('settings.goalNotifications')}
</label>
</div>
<div className="setting-item">
<span>{t('settings.extractThreshold')}</span>
<input
type="number"
min={0}
max={1}
step={0.05}
data-testid="settings-extract-threshold"
className="form-control form-control--narrow"
value={settings.knowledgeExtractConfidenceThreshold ?? 0.8}
onChange={(e) =>
void settings.update({
knowledgeExtractConfidenceThreshold: Math.min(
1,
Math.max(0, Number(e.target.value) || 0.8)
)
})
}
/>
</div>
</>
)}
{section === 'ai' && <AiSettingsPage />}
{section === 'shortcuts' && <ShortcutEditor />}
{section === 'about' && (
<p style={{ color: 'var(--text-secondary)', lineHeight: 1.8 }}>
{t('app.name')} v1.0.0
<br />
{t('app.tagline')}
</p>
)}
</div>
</div>
)
}