CI / build (push) Successful in 4m26s
Adds a Postgres-backed quotes domain (poems/quotes/favorites) exposed as five agent-native actions (list-quotes, get-random-quote, create-quote, toggle-favorite, list-favorites) callable from both chat and the React UI via useActionQuery/useActionMutation. Moves the primary UI from chat to a mobile-first Quotes/Favorites experience (chat moves to /chat), adds manual OpenTelemetry instrumentation exporting traces/metrics/logs over OTLP, and wires a local Docker Postgres for shared state.
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
import { IconPlus } from "@tabler/icons-react";
|
|
import { useId, useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
|
|
const emptyForm = { text: "", author: "", poemTitle: "", source: "", year: "", tags: "" };
|
|
|
|
export function SubmitQuoteDialog() {
|
|
const [open, setOpen] = useState(false);
|
|
const [form, setForm] = useState(emptyForm);
|
|
const formId = useId();
|
|
|
|
const { mutate, isPending } = useActionMutation("create-quote", {
|
|
onSuccess: () => {
|
|
toast.success("Quote added");
|
|
setForm(emptyForm);
|
|
setOpen(false);
|
|
},
|
|
onError: () => toast.error("Couldn't save that quote — try again"),
|
|
});
|
|
|
|
function handleSubmit(event: React.FormEvent) {
|
|
event.preventDefault();
|
|
if (!form.text.trim() || !form.author.trim() || !form.poemTitle.trim()) return;
|
|
mutate({
|
|
text: form.text.trim(),
|
|
author: form.author.trim(),
|
|
poemTitle: form.poemTitle.trim(),
|
|
source: form.source.trim() || undefined,
|
|
year: form.year ? Number(form.year) : undefined,
|
|
tags: form.tags
|
|
.split(",")
|
|
.map((t) => t.trim().toLowerCase())
|
|
.filter(Boolean),
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button type="button" className="gap-1.5">
|
|
<IconPlus className="size-4" />
|
|
Submit a quote
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Submit a quote</DialogTitle>
|
|
<DialogDescription>
|
|
Share a line or passage from a poem you love.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form id={formId} onSubmit={handleSubmit} className="grid gap-4">
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor={`${formId}-text`}>Quote</Label>
|
|
<Textarea
|
|
id={`${formId}-text`}
|
|
required
|
|
rows={4}
|
|
value={form.text}
|
|
onChange={(e) => setForm((f) => ({ ...f, text: e.target.value }))}
|
|
placeholder="Two roads diverged in a wood..."
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor={`${formId}-author`}>Poet</Label>
|
|
<Input
|
|
id={`${formId}-author`}
|
|
required
|
|
value={form.author}
|
|
onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}
|
|
placeholder="Robert Frost"
|
|
/>
|
|
</div>
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor={`${formId}-title`}>Poem title</Label>
|
|
<Input
|
|
id={`${formId}-title`}
|
|
required
|
|
value={form.poemTitle}
|
|
onChange={(e) => setForm((f) => ({ ...f, poemTitle: e.target.value }))}
|
|
placeholder="The Road Not Taken"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor={`${formId}-source`}>Source (optional)</Label>
|
|
<Input
|
|
id={`${formId}-source`}
|
|
value={form.source}
|
|
onChange={(e) => setForm((f) => ({ ...f, source: e.target.value }))}
|
|
placeholder="Collection name"
|
|
/>
|
|
</div>
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor={`${formId}-year`}>Year (optional)</Label>
|
|
<Input
|
|
id={`${formId}-year`}
|
|
inputMode="numeric"
|
|
value={form.year}
|
|
onChange={(e) => setForm((f) => ({ ...f, year: e.target.value.replace(/\D/g, "") }))}
|
|
placeholder="1916"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor={`${formId}-tags`}>Tags (optional, comma separated)</Label>
|
|
<Input
|
|
id={`${formId}-tags`}
|
|
value={form.tags}
|
|
onChange={(e) => setForm((f) => ({ ...f, tags: e.target.value }))}
|
|
placeholder="hope, nature, journey"
|
|
/>
|
|
</div>
|
|
</form>
|
|
<DialogFooter>
|
|
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" form={formId} disabled={isPending}>
|
|
{isPending ? "Saving..." : "Save quote"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|