import * as React from 'react'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
import { useConfig } from '@/state/config'
import { isTauriHost } from '@/lib/api/desktop'
import { BrainCircuit, Check, Loader2 } from 'lucide-react'
type ToolStatus = {
id: string
name: string
installed: boolean
spool_injected: boolean
hooks_installed: boolean
}
export function OnboardingWizard() {
const cfg = useConfig()
const [step, setStep] = React.useState(1)
const [configPath, setConfigPath] = React.useState('')
const [vaultRoot, setVaultRoot] = React.useState('')
const [actor, setActor] = React.useState('')
const [tools, setTools] = React.useState<ToolStatus[]>([])
const [detecting, setDetecting] = React.useState(false)
const [installing, setInstalling] = React.useState<string | null>(null)
const [msg, setMsg] = React.useState('')
async function pickConfig() {
const sel = await openDialog({ multiple: false, filters: [{ name: 'TOML', extensions: ['toml'] }] })
if (typeof sel === 'string') setConfigPath(sel)
}
async function pickVault() {
const sel = await openDialog({ directory: true, multiple: false })
if (typeof sel === 'string') setVaultRoot(sel)
}
async function detectTools() {
if (!isTauriHost()) return
setDetecting(true)
try {
const res = await tauriInvoke<{ tools: ToolStatus[] }>('desktop_detect_ai_tools')
setTools(res.tools.filter(t => t.installed))
} catch {
// ignore
}
setDetecting(false)
}
async function installAll() {
if (!isTauriHost()) return
setMsg('')
for (const tool of tools) {
if (tool.spool_injected && tool.hooks_installed) continue
setInstalling(tool.id)
try {
await Promise.all([
!tool.spool_injected ? tauriInvoke('desktop_install_tool', { client: tool.id }) : Promise.resolve(),
!tool.hooks_installed ? tauriInvoke('desktop_install_hooks', { client: tool.id }) : Promise.resolve(),
])
} catch { /* ignore */ }
}
setInstalling(null)
setMsg('注入完成')
detectTools()
}
React.useEffect(() => {
if (step === 4) detectTools()
}, [step])
function finish() {
cfg.setConfigPath(configPath)
if (vaultRoot.trim()) cfg.setVaultRoot(vaultRoot)
if (actor.trim()) cfg.setDefaultActor(actor)
}
const allReady = tools.length > 0 && tools.every(t => t.spool_injected && t.hooks_installed)
return (
<div className="flex h-screen flex-col bg-background">
{/* macOS traffic-light drag zone */}
<div
className="h-11 shrink-0 select-none cursor-default"
onMouseDown={(e) => {
if (e.button === 0) void getCurrentWindow().startDragging()
}}
/>
<div className="flex flex-1 items-center justify-center p-6">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-md bg-primary/10">
<BrainCircuit className="h-6 w-6 text-primary" />
</div>
<CardTitle>欢迎使用 spool</CardTitle>
<CardDescription>Step {step} / 4</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{step === 1 && (
<div className="space-y-3">
<Label>配置文件路径 (.toml)</Label>
<p className="text-xs text-muted-foreground">spool 用 toml 配置路由 vault 和 ledger</p>
<div className="flex gap-2">
<Input value={configPath} onChange={e => setConfigPath(e.target.value)} placeholder="/path/to/spool.toml" />
<Button variant="outline" onClick={pickConfig}>浏览</Button>
</div>
<Button className="w-full" disabled={!configPath.trim()} onClick={() => setStep(2)}>下一步</Button>
</div>
)}
{step === 2 && (
<div className="space-y-3">
<Label>Vault 根目录(可选)</Label>
<p className="text-xs text-muted-foreground">留空则使用 config 中的默认值</p>
<div className="flex gap-2">
<Input value={vaultRoot} onChange={e => setVaultRoot(e.target.value)} placeholder="/path/to/vault" />
<Button variant="outline" onClick={pickVault}>浏览</Button>
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={() => setStep(1)}>上一步</Button>
<Button className="flex-1" onClick={() => setStep(3)}>下一步</Button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-3">
<Label>你的名字(默认 actor)</Label>
<p className="text-xs text-muted-foreground">用作 lifecycle 操作的 actor 字段</p>
<Input value={actor} onChange={e => setActor(e.target.value)} placeholder="long" />
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={() => setStep(2)}>上一步</Button>
<Button className="flex-1" onClick={() => setStep(4)}>下一步</Button>
</div>
</div>
)}
{step === 4 && (
<div className="space-y-3">
<div>
<Label>连接 AI 工具</Label>
<p className="text-xs text-muted-foreground mt-1">
注入 MCP 让 AI 读写记忆,注入 Hooks 让 AI 自动提炼对话
</p>
</div>
{detecting ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
检测中…
</div>
) : tools.length === 0 ? (
<p className="text-xs text-muted-foreground">未检测到已安装的 AI 工具</p>
) : (
<div className="space-y-2">
{tools.map(tool => {
const fullyInjected = tool.spool_injected && tool.hooks_installed
const busy = installing === tool.id
return (
<div key={tool.id} className="flex items-center justify-between rounded-md border px-3 py-2">
<div className="flex items-center gap-2 min-w-0">
<span className="text-sm font-medium">{tool.name}</span>
<Check className="h-3 w-3 shrink-0 text-green-500" />
<div className="flex items-center gap-1">
<Badge variant={tool.spool_injected ? 'default' : 'secondary'} className="text-[10px] h-4">MCP</Badge>
<Badge variant={tool.hooks_installed ? 'default' : 'secondary'} className="text-[10px] h-4">Hooks</Badge>
</div>
</div>
{!fullyInjected && (
<button
onClick={async () => {
setInstalling(tool.id)
try {
await Promise.all([
!tool.spool_injected ? tauriInvoke('desktop_install_tool', { client: tool.id }) : Promise.resolve(),
!tool.hooks_installed ? tauriInvoke('desktop_install_hooks', { client: tool.id }) : Promise.resolve(),
])
} catch { /* ignore */ }
setInstalling(null)
detectTools()
}}
disabled={installing !== null}
className="ml-2 shrink-0 rounded border px-2 py-0.5 text-[10px] hover:bg-primary/10 hover:border-primary/30 transition-colors disabled:opacity-50"
>
{busy ? <Loader2 className="h-2.5 w-2.5 animate-spin inline" /> : '一键注入'}
</button>
)}
</div>
)
})}
</div>
)}
{tools.length > 0 && !allReady && (
<Button
variant="outline"
className="w-full text-xs"
onClick={installAll}
disabled={installing !== null}
>
{installing ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" /> : null}
一键注入全部
</Button>
)}
{msg && <p className="text-xs text-green-600">{msg}</p>}
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={() => setStep(3)}>上一步</Button>
<Button className="flex-1" onClick={finish}>
{allReady ? '完成' : '跳过'}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
)
}