import * as React from 'react'
import { useMutation } from '@tanstack/react-query'
import { useConfig } from '@/state/config'
import { ipc, isTauriHost } from '@/lib/api/desktop'
import { asEnvelope } from '@/lib/error'
import type {
DesktopPromptOptimizeResponse,
DesktopWakeupResponse,
TargetTool,
WakeupProfile,
} from '@/lib/types/desktop'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ErrorBanner } from '@/components/error-banner'
import { Loader2, Wand2, Zap } from 'lucide-react'
const TARGETS: TargetTool[] = ['claude', 'codex', 'opencode']
const PROFILES: WakeupProfile[] = ['developer', 'project']
type Mode = 'wakeup' | 'prompt'
export function WakeupPage() {
const cfg = useConfig()
const [mode, setMode] = React.useState<Mode>('wakeup')
const [task, setTask] = React.useState('')
const [filesRaw, setFilesRaw] = React.useState('')
const [target, setTarget] = React.useState<TargetTool>('claude')
const [profile, setProfile] = React.useState<WakeupProfile>('developer')
const [provider, setProvider] = React.useState('')
const [sessionId, setSessionId] = React.useState('')
const wakeupMutation = useMutation({
mutationFn: () => {
const files = filesRaw
.split(/[\n,]/)
.map((s) => s.trim())
.filter(Boolean)
return ipc.buildWakeup({
config_path: cfg.configPath,
vault_root_override: cfg.vaultRoot.trim() || null,
cwd: cfg.cwd,
task,
files,
target,
profile,
})
},
})
const promptMutation = useMutation({
mutationFn: () => {
const files = filesRaw
.split(/[\n,]/)
.map((s) => s.trim())
.filter(Boolean)
return ipc.optimizePrompt({
config_path: cfg.configPath,
vault_root_override: cfg.vaultRoot.trim() || null,
cwd: cfg.cwd,
task,
files,
target,
profile,
provider: provider.trim() || null,
session_id: sessionId.trim() || null,
})
},
})
const wakeup = wakeupMutation.data ?? null
const prompt = promptMutation.data ?? null
const loading = wakeupMutation.isPending || promptMutation.isPending
const error = wakeupMutation.error ?? promptMutation.error
const errorEnvelope = error ? asEnvelope(error) : null
function run() {
if (mode === 'wakeup') wakeupMutation.mutate()
else promptMutation.mutate()
}
return (
<div className="space-y-4">
<Tabs value={mode} onValueChange={(v) => setMode(v as Mode)}>
<TabsList>
<TabsTrigger value="wakeup" className="gap-1.5">
<Wand2 className="h-3.5 w-3.5" />
唤醒包
</TabsTrigger>
<TabsTrigger value="prompt" className="gap-1.5">
<Zap className="h-3.5 w-3.5" />
Prompt 优化
</TabsTrigger>
</TabsList>
</Tabs>
<div className="grid gap-4 lg:grid-cols-[1fr_1.2fr]">
<Card>
<CardHeader>
<CardTitle className="text-base">
{mode === 'wakeup' ? '生成唤醒包' : '生成优化后的 Prompt'}
</CardTitle>
<CardDescription>
{mode === 'wakeup'
? '根据 profile 聚合身份 / 工作风格 / 近期上下文 / 约束 / 决策 / 事件。'
: '叠加 context + wakeup 两段,生成一段可直接丢给目标 AI 的 combined prompt。'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="task">任务</Label>
<Textarea
id="task"
rows={3}
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="resume spool lifecycle work"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="files">相关文件(逗号 / 换行分隔)</Label>
<Textarea
id="files"
rows={2}
value={filesRaw}
onChange={(e) => setFilesRaw(e.target.value)}
placeholder="src/lifecycle_service.rs"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-2">
<Label>target</Label>
<Select value={target} onValueChange={(v) => setTarget(v as TargetTool)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{TARGETS.map((t) => (
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label>profile</Label>
<Select value={profile} onValueChange={(v) => setProfile(v as WakeupProfile)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROFILES.map((p) => (
<SelectItem key={p} value={p}>
{p}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{mode === 'prompt' && (
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-2">
<Label>provider(可选)</Label>
<Input
value={provider}
onChange={(e) => setProvider(e.target.value)}
placeholder="claude / codex"
/>
</div>
<div className="grid gap-2">
<Label>session_id(可选)</Label>
<Input
value={sessionId}
onChange={(e) => setSessionId(e.target.value)}
/>
</div>
</div>
)}
<div className="flex items-center gap-3 pt-2">
<Button
onClick={run}
disabled={loading || !task.trim() || !cfg.cwd.trim()}
>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
{mode === 'wakeup' ? '生成唤醒包' : '生成 Prompt'}
</Button>
{!isTauriHost() && <Badge variant="muted">Bridge 未就绪</Badge>}
</div>
{errorEnvelope && <ErrorBanner envelope={errorEnvelope} />}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">输出</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{mode === 'wakeup' && wakeup && <WakeupResult response={wakeup} />}
{mode === 'prompt' && prompt && <PromptResult response={prompt} />}
{((mode === 'wakeup' && !wakeup) || (mode === 'prompt' && !prompt)) &&
!loading && (
<p className="text-sm text-muted-foreground">尚未生成。</p>
)}
</CardContent>
</Card>
</div>
</div>
)
}
function WakeupResult({ response }: { response: DesktopWakeupResponse }) {
const knowledgeIndex = response.packet?.knowledge_index
const maintenanceHints = response.packet?.maintenance_hints ?? []
return (
<div className="space-y-3">
<p className="text-xs text-muted-foreground">
vault_root: <span className="text-foreground">{response.used_vault_root}</span>
</p>
{maintenanceHints.length > 0 && (
<div className="rounded-md border border-yellow-500/30 bg-yellow-500/5 p-3">
<Label className="mb-1 block text-xs text-yellow-600">维护提醒</Label>
<ul className="list-disc pl-4 text-xs text-yellow-700">
{maintenanceHints.map((hint, i) => (
<li key={i}>{hint}</li>
))}
</ul>
</div>
)}
{knowledgeIndex && (
<div>
<Label className="mb-1 block text-xs">知识导航 (INDEX)</Label>
<pre className="max-h-[200px] overflow-auto rounded-md border border-primary/20 bg-primary/5 p-3 text-xs leading-relaxed whitespace-pre-wrap">
{knowledgeIndex}
</pre>
</div>
)}
<div>
<Label className="mb-1 block text-xs">Packet</Label>
<pre className="max-h-[480px] overflow-auto rounded-md border bg-muted/30 p-3 text-xs leading-relaxed">
{JSON.stringify(response.packet, null, 2)}
</pre>
</div>
</div>
)
}
function PromptResult({ response }: { response: DesktopPromptOptimizeResponse }) {
const combined = typeof response.combined_prompt === 'string' ? response.combined_prompt : ''
const ctx = typeof response.context_prompt === 'string' ? response.context_prompt : ''
const wake = typeof response.wakeup_prompt === 'string' ? response.wakeup_prompt : ''
return (
<Tabs defaultValue="combined">
<TabsList>
<TabsTrigger value="combined">Combined</TabsTrigger>
<TabsTrigger value="context">Context</TabsTrigger>
<TabsTrigger value="wakeup">Wakeup</TabsTrigger>
<TabsTrigger value="raw">Raw JSON</TabsTrigger>
</TabsList>
<TabsContent value="combined">
<pre className="max-h-[480px] overflow-auto rounded-md border bg-muted/30 p-3 text-xs leading-relaxed whitespace-pre-wrap">
{combined}
</pre>
</TabsContent>
<TabsContent value="context">
<pre className="max-h-[480px] overflow-auto rounded-md border bg-muted/30 p-3 text-xs leading-relaxed whitespace-pre-wrap">
{ctx}
</pre>
</TabsContent>
<TabsContent value="wakeup">
<pre className="max-h-[480px] overflow-auto rounded-md border bg-muted/30 p-3 text-xs leading-relaxed whitespace-pre-wrap">
{wake}
</pre>
</TabsContent>
<TabsContent value="raw">
<pre className="max-h-[480px] overflow-auto rounded-md border bg-muted/30 p-3 text-xs leading-relaxed">
{JSON.stringify(response, null, 2)}
</pre>
</TabsContent>
</Tabs>
)
}