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

179 lines
5.4 KiB
TypeScript

"use client";
import { CodeBlock, CodeBlockCopyButton } from "@/components/ai-elements/code-block";
import {
Sandbox,
SandboxContent,
SandboxHeader,
SandboxTabContent,
SandboxTabs,
SandboxTabsBar,
SandboxTabsList,
SandboxTabsTrigger,
} from "@/components/ai-elements/sandbox";
import {
StackTrace,
StackTraceActions,
StackTraceContent,
StackTraceCopyButton,
StackTraceError,
StackTraceErrorMessage,
StackTraceErrorType,
StackTraceExpandButton,
StackTraceFrames,
StackTraceHeader,
} from "@/components/ai-elements/stack-trace";
import { Button } from "@/components/ui/button";
import type { ToolUIPart } from "ai";
import { memo, useCallback, useState } from "react";
const code = `import math
def calculate_primes(limit):
"""Find all prime numbers up to a given limit using Sieve of Eratosthenes."""
sieve = [True] * (limit + 1)
sieve[0] = sieve[1] = False
for i in range(2, int(math.sqrt(limit)) + 1):
if sieve[i]:
for j in range(i * i, limit + 1, i):
sieve[j] = False
return [i for i, is_prime in enumerate(sieve) if is_prime]
if __name__ == "__main__":
primes = calculate_primes(50)
print(f"Found {len(primes)} prime numbers up to 50:")
print(primes)`;
const outputs: Record<ToolUIPart["state"], string | undefined> = {
"input-available": undefined,
"input-streaming": undefined,
"output-available": `Found 15 prime numbers up to 50:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]`,
"output-error": `TypeError: Cannot read properties of undefined (reading 'map')
at calculatePrimes (/src/utils/primes.ts:15:23)
at runCalculation (/src/components/Calculator.tsx:42:12)
at onClick (/src/components/Button.tsx:18:5)
at HTMLButtonElement.dispatch (node_modules/react-dom/cjs/react-dom.development.js:3456:9)
at node_modules/react-dom/cjs/react-dom.development.js:4245:12`,
};
const states: ToolUIPart["state"][] = [
"input-streaming",
"input-available",
"output-available",
"output-error",
];
interface StateButtonProps {
s: ToolUIPart["state"];
currentState: ToolUIPart["state"];
onStateChange: (state: ToolUIPart["state"]) => void;
}
const StateButton = memo(
({ s, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(() => onStateChange(s), [onStateChange, s]);
return (
<Button
key={s}
onClick={handleClick}
size="sm"
variant={currentState === s ? "default" : "outline"}
>
{s}
</Button>
);
}
);
StateButton.displayName = "StateButton";
const Example = () => {
const [state, setState] = useState<ToolUIPart["state"]>("output-available");
const handleStateChange = useCallback((s: ToolUIPart["state"]) => {
setState(s);
}, []);
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{states.map((s) => (
<StateButton
currentState={state}
key={s}
onStateChange={handleStateChange}
s={s}
/>
))}
</div>
<Sandbox>
<SandboxHeader state={state} title="primes.py" />
<SandboxContent>
<SandboxTabs defaultValue="code">
<SandboxTabsBar>
<SandboxTabsList>
<SandboxTabsTrigger value="code">Code</SandboxTabsTrigger>
<SandboxTabsTrigger value="output">Output</SandboxTabsTrigger>
</SandboxTabsList>
</SandboxTabsBar>
<SandboxTabContent value="code">
<CodeBlock
className="border-0"
code={
state === "input-streaming" ? "# Generating code..." : code
}
language="python"
>
<CodeBlockCopyButton
className="absolute top-2 right-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
size="sm"
/>
</CodeBlock>
</SandboxTabContent>
<SandboxTabContent value="output">
{state === "output-error" ? (
<StackTrace
className="rounded-none border-0"
defaultOpen
trace={outputs[state] ?? ""}
>
<StackTraceHeader>
<StackTraceError>
<StackTraceErrorType />
<StackTraceErrorMessage />
</StackTraceError>
<StackTraceActions>
<StackTraceCopyButton />
<StackTraceExpandButton />
</StackTraceActions>
</StackTraceHeader>
<StackTraceContent>
<StackTraceFrames />
</StackTraceContent>
</StackTrace>
) : (
<CodeBlock
className="border-0"
code={outputs[state] ?? ""}
language="log"
>
<CodeBlockCopyButton
className="absolute top-2 right-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
size="sm"
/>
</CodeBlock>
)}
</SandboxTabContent>
</SandboxTabs>
</SandboxContent>
</Sandbox>
</div>
);
};
export default Example;