♿ startup wizard
This commit is contained in:
@@ -53,6 +53,12 @@ function buildAppMenu(win: BrowserWindow): void {
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open…',
|
||||
accelerator: 'CmdOrCtrl+Shift+O',
|
||||
click: () => send(win, 'openProject')
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Save',
|
||||
accelerator: 'CmdOrCtrl+S',
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function App(): JSX.Element {
|
||||
const {
|
||||
setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme,
|
||||
loadSession, revisionPanelOpen, toggleRevisionPanel, fontSize, setFontSize,
|
||||
openProjectSearch
|
||||
openProjectSearch, clearActiveFile
|
||||
} = useEditorStore()
|
||||
const [sidebarOpen, setSidebarOpen] = useState(
|
||||
() => localStorage.getItem('sidebarOpen') !== 'false'
|
||||
@@ -23,12 +23,26 @@ export default function App(): JSX.Element {
|
||||
() => localStorage.getItem('chatOpen') !== 'false'
|
||||
)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [isFirstRun, setIsFirstRun] = useState(false)
|
||||
|
||||
// Load file tree, apply persisted theme, and restore last session
|
||||
const switchProject = async (newPath: string): Promise<void> => {
|
||||
await window.api.writeConfig({ projectPath: newPath })
|
||||
clearActiveFile()
|
||||
loadSession()
|
||||
window.api.listFiles().then(setFileTree)
|
||||
}
|
||||
|
||||
// Load file tree, apply persisted theme, restore session, and detect first run
|
||||
useEffect(() => {
|
||||
window.api.listFiles().then(setFileTree)
|
||||
document.documentElement.classList.toggle('light', theme === 'light')
|
||||
loadSession()
|
||||
window.api.readConfig().then((cfg) => {
|
||||
if (!cfg.apiKey || !cfg.projectPath) {
|
||||
setIsFirstRun(true)
|
||||
setSettingsOpen(true)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Handle menu actions sent from the main process
|
||||
@@ -58,6 +72,9 @@ export default function App(): JSX.Element {
|
||||
openProjectSearch()
|
||||
} else if (action === 'openSettings') {
|
||||
setSettingsOpen(true)
|
||||
} else if (action === 'openProject') {
|
||||
const picked = await window.api.pickProjectFolder()
|
||||
if (picked) await switchProject(picked)
|
||||
}
|
||||
})
|
||||
}, [activeFilePath, isDirty, activeFileContent, fontSize])
|
||||
@@ -136,7 +153,17 @@ export default function App(): JSX.Element {
|
||||
<ChatPanel />
|
||||
</aside>
|
||||
<ProjectSearchModal />
|
||||
{settingsOpen && <SettingsDialog onClose={() => setSettingsOpen(false)} />}
|
||||
{settingsOpen && (
|
||||
<SettingsDialog
|
||||
onClose={() => { setSettingsOpen(false); setIsFirstRun(false) }}
|
||||
isSetup={isFirstRun}
|
||||
onProjectChanged={() => {
|
||||
clearActiveFile()
|
||||
loadSession()
|
||||
window.api.listFiles().then(setFileTree)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
.settings-setup-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
padding: 0 20px 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
@@ -3,9 +3,11 @@ import './Settings.css'
|
||||
|
||||
interface Props {
|
||||
onClose: () => void
|
||||
onProjectChanged?: () => void
|
||||
isSetup?: boolean
|
||||
}
|
||||
|
||||
export function SettingsDialog({ onClose }: Props): JSX.Element {
|
||||
export function SettingsDialog({ onClose, onProjectChanged, isSetup }: Props): JSX.Element {
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [projectPath, setProjectPath] = useState('')
|
||||
const [originalPath, setOriginalPath] = useState('')
|
||||
@@ -21,15 +23,16 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSetup) return
|
||||
const handler = (e: KeyboardEvent): void => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [onClose])
|
||||
}, [onClose, isSetup])
|
||||
|
||||
const handleOverlayClick = (e: React.MouseEvent): void => {
|
||||
if (e.target === overlayRef.current) onClose()
|
||||
if (!isSetup && e.target === overlayRef.current) onClose()
|
||||
}
|
||||
|
||||
const handleBrowse = async (): Promise<void> => {
|
||||
@@ -39,36 +42,27 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
|
||||
|
||||
const handleSave = async (): Promise<void> => {
|
||||
await window.api.writeConfig({ apiKey: apiKey || undefined, projectPath: projectPath || undefined })
|
||||
if (projectPath !== originalPath) onProjectChanged?.()
|
||||
setOriginalPath(projectPath)
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
setTimeout(() => { setSaved(false); onClose() }, 800)
|
||||
}
|
||||
|
||||
const pathChanged = projectPath !== originalPath
|
||||
|
||||
return (
|
||||
<div className="settings-overlay" ref={overlayRef} onClick={handleOverlayClick}>
|
||||
<div className="settings-dialog" role="dialog" aria-modal="true" aria-label="Preferences">
|
||||
<div className="settings-dialog" role="dialog" aria-modal="true" aria-label={isSetup ? 'Welcome' : 'Preferences'}>
|
||||
<div className="settings-header">
|
||||
<span className="settings-title">Preferences</span>
|
||||
<button className="settings-close" onClick={onClose} aria-label="Close">✕</button>
|
||||
<span className="settings-title">{isSetup ? 'Welcome to Hohoff' : 'Preferences'}</span>
|
||||
{!isSetup && (
|
||||
<button className="settings-close" onClick={onClose} aria-label="Close">✕</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="settings-body">
|
||||
<div className="settings-field">
|
||||
<label className="settings-label" htmlFor="settings-api-key">Anthropic API Key</label>
|
||||
<input
|
||||
id="settings-api-key"
|
||||
className="settings-input"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="sk-ant-…"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<p className="settings-hint">Changes take effect immediately — no restart needed.</p>
|
||||
</div>
|
||||
{isSetup && (
|
||||
<p className="settings-setup-subtitle">Configure your project folder and API key to get started.</p>
|
||||
)}
|
||||
|
||||
<div className="settings-body">
|
||||
<div className="settings-field">
|
||||
<label className="settings-label" htmlFor="settings-project-path">Project Folder</label>
|
||||
<div className="settings-path-row">
|
||||
@@ -83,16 +77,30 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
|
||||
/>
|
||||
<button className="settings-browse-btn" onClick={handleBrowse}>Browse…</button>
|
||||
</div>
|
||||
{pathChanged && (
|
||||
<p className="settings-hint settings-hint--warn">Restart the app to load the new project.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="settings-field">
|
||||
<label className="settings-label" htmlFor="settings-api-key">Anthropic API Key</label>
|
||||
<input
|
||||
id="settings-api-key"
|
||||
className="settings-input"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="sk-ant-…"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<p className="settings-hint">Used for AI features. Changes take effect immediately.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-footer">
|
||||
<span className="settings-config-path">Config: ~/.hohoff/config.json</span>
|
||||
<div className="settings-footer-actions">
|
||||
<button className="settings-btn settings-btn--secondary" onClick={onClose}>Cancel</button>
|
||||
{!isSetup && (
|
||||
<button className="settings-btn settings-btn--secondary" onClick={onClose}>Cancel</button>
|
||||
)}
|
||||
<button className="settings-btn settings-btn--primary" onClick={handleSave}>
|
||||
{saved ? 'Saved ✓' : 'Save'}
|
||||
</button>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user