feat(chat): file attachments as chips, CoT closed, AI add-to-inventory

Chat fixes (#1/#6/#7):
- chat-input passes raw File[] up instead of inlining file text
- chat-panel sends files as FileUIPart and renders them with the
  ai-elements Attachments component (no more raw text dumps)
- backend extracts text-like file content for the model in routes/chat
- Chain-of-Thought now defaults to collapsed
- file upload works regardless of whether the input has text

AI add-to-inventory (#2):
- new proposeInventory tool (validates items, streams an approval card)
- system prompt distinguishes stocking inventory vs. billing a patient
- ActionPreviewCard commits inventory via POST /api/inventory

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-15 18:27:43 +03:00
parent 3aa699aefe
commit 6e5598262e
7 changed files with 235 additions and 51 deletions
+7 -27
View File
@@ -17,7 +17,7 @@ import type { Effort } from "@/lib/ai-models";
import { cn } from "@/lib/utils";
type ChatInputProps = {
onSubmit: (text: string) => void;
onSubmit: (text: string, files: File[]) => void;
status: ChatStatus;
onStop?: () => void;
model: string;
@@ -55,36 +55,16 @@ export function ChatInput({
const canSend =
(value.trim().length > 0 || files.length > 0) && !isGenerating;
const submit = useCallback(async () => {
const submit = useCallback(() => {
const trimmed = value.trim();
// Allow submitting while generating — the panel queues it (Claude-style).
if (!trimmed && files.length === 0) {
return;
}
const parts: string[] = [];
if (trimmed) {
parts.push(trimmed);
}
// Include the text of attached files so the agent can parse them (e.g. a
// database export to import). Read text-like files; cap each so a huge file
// can't blow the context. Binary files are referenced by name only.
for (const file of files) {
const textLike =
/\.(csv|tsv|json|txt|md|xml|ndjson|tab)$/i.test(file.name) ||
file.type.startsWith("text/") ||
file.type === "application/json";
if (textLike) {
try {
const content = (await file.text()).slice(0, 200_000);
parts.push(`--- File: ${file.name} ---\n${content}`);
} catch {
parts.push(`[Attached: ${file.name}]`);
}
} else {
parts.push(`[Attached: ${file.name}]`);
}
}
onSubmit(parts.join("\n\n"));
// Hand the raw files to the panel; it sends them as proper attachment parts
// (rendered as chips, not raw inlined text) and the backend extracts any
// text-like content for the model.
onSubmit(trimmed, files);
setValue("");
setFiles([]);
}, [value, files, onSubmit]);
@@ -234,7 +214,7 @@ export function ChatInput({
<PatientFormDialog
key={addKey}
mode="create"
onCreated={(fileNumber) => onSubmit(`/patient ${fileNumber}`)}
onCreated={(fileNumber) => onSubmit(`/patient ${fileNumber}`, [])}
onOpenChange={setAddOpen}
open={addOpen}
/>