import { useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import {
useAIPrompt,
useAIPromptKeys,
useAIPromptVersions,
useActivateAIPrompt,
useCreateAIPrompt,
useRenderAIPrompt,
} from "@/hooks/queries/useAIPrompts";
import type { AIPromptSummary } from "@/types/ai-prompt";
import { CheckCircle2, FileText, History, Play, PlusCircle } from "lucide-react";
import { toast } from "sonner";
function SelectedKeyPanel({
selectedKey,
onSelectVersion,
activePromptId,
}: {
selectedKey: string;
onSelectVersion: (id: number) => void;
activePromptId: number | null;
}) {
const { data, isLoading } = useAIPromptVersions(selectedKey);
const activate = useActivateAIPrompt();
if (isLoading) {
return ;
}
const versions = data ?? [];
if (!versions.length) {
return (
No versions yet for this prompt.
);
}
return (
Version
Title
Author
Status
Actions
{versions.map((v) => (
v{v.version}
{v.title}
{v.author_name || "—"}
{v.is_active ? (
Active
) : (
Archived
)}
onSelectVersion(v.id)}
>
Open
{!v.is_active ? (
{
try {
await activate.mutateAsync(v.id);
toast.success(`Activated v${v.version}`);
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Activate failed",
);
}
}}
disabled={activate.isPending}
>
Activate
) : null}
))}
);
}
function PromptDetail({ promptId }: { promptId: number }) {
const { data, isLoading } = useAIPrompt(promptId);
const render = useRenderAIPrompt();
// Variables detected from the prompt body — keep local state keyed by name
// so the editor can show one input per placeholder.
const [sampleValues, setSampleValues] = useState>({});
const [rendered, setRendered] = useState("");
useEffect(() => {
setRendered("");
setSampleValues({});
}, [promptId]);
if (isLoading || !data) {
return ;
}
const variables = data.variables ?? [];
return (
{data.title}{" "}
({data.key} v{data.version})
{data.description ? (
{data.description}
) : null}
{variables.length > 0 ? (
Sample variables
{variables.map((v) => (
{v}
setSampleValues((prev) => ({
...prev,
[v]: e.target.value,
}))
}
placeholder={`Sample value for {${v}}`}
/>
))}
{
try {
const r = await render.mutateAsync({
id: data.id,
variables: sampleValues,
});
setRendered(r.rendered);
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Render failed",
);
}
}}
disabled={render.isPending}
>
Render preview
{rendered ? (
{rendered}
) : null}
) : null}
);
}
function NewVersionDialog({
open,
onOpenChange,
defaultKey,
defaultContent,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
defaultKey: string;
defaultContent: string;
}) {
const create = useCreateAIPrompt();
const [key, setKey] = useState(defaultKey);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [content, setContent] = useState(defaultContent);
const [activate, setActivate] = useState(true);
useEffect(() => {
if (open) {
setKey(defaultKey);
setContent(defaultContent);
setTitle("");
setDescription("");
setActivate(true);
}
}, [open, defaultKey, defaultContent]);
return (
New prompt version
onOpenChange(false)}>
Cancel
{
if (!key || !title || !content.trim()) {
toast.error("Key, title, and content are required");
return;
}
try {
await create.mutateAsync({
key,
title,
description,
content,
activate,
});
toast.success("Prompt version created");
onOpenChange(false);
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Create failed",
);
}
}}
disabled={create.isPending}
>
Save version
);
}
export default function AIPromptEditor() {
const [search, setSearch] = useState("");
const { data, isLoading } = useAIPromptKeys({ search, page: 1, size: 50 });
const [selectedKey, setSelectedKey] = useState(null);
const [selectedPromptId, setSelectedPromptId] = useState(null);
const [newOpen, setNewOpen] = useState(false);
const items: AIPromptSummary[] = data?.items ?? [];
const activePromptId = useMemo(() => {
if (!selectedKey) return null;
const match = items.find((it) => it.key === selectedKey);
return match?.id ?? null;
}, [items, selectedKey]);
// Autoselect first key on load.
useEffect(() => {
if (!selectedKey && items.length) {
setSelectedKey(items[0].key);
setSelectedPromptId(items[0].id);
}
}, [items, selectedKey]);
return (
AI prompt library
Versioned, auditable templates that the AI pipelines render at
runtime. Non-engineers can iterate here without shipping code.
setNewOpen(true)}>
New version
Prompt keys
Each row shows the latest version of a prompt key. Click a row to
inspect all versions.
setSearch(e.target.value)}
className="max-w-sm"
/>
{isLoading ? (
) : items.length === 0 ? (
No prompts yet. Click New version to add one.
) : (
Key
Latest
Title
Variables
Status
{items.map((p) => (
{
setSelectedKey(p.key);
setSelectedPromptId(p.id);
}}
>
{p.key}
v{p.version}{" "}
{p.total_versions && p.total_versions > 1 ? (
(of {p.total_versions})
) : null}
{p.title}
{p.variables.slice(0, 4).map((v) => (
{v}
))}
{p.variables.length > 4 ? (
+{p.variables.length - 4}
) : null}
{p.is_active ? (
Active
) : (
No active
)}
))}
)}
{selectedKey ? (
Versions of {selectedKey}
Preview
Inspect template body and try a dry-run render.
{selectedPromptId ? (
) : (
Select a version from the list.
)}
) : null}
);
}