mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
929bec8f31
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>
111 lines
2.5 KiB
TypeScript
111 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import type { PersonaState } from "@/components/ai-elements/persona";
|
|
import { Persona } from "@/components/ai-elements/persona";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ButtonGroup } from "@/components/ui/button-group";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import {
|
|
BrainIcon,
|
|
CircleIcon,
|
|
EyeClosedIcon,
|
|
MegaphoneIcon,
|
|
MicIcon,
|
|
} from "lucide-react";
|
|
import { memo, useCallback, useState } from "react";
|
|
|
|
const states: {
|
|
state: PersonaState;
|
|
icon: LucideIcon;
|
|
label: string;
|
|
}[] = [
|
|
{
|
|
icon: CircleIcon,
|
|
label: "Idle",
|
|
state: "idle",
|
|
},
|
|
{
|
|
icon: MicIcon,
|
|
label: "Listening",
|
|
state: "listening",
|
|
},
|
|
{
|
|
icon: BrainIcon,
|
|
label: "Thinking",
|
|
state: "thinking",
|
|
},
|
|
{
|
|
icon: MegaphoneIcon,
|
|
label: "Speaking",
|
|
state: "speaking",
|
|
},
|
|
{
|
|
icon: EyeClosedIcon,
|
|
label: "Asleep",
|
|
state: "asleep",
|
|
},
|
|
];
|
|
|
|
interface StateButtonProps {
|
|
state: (typeof states)[0];
|
|
currentState: PersonaState;
|
|
onStateChange: (state: PersonaState) => void;
|
|
}
|
|
|
|
const StateButton = memo(
|
|
({ state, currentState, onStateChange }: StateButtonProps) => {
|
|
const handleClick = useCallback(
|
|
() => onStateChange(state.state),
|
|
[onStateChange, state.state]
|
|
);
|
|
return (
|
|
<Tooltip key={state.state}>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
onClick={handleClick}
|
|
size="icon-sm"
|
|
variant={currentState === state.state ? "default" : "outline"}
|
|
>
|
|
<state.icon className="size-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{state.label}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
);
|
|
|
|
StateButton.displayName = "StateButton";
|
|
|
|
const Example = () => {
|
|
const [currentState, setCurrentState] = useState<PersonaState>("idle");
|
|
|
|
const handleStateChange = useCallback((state: PersonaState) => {
|
|
setCurrentState(state);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="flex size-full flex-col items-center justify-center gap-4">
|
|
<Persona className="size-32" state={currentState} variant="glint" />
|
|
|
|
<ButtonGroup orientation="horizontal">
|
|
{states.map((state) => (
|
|
<StateButton
|
|
currentState={currentState}
|
|
key={state.state}
|
|
onStateChange={handleStateChange}
|
|
state={state}
|
|
/>
|
|
))}
|
|
</ButtonGroup>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Example;
|