Files
temetro/.agents/skills/ai-elements/scripts/model-selector.tsx
T
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

366 lines
8.5 KiB
TypeScript

"use client";
import {
ModelSelector,
ModelSelectorContent,
ModelSelectorEmpty,
ModelSelectorGroup,
ModelSelectorInput,
ModelSelectorItem,
ModelSelectorList,
ModelSelectorLogo,
ModelSelectorLogoGroup,
ModelSelectorName,
ModelSelectorTrigger,
} from "@/components/ai-elements/model-selector";
import { Button } from "@/components/ui/button";
import { CheckIcon } from "lucide-react";
import { memo, useCallback, useState } from "react";
const models = [
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o",
name: "GPT-4o",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o-mini",
name: "GPT-4o Mini",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "o1",
name: "o1",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "o1-mini",
name: "o1 Mini",
providers: ["openai", "azure"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-opus-4-20250514",
name: "Claude 4 Opus",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-3.5-sonnet",
name: "Claude 3.5 Sonnet",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-3.5-haiku",
name: "Claude 3.5 Haiku",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-2.0-flash-exp",
name: "Gemini 2.0 Flash",
providers: ["google", "google-vertex"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-1.5-pro",
name: "Gemini 1.5 Pro",
providers: ["google", "google-vertex"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-1.5-flash",
name: "Gemini 1.5 Flash",
providers: ["google", "google-vertex"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.3-70b",
name: "Llama 3.3 70B",
providers: ["groq", "togetherai", "amazon-bedrock"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.1-405b",
name: "Llama 3.1 405B",
providers: ["togetherai", "amazon-bedrock"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.1-70b",
name: "Llama 3.1 70B",
providers: ["groq", "togetherai", "amazon-bedrock"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.1-8b",
name: "Llama 3.1 8B",
providers: ["groq", "togetherai"],
},
{
chef: "DeepSeek",
chefSlug: "deepseek",
id: "deepseek-r1",
name: "DeepSeek R1",
providers: ["deepseek", "openrouter"],
},
{
chef: "DeepSeek",
chefSlug: "deepseek",
id: "deepseek-v3",
name: "DeepSeek V3",
providers: ["deepseek", "openrouter"],
},
{
chef: "DeepSeek",
chefSlug: "deepseek",
id: "deepseek-coder-v2",
name: "DeepSeek Coder V2",
providers: ["deepseek", "openrouter"],
},
{
chef: "Mistral AI",
chefSlug: "mistral",
id: "mistral-large",
name: "Mistral Large",
providers: ["mistral", "azure"],
},
{
chef: "Mistral AI",
chefSlug: "mistral",
id: "mistral-small",
name: "Mistral Small",
providers: ["mistral", "azure"],
},
{
chef: "Mistral AI",
chefSlug: "mistral",
id: "codestral",
name: "Codestral",
providers: ["mistral"],
},
{
chef: "Alibaba",
chefSlug: "alibaba",
id: "qwen-2.5-72b",
name: "Qwen 2.5 72B",
providers: ["alibaba", "openrouter"],
},
{
chef: "Alibaba",
chefSlug: "alibaba",
id: "qwen-2.5-coder-32b",
name: "Qwen 2.5 Coder 32B",
providers: ["alibaba", "openrouter"],
},
{
chef: "Alibaba",
chefSlug: "alibaba",
id: "qwen-max",
name: "Qwen Max",
providers: ["alibaba"],
},
{
chef: "Cohere",
chefSlug: "cohere",
id: "command-r-plus",
name: "Command R+",
providers: ["cohere", "azure", "amazon-bedrock"],
},
{
chef: "Cohere",
chefSlug: "cohere",
id: "command-r",
name: "Command R",
providers: ["cohere", "azure", "amazon-bedrock"],
},
{
chef: "xAI",
chefSlug: "xai",
id: "grok-3",
name: "Grok 3",
providers: ["xai"],
},
{
chef: "xAI",
chefSlug: "xai",
id: "grok-2-1212",
name: "Grok 2 1212",
providers: ["xai"],
},
{
chef: "xAI",
chefSlug: "xai",
id: "grok-vision",
name: "Grok Vision",
providers: ["xai"],
},
{
chef: "Moonshot AI",
chefSlug: "moonshotai",
id: "moonshot-v1-128k",
name: "Moonshot v1 128K",
providers: ["moonshotai"],
},
{
chef: "Moonshot AI",
chefSlug: "moonshotai",
id: "moonshot-v1-32k",
name: "Moonshot v1 32K",
providers: ["moonshotai"],
},
{
chef: "Perplexity",
chefSlug: "perplexity",
id: "sonar-pro",
name: "Sonar Pro",
providers: ["perplexity"],
},
{
chef: "Perplexity",
chefSlug: "perplexity",
id: "sonar",
name: "Sonar",
providers: ["perplexity"],
},
{
chef: "Vercel",
chefSlug: "v0",
id: "v0-chat",
name: "v0 Chat",
providers: ["vercel"],
},
{
chef: "Amazon",
chefSlug: "amazon-bedrock",
id: "nova-pro",
name: "Nova Pro",
providers: ["amazon-bedrock"],
},
{
chef: "Amazon",
chefSlug: "amazon-bedrock",
id: "nova-lite",
name: "Nova Lite",
providers: ["amazon-bedrock"],
},
{
chef: "Amazon",
chefSlug: "amazon-bedrock",
id: "nova-micro",
name: "Nova Micro",
providers: ["amazon-bedrock"],
},
];
interface ModelItemProps {
model: (typeof models)[0];
selectedModel: string;
onSelect: (id: string) => void;
}
const ModelItem = memo(({ model, selectedModel, onSelect }: ModelItemProps) => {
const handleSelect = useCallback(
() => onSelect(model.id),
[onSelect, model.id]
);
return (
<ModelSelectorItem key={model.id} onSelect={handleSelect} value={model.id}>
<ModelSelectorLogo provider={model.chefSlug} />
<ModelSelectorName>{model.name}</ModelSelectorName>
<ModelSelectorLogoGroup>
{model.providers.map((provider) => (
<ModelSelectorLogo key={provider} provider={provider} />
))}
</ModelSelectorLogoGroup>
{selectedModel === model.id ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</ModelSelectorItem>
);
});
ModelItem.displayName = "ModelItem";
const Example = () => {
const [open, setOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState<string>("gpt-4o");
const handleModelSelect = useCallback((id: string) => {
setSelectedModel(id);
setOpen(false);
}, []);
const selectedModelData = models.find((model) => model.id === selectedModel);
// Get unique chefs in order of appearance
const chefs = [...new Set(models.map((model) => model.chef))];
return (
<div className="flex size-full items-center justify-center p-8">
<ModelSelector onOpenChange={setOpen} open={open}>
<ModelSelectorTrigger asChild>
<Button className="w-[200px] justify-between" variant="outline">
{selectedModelData?.chefSlug && (
<ModelSelectorLogo provider={selectedModelData.chefSlug} />
)}
{selectedModelData?.name && (
<ModelSelectorName>{selectedModelData.name}</ModelSelectorName>
)}
</Button>
</ModelSelectorTrigger>
<ModelSelectorContent>
<ModelSelectorInput placeholder="Search models..." />
<ModelSelectorList>
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
{chefs.map((chef) => (
<ModelSelectorGroup heading={chef} key={chef}>
{models
.filter((model) => model.chef === chef)
.map((model) => (
<ModelItem
key={model.id}
model={model}
onSelect={handleModelSelect}
selectedModel={selectedModel}
/>
))}
</ModelSelectorGroup>
))}
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
</div>
);
};
export default Example;