Files
Khalid Abdi 929bec8f31 feat: AI-added records save with placeholders + "Added by AI" provenance
Stop blocking AI imports/proposals on missing non-critical fields. Records
the chat agent drafts now save with safe placeholders, auto-generated file
numbers, and a source="ai" marker that surfaces an "Added by AI" badge so a
clinician can review/edit them later.

Backend:
- add `source` (manual|ai) column to patients/appointments/prescriptions
  (migration 0014) + canonical types, services, validation schemas
- relax patient/appointment validation: empty file number allowed, demographic
  + type/provider/initials fall back to placeholders (initials derived from name)
- patients.generateFileNumber() auto-assigns an MRN when one is missing
- proposeAppointment accepts a name when no file number resolves; AI commits +
  /api/ai/import stamp source="ai"

Frontend:
- `source` on Appointment/Patient/Prescription types; AI commits send source="ai"
- reusable <AiBadge> shown on the Patients table/detail and prescriptions list

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:27:17 +03:00

105 lines
3.2 KiB
TypeScript

"use client";
import {
JSXPreview,
JSXPreviewContent,
JSXPreviewError,
} from "@/components/ai-elements/jsx-preview";
import { Button } from "@/components/ui/button";
import { useCallback, useEffect, useRef, useState } from "react";
const handleError = (error: Error) => {
console.log("JSX Parse Error:", error);
};
const fullJsx = `<div className="rounded-lg border bg-card p-6 shadow-sm">
<div className="flex items-center gap-4 mb-4">
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
<span className="text-primary text-xl font-bold">AI</span>
</div>
<div>
<h2 className="text-lg font-semibold">AI-Generated Component</h2>
<p className="text-sm text-muted-foreground">Rendered from JSX string</p>
</div>
</div>
<div className="space-y-3">
<p className="text-sm">This component was dynamically rendered from a JSX string. The JSXPreview component supports streaming mode, automatically closing unclosed tags as content arrives.</p>
<div className="flex gap-2">
<span className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">React</span>
<span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">Streaming</span>
<span className="inline-flex items-center rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800">Dynamic</span>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Generated just now</span>
<button className="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors">
Learn more
</button>
</div>
</div>
</div>`;
const Example = () => {
const [streamedJsx, setStreamedJsx] = useState(fullJsx);
const [isStreaming, setIsStreaming] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const simulateStreaming = useCallback(() => {
setIsStreaming(true);
setStreamedJsx("");
let index = 0;
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(() => {
if (index < fullJsx.length) {
setStreamedJsx(fullJsx.slice(0, index + 15));
index += 15;
} else {
setIsStreaming(false);
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, 30);
}, []);
useEffect(
() => () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
},
[]
);
return (
<div className="space-y-4">
<Button
disabled={isStreaming}
onClick={simulateStreaming}
size="sm"
variant="outline"
>
{isStreaming ? "Streaming..." : "Simulate Streaming"}
</Button>
<JSXPreview
className="min-h-[200px]"
isStreaming={isStreaming}
jsx={streamedJsx}
onError={handleError}
>
<JSXPreviewContent />
<JSXPreviewError className="mt-2" />
</JSXPreview>
</div>
);
};
export default Example;