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
+72
View File
@@ -8,6 +8,7 @@ import { db } from "../../db/index.js";
import { organization } from "../../db/schema/auth.js";
import { appointmentInputSchema } from "../../lib/appointment-validation.js";
import { initialsFromName } from "../../lib/initials.js";
import { inventoryInputSchema } from "../../lib/inventory-validation.js";
import { invoiceInputSchema } from "../../lib/invoice-validation.js";
import { patientInputSchema } from "../../lib/patient-validation.js";
import { prescriptionInputSchema } from "../../lib/prescription-validation.js";
@@ -474,6 +475,77 @@ export function createChatTools(ctx: ToolContext) {
},
}),
proposeInventory: tool({
description:
"Propose adding one or more items to the clinic's inventory (medications/supplies) for the clinician to approve — e.g. parse an uploaded stock or purchase list into stock items. Does NOT save; it shows an approval card the clinician confirms. Each item needs a name; form/strength/unit/stockQuantity/reorderThreshold/location/expiresAt (YYYY-MM-DD)/notes are optional. Use this for STOCKING inventory; use proposeInvoice instead when billing a patient for purchased items.",
inputSchema: z.object({
items: z
.array(
z.object({
name: z.string().describe("Item / medication name"),
form: z
.string()
.optional()
.describe("Dosage form, e.g. Tablet, Capsule, Syrup"),
strength: z.string().optional().describe("e.g. 500mg"),
unit: z
.string()
.optional()
.describe("Dispensing unit, e.g. box, bottle"),
stockQuantity: z
.number()
.optional()
.describe("Units currently in stock"),
reorderThreshold: z
.number()
.optional()
.describe("Low-stock reorder level"),
location: z.string().optional().describe("Storage location"),
expiresAt: z
.string()
.nullish()
.describe("Expiry date, YYYY-MM-DD"),
notes: z.string().nullish(),
}),
)
.describe("Inventory items to add (prices/quantities from the document)"),
}),
execute: async ({ items }) => {
step(`Drafting ${items.length} inventory item(s)`);
// Inventory is non-PHI — no Veil resolution needed.
const validated: unknown[] = [];
const issues: string[] = [];
items.forEach((item, index) => {
const parsed = inventoryInputSchema.safeParse(item);
if (parsed.success) {
validated.push(parsed.data);
} else {
issues.push(
...parsed.error.issues.map(
(i) =>
`item ${index + 1} ${i.path.join(".") || "(root)"}: ${i.message}`,
),
);
}
});
writer.write({
type: "data-actionPreview",
data: {
token: `inventory-${stepSeq}`,
kind: "inventory" as const,
record: { items: validated.length ? validated : items },
issues,
},
});
return {
ok: issues.length === 0,
count: validated.length,
issues,
note: "Preview only — awaiting clinician approval before any write.",
};
},
}),
proposeInvoice: tool({
description:
"Propose a new invoice for the clinician to approve — e.g. parse an uploaded list of purchased medications into billable line items. Does NOT save; it shows an approval card the clinician confirms. Provide the patient/client name (a file number if known) and line items {description, quantity, unitPrice}; prices come from the uploaded document.",