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