Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ ipcMain.handle('wizard:complete', async (_e, input: WizardInput) => {
const b = base()
await startStack(b, process.env)
await waitHealthy(b)
await runAdminFixture(b, input.adminEmail, input.adminPassword)
const admin = await runAdminFixture(b, input.adminEmail, input.adminPassword)
if (admin.code !== 0) {
throw new Error(
`Could not set up the login: ${admin.stderr.trim() || admin.stdout.trim() || 'admin fixture failed'}`
)
}
saveConfig(cfg)
return { ok: true }
} catch (err) {
Expand Down
63 changes: 45 additions & 18 deletions desktop/src/renderer/wizard.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
/**
* First-run wizard. Secrets are auto-generated (no UI); the user picks inference + admin
* login. On submit the main process generates secrets, writes the .env, starts the stack,
* waits for HEALTHY, and runs the admin fixture. The API key is OPTIONALlogin and chat
* creation work without it, and keys can also be set later in the app's Settings (BYOK);
* the field just seeds the gateway for the simplest first run.
* First-run wizard. Secrets are auto-generated (no UI); the user picks inference + sets a
* password. The backend ships a fixed bootstrap admin (admin@lq.ai) and only a
* reset-admin-password command (no create-user), so the login email is admin@lq.aithe
* user can change it later in the app's Settings. The API key is OPTIONAL (login/chat work
* without it; keys can also be set later in Settings).
*/
const ADMIN_EMAIL = 'admin@lq.ai'

interface Snapshot {
state: string
services?: { health: string }[]
}

export function renderWizard(root: HTMLElement, onDone: () => void): void {
root.innerHTML = `
<h1>Welcome to Donna</h1>
<p>Donna runs a private legal-AI workspace on your Mac. This one-time setup creates
your login and starts the engine. The first start downloads AI models and can take
<p>Donna runs a private legal-AI workspace on your Mac. This one-time setup sets your
password and starts the engine. The first start downloads AI models and can take
several minutes.</p>

<div class="step">
Expand All @@ -20,26 +27,40 @@ export function renderWizard(root: HTMLElement, onDone: () => void): void {
</div>

<div class="step">
<h3>2. Create your login</h3>
<input id="email" type="email" placeholder="you@example.com" />
<input id="password" type="password" placeholder="Choose a password (12+ characters)" style="margin-top:8px" />
<h3>2. Set your password</h3>
<p style="margin:4px 0 8px; color:#555">Your login is <strong>${ADMIN_EMAIL}</strong> — you can change it later in Settings → Account.</p>
<input id="password" type="password" placeholder="Choose a password (12+ characters)" />
</div>

<div class="step">
<button id="go">Start Donna</button>
<p id="err" style="color:#c00"></p>
<p id="status"></p>
</div>
`

const $ = <T extends HTMLElement>(id: string): T => document.getElementById(id) as T
const status = $('status')

// Live progress while the stack comes up (replaces a static, misleading message).
window.donna.onState((snap) => {
const s = snap as Snapshot
if (s.state === 'STACK_STARTING') {
const healthy = (s.services ?? []).filter((x) => x.health === 'healthy').length
status.style.color = '#555'
status.textContent = `Starting Donna… ${healthy}/8 services ready (first run pulls images + models; this can take a few minutes).`
} else if (s.state === 'NO_ENGINE') {
status.style.color = '#c00'
status.textContent = "Docker isn't running — start Docker Desktop and try again."
}
})

$('go').addEventListener('click', async () => {
const mode = (document.querySelector('input[name="inf"]:checked') as HTMLInputElement).value
const email = $<HTMLInputElement>('email').value.trim()
const password = $<HTMLInputElement>('password').value
const err = $('err')

if (!email || password.length < 12) {
err.textContent = 'Enter an email and a password of at least 12 characters.'
if (password.length < 12) {
status.style.color = '#c00'
status.textContent = 'Choose a password of at least 12 characters.'
return
}
const inference =
Expand All @@ -49,11 +70,17 @@ export function renderWizard(root: HTMLElement, onDone: () => void): void {

const goBtn = $<HTMLButtonElement>('go')
goBtn.disabled = true
err.textContent = 'Starting… downloading models on first run; this can take a few minutes.'
const res = await window.donna.completeWizard({ inference, adminEmail: email, adminPassword: password })
status.style.color = '#555'
status.textContent = 'Starting Donna…'
const res = await window.donna.completeWizard({
inference,
adminEmail: ADMIN_EMAIL,
adminPassword: password
})
if (res.ok) onDone()
else {
err.textContent = res.error ?? 'Setup failed.'
status.style.color = '#c00'
status.textContent = res.error ?? 'Setup failed.'
goBtn.disabled = false
}
})
Expand Down
Loading