import * as React from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { DraftFormFields } from '@/components/draft-form-fields'
import { ErrorBanner } from '@/components/error-banner'
import { emptyDraft, type DraftForm, buildMetadataDto, parseCsvField } from '@/lib/drafts'
import { ipc } from '@/lib/api/desktop'
import { asEnvelope } from '@/lib/error'
import { qk } from '@/lib/queryKeys'
import type { DesktopMemoryDraftRequest } from '@/lib/types/desktop'
import { useConfig } from '@/state/config'
import { Loader2 } from 'lucide-react'
type Mode = 'manual' | 'propose'
export function DraftDialog({
mode,
open,
onOpenChange,
onCreated,
}: {
mode: Mode
open: boolean
onOpenChange: (open: boolean) => void
onCreated: () => void
}) {
const cfg = useConfig()
const qc = useQueryClient()
const [form, setForm] = React.useState<DraftForm>(emptyDraft)
const submitMutation = useMutation({
mutationFn: async (input: { mode: Mode; payload: DesktopMemoryDraftRequest }) => {
if (input.mode === 'manual') {
return ipc.recordManual(input.payload)
}
return ipc.proposeMemory(input.payload)
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: qk.workbench(cfg.configPath) })
onOpenChange(false)
onCreated()
},
})
React.useEffect(() => {
if (open) {
setForm({
...emptyDraft,
source_ref: mode === 'manual' ? 'manual:desktop' : 'ai:desktop',
})
submitMutation.reset()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, mode])
function submit() {
const payload: DesktopMemoryDraftRequest = {
config_path: cfg.configPath,
title: form.title.trim(),
summary: form.summary.trim(),
memory_type: form.memory_type.trim(),
scope: form.scope,
source_ref: form.source_ref.trim(),
project_id: form.project_id.trim() || null,
user_id: form.user_id.trim() || null,
sensitivity: form.sensitivity.trim() || null,
metadata: buildMetadataDto(form),
entities: parseCsvField(form.entities),
tags: parseCsvField(form.tags),
triggers: parseCsvField(form.triggers),
related_files: parseCsvField(form.related_files),
related_records: parseCsvField(form.related_records),
supersedes: form.supersedes.trim() || null,
applies_to: parseCsvField(form.applies_to),
valid_until: form.valid_until.trim() || null,
}
submitMutation.mutate({ mode, payload })
}
const loading = submitMutation.isPending
const errorEnvelope = submitMutation.error ? asEnvelope(submitMutation.error) : null
const title = mode === 'manual' ? '记录手动记忆' : '提交 AI 候选记忆'
const description =
mode === 'manual'
? '手动记录的记忆会以 Accepted 状态直接入库。'
: 'AI 候选以 Candidate 状态入库,需要在审核队列里采纳后才可唤醒。'
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DraftFormFields value={form} onChange={setForm} />
{errorEnvelope && <ErrorBanner envelope={errorEnvelope} />}
<DialogFooter>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={loading}>
取消
</Button>
<Button
onClick={submit}
disabled={
loading ||
!form.title.trim() ||
!form.summary.trim() ||
!form.memory_type.trim() ||
!form.source_ref.trim()
}
>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
提交
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}