import React, { useState, useEffect, useCallback } from 'react'
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { open } from '@tauri-apps/plugin-dialog'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { RulesEditor } from '@/components/rules-editor'
import { useConfig } from '@/state/config'
import { useI18n } from '@/state/i18n'
import { isTauriHost } from '@/lib/api/desktop'
import {
ArrowLeft,
Check,
FolderOpen,
Globe,
HardDrive,
Info,
Loader2,
Shield,
Wrench,
X,
} from 'lucide-react'
type Props = { onBack: () => void }
type ToolStatus = {
id: string
name: string
config_dir: string
installed: boolean
spool_injected: boolean
hooks_installed: boolean
}
export function SettingsPage({ onBack }: Props) {
const cfg = useConfig()
const { lang, setLang: setI18nLang } = useI18n()
const [tools, setTools] = useState<ToolStatus[]>([])
const handleHeaderMouseDown = useCallback((e: React.MouseEvent<HTMLElement>) => {
if (e.button !== 0) return
const target = e.target as HTMLElement
if (target.closest('button') || target.closest('a') || target.closest('input')) return
void getCurrentWindow().startDragging()
}, [])
const [detecting, setDetecting] = useState(false)
const [exportMsg, setExportMsg] = useState('')
const [importMsg, setImportMsg] = useState('')
const [installMsg, setInstallMsg] = useState('')
const [hookMsg, setHookMsg] = useState('')
useEffect(() => {
detectTools()
}, [])
async function detectTools() {
if (!isTauriHost()) return
setDetecting(true)
try {
const res = await tauriInvoke<{ tools: ToolStatus[] }>('desktop_detect_ai_tools')
setTools(res.tools)
} catch {
// ignore
}
setDetecting(false)
}
async function handleExport() {
if (!isTauriHost()) return
try {
const res = await tauriInvoke<{ path: string; records: number }>('desktop_export_data')
setExportMsg(`已导出 ${res.records} 条记录到 ${res.path}`)
} catch (e) {
setExportMsg(`导出失败: ${e}`)
}
}
async function handleImport() {
if (!isTauriHost()) return
setImportMsg('')
try {
const selected = await open({
title: '选择导入文件',
filters: [{ name: 'JSONL', extensions: ['jsonl', 'json'] }],
multiple: false,
})
if (!selected) return
const path = typeof selected === 'string' ? selected : selected
const res = await tauriInvoke<{ imported_records: number }>('desktop_import_data', { path })
setImportMsg(`已导入 ${res.imported_records} 条记录`)
} catch (e) {
setImportMsg(`导入失败: ${e}`)
}
}
function switchLang(l: 'zh' | 'en') {
setI18nLang(l)
}
async function handleInstallAll(clientId: string) {
if (!isTauriHost()) return
setInstallMsg('')
setHookMsg('')
try {
const [mcpRes, hookRes] = await Promise.all([
tauriInvoke<{ success: boolean; status: string }>('desktop_install_tool', { client: clientId }),
tauriInvoke<{ success: boolean; changed: boolean }>('desktop_install_hooks', { client: clientId }),
])
const mcpOk = mcpRes.success
const hookOk = hookRes.success
setInstallMsg(
mcpOk && hookOk
? `${clientId} MCP + Hooks 注入成功`
: `MCP: ${mcpRes.status}, Hooks: ${hookOk ? 'ok' : 'failed'}`
)
detectTools()
} catch (e) {
setInstallMsg(`注入失败: ${e}`)
}
}
return (
<div className="flex h-screen flex-col">
<header
onMouseDown={handleHeaderMouseDown}
className="flex h-11 shrink-0 items-center gap-3 border-b border-border-subtle px-5 pl-[76px] select-none cursor-default"
>
<button
onClick={onBack}
className="rounded-md p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors cursor-pointer"
>
<ArrowLeft className="h-4 w-4" />
</button>
<span className="text-sm font-semibold">设置</span>
</header>
<div className="flex-1 overflow-auto p-5 space-y-5">
{/* 知识库 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<FolderOpen className="h-4 w-4 text-primary" />
知识库
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid gap-1.5">
<Label htmlFor="vault-root" className="text-xs text-muted-foreground">
Obsidian Vault 路径
</Label>
<Input
id="vault-root"
value={cfg.vaultRoot}
placeholder="/Users/you/Documents/KnowledgeVault"
onChange={(e) => cfg.setVaultRoot(e.target.value)}
className="text-sm"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="default-actor" className="text-xs text-muted-foreground">
默认 actor
</Label>
<Input
id="default-actor"
value={cfg.defaultActor}
placeholder="long"
onChange={(e) => cfg.setDefaultActor(e.target.value)}
className="text-sm"
/>
<p className="text-[10px] text-muted-foreground">
动作 confirm 弹窗的 actor 字段会自动预填这个值。
</p>
</div>
</CardContent>
</Card>
{/* AI 工具检测 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<Wrench className="h-4 w-4 text-primary" />
AI 工具
{detecting && <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{tools.map((tool) => {
const fullyInjected = tool.spool_injected && tool.hooks_installed
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>
{tool.installed ? (
<Check className="h-3 w-3 shrink-0 text-green-500" />
) : (
<X className="h-3 w-3 shrink-0 text-muted-foreground" />
)}
{tool.installed && (
<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>
{tool.installed && !fullyInjected && (
<button
onClick={() => handleInstallAll(tool.id)}
className="ml-2 shrink-0 rounded border px-2 py-0.5 text-[10px] hover:bg-primary/10 hover:border-primary/30 transition-colors"
>
一键注入
</button>
)}
{!tool.installed && (
<Badge variant="outline" className="text-[10px]">未安装</Badge>
)}
</div>
)
})}
{tools.length === 0 && !detecting && (
<p className="text-xs text-muted-foreground">点击检测按钮扫描本地 AI 工具</p>
)}
</div>
{installMsg && (
<p className="mt-2 text-xs text-muted-foreground">{installMsg}</p>
)}
{hookMsg && (
<p className="mt-1 text-xs text-muted-foreground">{hookMsg}</p>
)}
<button
onClick={detectTools}
className="mt-3 rounded-md border px-3 py-1.5 text-xs hover:bg-muted transition-colors"
>
重新检测
</button>
</CardContent>
</Card>
{/* 规则 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<Shield className="h-4 w-4 text-primary" />
自定义规则
</CardTitle>
</CardHeader>
<CardContent>
<RulesEditor />
</CardContent>
</Card>
{/* 数据 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<HardDrive className="h-4 w-4 text-primary" />
数据
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<button
onClick={handleExport}
className="rounded-md border px-3 py-1.5 text-xs hover:bg-muted transition-colors"
>
导出记忆
</button>
<button
onClick={handleImport}
className="rounded-md border px-3 py-1.5 text-xs hover:bg-muted transition-colors"
>
导入记忆
</button>
</div>
{exportMsg && (
<p className="text-xs text-muted-foreground">{exportMsg}</p>
)}
{importMsg && (
<p className="text-xs text-muted-foreground">{importMsg}</p>
)}
</CardContent>
</Card>
{/* 语言 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<Globe className="h-4 w-4 text-primary" />
语言
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<button
onClick={() => switchLang('zh')}
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
lang === 'zh'
? 'bg-primary/10 border-primary/20 text-primary'
: 'text-muted-foreground hover:bg-muted'
}`}
>
中文
</button>
<button
onClick={() => switchLang('en')}
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
lang === 'en'
? 'bg-primary/10 border-primary/20 text-primary'
: 'text-muted-foreground hover:bg-muted'
}`}
>
English
</button>
</div>
</CardContent>
</Card>
{/* 关于 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<Info className="h-4 w-4 text-primary" />
关于
</CardTitle>
</CardHeader>
<CardContent className="space-y-1">
<p className="text-xs">
<span className="text-muted-foreground">版本:</span>0.1.0
</p>
<p className="text-xs">
<span className="text-muted-foreground">运行时:</span>Tauri v2
</p>
<p className="text-xs">
<span className="text-muted-foreground">配置:</span>
<code className="font-mono text-[10px]">{cfg.configPath || '~/.spool/config.toml'}</code>
</p>
</CardContent>
</Card>
</div>
</div>
)
}