4adafa1936
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>
240 lines
9.1 KiB
TypeScript
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>
|
|
)
|
|
}
|