feat(giellie): a Stock update shortcut in the composer

Opens a paste panel for the SKU and quantity lines and sends them to
the chat. It carries no set/add intent — the operator is asked that
every time — so it is only a shortcut into the conversation.
This commit is contained in:
Dave 2026-11-06 01:39:32 +02:00
parent c943118300
commit e08d28f365
2 changed files with 78 additions and 1 deletions

View file

@ -26,3 +26,29 @@ describe("Composer draft", () => {
expect(onSend).toHaveBeenCalledTimes(1);
});
});
describe("the stock update shortcut", () => {
it("sends the pasted lines to Giellie", () => {
const onSend = vi.fn();
render(<Composer disabled={false} onSend={onSend} />);
fireEvent.click(screen.getByRole("button", { name: /stock update/i }));
fireEvent.change(screen.getByPlaceholderText(/ABC-123 50/i), {
target: { value: "ABC-123 50\nBOLT-6MM 120" },
});
fireEvent.click(screen.getByRole("button", { name: /send to giellie/i }));
expect(onSend).toHaveBeenCalledTimes(1);
const sent = String(onSend.mock.calls[0][0]);
expect(sent).toContain("ABC-123 50");
expect(sent).toContain("BOLT-6MM 120");
});
it("does not send an empty list", () => {
const onSend = vi.fn();
render(<Composer disabled={false} onSend={onSend} />);
fireEvent.click(screen.getByRole("button", { name: /stock update/i }));
fireEvent.click(screen.getByRole("button", { name: /send to giellie/i }));
expect(onSend).not.toHaveBeenCalled();
});
});

View file

@ -1,7 +1,7 @@
"use client";
import { useRef, useState } from "react";
import { Camera, Paperclip, Send, X } from "lucide-react";
import { Camera, Package, Paperclip, Send, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { downscaleImage } from "@/lib/giellie-image";
@ -15,6 +15,8 @@ interface Props {
export function Composer({ disabled, onSend }: Props) {
const [text, setText] = useState("");
const [files, setFiles] = useState<File[]>([]);
const [stockOpen, setStockOpen] = useState(false);
const [stockText, setStockText] = useState("");
const fileRef = useRef<HTMLInputElement>(null);
const cameraRef = useRef<HTMLInputElement>(null);
@ -32,6 +34,19 @@ export function Composer({ disabled, onSend }: Props) {
});
}
function sendStockList() {
const list = stockText.trim();
if (!list || disabled) return;
onSend(
`Update the stock quantities on these lines.\n\n${list}`,
[],
() => {
setStockText("");
setStockOpen(false);
},
);
}
return (
<div className="border-t bg-background p-3 space-y-2">
{files.length > 0 && (
@ -75,6 +90,17 @@ export function Composer({ disabled, onSend }: Props) {
<Button type="button" variant="ghost" size="icon" aria-label="Take a photo" onClick={() => cameraRef.current?.click()}>
<Camera className="h-5 w-5" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Stock update"
title="Stock update"
disabled={disabled}
onClick={() => setStockOpen((v) => !v)}
>
<Package className="h-5 w-5" />
</Button>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
@ -92,6 +118,31 @@ export function Composer({ disabled, onSend }: Props) {
<Send className="h-5 w-5" />
</Button>
</div>
{stockOpen && (
<div className="space-y-2 rounded-md border bg-muted/30 p-2">
<Textarea
value={stockText}
onChange={(e) => setStockText(e.target.value)}
placeholder={"ABC-123 50\nBOLT-6MM 120"}
rows={4}
className="min-h-[80px] font-mono text-xs"
/>
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground">
One SKU and quantity per line. Giellie asks whether this is the new quantity on
hand or an addition.
</p>
<Button
type="button"
size="sm"
disabled={disabled || !stockText.trim()}
onClick={sendStockList}
>
Send to Giellie
</Button>
</div>
</div>
)}
</div>
);
}