mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
frontend: declutter chat input to a single model+effort picker
Replace the five clinical context selects (access, response mode, specialty, facility, time range) with one model picker styled like the reference design: a primary model list with descriptions, an Effort submenu, and a "More models" submenu. Move the Add-patient pill up into the toolbar and drop the now-empty bottom context-selector card so the input is a single clean rounded surface. Model/effort selection is lifted to ChatPanel so it can travel with each send (wired to the backend agent in a later phase). Adds a shared model catalog (lib/ai-models.ts) reused by the picker, Settings, and the chat wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,78 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import type { ChatStatus } from "ai";
|
||||
import {
|
||||
ArrowUp,
|
||||
Building2,
|
||||
CalendarRange,
|
||||
ChevronDown,
|
||||
Hand,
|
||||
Mic,
|
||||
Plus,
|
||||
Square,
|
||||
Stethoscope,
|
||||
UserPlus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { ArrowUp, Mic, Plus, Square, UserPlus, X } from "lucide-react";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ModelPicker } from "@/components/chat/model-picker";
|
||||
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
|
||||
import {
|
||||
Menu,
|
||||
MenuPopup,
|
||||
MenuRadioGroup,
|
||||
MenuRadioItem,
|
||||
MenuTrigger,
|
||||
} from "@/components/ui/menu";
|
||||
import type { Effort } from "@/lib/ai-models";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ChatInputProps = {
|
||||
onSubmit: (text: string) => void;
|
||||
status: ChatStatus;
|
||||
onStop?: () => void;
|
||||
model: string;
|
||||
effort: Effort;
|
||||
onModelChange: (model: string) => void;
|
||||
onEffortChange: (effort: Effort) => void;
|
||||
};
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
type OptionKey = { value: string; labelKey: string };
|
||||
|
||||
const ACCESS_OPTIONS: OptionKey[] = [
|
||||
{ value: "standard", labelKey: "chat.input.access.standard" },
|
||||
{ value: "break-glass", labelKey: "chat.input.access.breakGlass" },
|
||||
{ value: "read-only", labelKey: "chat.input.access.readOnly" },
|
||||
];
|
||||
const RESPONSE_OPTIONS: OptionKey[] = [
|
||||
{ value: "concise", labelKey: "chat.input.response.concise" },
|
||||
{ value: "detailed", labelKey: "chat.input.response.detailed" },
|
||||
{ value: "comprehensive", labelKey: "chat.input.response.comprehensive" },
|
||||
];
|
||||
const SPECIALTY_OPTIONS: OptionKey[] = [
|
||||
{ value: "internal-medicine", labelKey: "chat.input.specialtyOptions.internalMedicine" },
|
||||
{ value: "cardiology", labelKey: "chat.input.specialtyOptions.cardiology" },
|
||||
{ value: "pediatrics", labelKey: "chat.input.specialtyOptions.pediatrics" },
|
||||
{ value: "emergency", labelKey: "chat.input.specialtyOptions.emergency" },
|
||||
{ value: "all", labelKey: "chat.input.specialtyOptions.all" },
|
||||
];
|
||||
const FACILITY_OPTIONS: OptionKey[] = [
|
||||
{ value: "main-hospital", labelKey: "chat.input.facilityOptions.mainHospital" },
|
||||
{ value: "north-clinic", labelKey: "chat.input.facilityOptions.northClinic" },
|
||||
{ value: "telehealth", labelKey: "chat.input.facilityOptions.telehealth" },
|
||||
];
|
||||
const TIME_OPTIONS: OptionKey[] = [
|
||||
{ value: "30d", labelKey: "chat.input.timeOptions.30d" },
|
||||
{ value: "12m", labelKey: "chat.input.timeOptions.12m" },
|
||||
{ value: "5y", labelKey: "chat.input.timeOptions.5y" },
|
||||
{ value: "all", labelKey: "chat.input.timeOptions.all" },
|
||||
];
|
||||
|
||||
const iconButton =
|
||||
"flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground";
|
||||
const pillButton =
|
||||
@@ -80,79 +33,19 @@ const pillButton =
|
||||
const contextPill =
|
||||
"flex h-7 items-center gap-1.5 rounded-md px-2 text-[13px] text-muted-foreground transition-colors hover:bg-foreground/5 hover:text-foreground";
|
||||
|
||||
function SelectPill({
|
||||
ariaLabel,
|
||||
triggerClassName,
|
||||
chevronClassName,
|
||||
icon,
|
||||
prefix,
|
||||
value,
|
||||
onValueChange,
|
||||
options,
|
||||
align = "start",
|
||||
}: {
|
||||
ariaLabel: string;
|
||||
triggerClassName: string;
|
||||
chevronClassName: string;
|
||||
icon: ReactNode;
|
||||
prefix?: string;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
options: Option[];
|
||||
align?: "start" | "center" | "end";
|
||||
}) {
|
||||
const selected = options.find((option) => option.value === value);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<button aria-label={ariaLabel} className={triggerClassName} type="button" />
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
{prefix ? (
|
||||
<span className="font-medium text-foreground">{prefix}</span>
|
||||
) : null}
|
||||
<span className="truncate">{selected?.label}</span>
|
||||
<ChevronDown className={chevronClassName} />
|
||||
</MenuTrigger>
|
||||
<MenuPopup align={align}>
|
||||
<MenuRadioGroup onValueChange={onValueChange} value={value}>
|
||||
{options.map((option) => (
|
||||
<MenuRadioItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuRadioItem>
|
||||
))}
|
||||
</MenuRadioGroup>
|
||||
</MenuPopup>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
export function ChatInput({
|
||||
onSubmit,
|
||||
status,
|
||||
onStop,
|
||||
model,
|
||||
effort,
|
||||
onModelChange,
|
||||
onEffortChange,
|
||||
}: ChatInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const toOptions = useCallback(
|
||||
(opts: OptionKey[]): Option[] =>
|
||||
opts.map((o) => ({ value: o.value, label: t(o.labelKey) })),
|
||||
[t],
|
||||
);
|
||||
const accessOptions = useMemo(() => toOptions(ACCESS_OPTIONS), [toOptions]);
|
||||
const responseOptions = useMemo(() => toOptions(RESPONSE_OPTIONS), [toOptions]);
|
||||
const specialtyOptions = useMemo(
|
||||
() => toOptions(SPECIALTY_OPTIONS),
|
||||
[toOptions],
|
||||
);
|
||||
const facilityOptions = useMemo(() => toOptions(FACILITY_OPTIONS), [toOptions]);
|
||||
const timeOptions = useMemo(() => toOptions(TIME_OPTIONS), [toOptions]);
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [access, setAccess] = useState("standard");
|
||||
const [responseMode, setResponseMode] = useState("detailed");
|
||||
const [specialty, setSpecialty] = useState("internal-medicine");
|
||||
const [facility, setFacility] = useState("main-hospital");
|
||||
const [timeRange, setTimeRange] = useState("12m");
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
// Bumped on each open so the dialog remounts with a fresh file number + form.
|
||||
const [addKey, setAddKey] = useState(0);
|
||||
@@ -216,10 +109,10 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
className="w-full overflow-hidden rounded-[28px] border border-border/60 bg-muted shadow-sm"
|
||||
className="w-full overflow-hidden rounded-[28px] border border-border/60 bg-input shadow-sm"
|
||||
>
|
||||
{/* Top (lighter) card: textarea + toolbar, with a slightly smaller bottom radius */}
|
||||
<div className="rounded-b-[22px] bg-input">
|
||||
{/* Textarea + toolbar, filling the rounded card. */}
|
||||
<div className="bg-input">
|
||||
<textarea
|
||||
aria-label={t("chat.input.message")}
|
||||
className="field-sizing-content block max-h-48 min-h-16 w-full resize-none bg-transparent px-5 pt-5 pb-2 text-base text-foreground outline-none placeholder:text-muted-foreground"
|
||||
@@ -270,28 +163,26 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
>
|
||||
<Plus className="size-[18px]" />
|
||||
</button>
|
||||
<SelectPill
|
||||
ariaLabel={t("chat.input.accessLevel")}
|
||||
chevronClassName="size-4 opacity-70"
|
||||
icon={<Hand className="size-4" />}
|
||||
onValueChange={setAccess}
|
||||
options={accessOptions}
|
||||
triggerClassName={pillButton}
|
||||
value={access}
|
||||
/>
|
||||
<button
|
||||
className={cn(contextPill, "ml-0.5")}
|
||||
onClick={() => {
|
||||
setAddKey((k) => k + 1);
|
||||
setAddOpen(true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<UserPlus className="size-4" />
|
||||
<span>{t("chat.input.addPatient")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<SelectPill
|
||||
align="end"
|
||||
ariaLabel={t("chat.input.responseMode")}
|
||||
chevronClassName="size-4 opacity-70"
|
||||
icon={null}
|
||||
onValueChange={setResponseMode}
|
||||
options={responseOptions}
|
||||
prefix={t("chat.input.clinical")}
|
||||
<ModelPicker
|
||||
effort={effort}
|
||||
model={model}
|
||||
onEffortChange={onEffortChange}
|
||||
onModelChange={onModelChange}
|
||||
triggerClassName={cn(pillButton, "mr-1")}
|
||||
value={responseMode}
|
||||
/>
|
||||
<button
|
||||
aria-label={t("chat.input.dictate")}
|
||||
@@ -321,48 +212,6 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom (darker) card peeking out below, more rounded: context selectors */}
|
||||
<div className="flex flex-wrap items-center gap-1 px-3 pt-2.5 pb-3">
|
||||
<SelectPill
|
||||
ariaLabel={t("chat.input.specialty")}
|
||||
chevronClassName="size-3.5 opacity-70"
|
||||
icon={<Stethoscope className="size-4" />}
|
||||
onValueChange={setSpecialty}
|
||||
options={specialtyOptions}
|
||||
triggerClassName={contextPill}
|
||||
value={specialty}
|
||||
/>
|
||||
<SelectPill
|
||||
ariaLabel={t("chat.input.facility")}
|
||||
chevronClassName="size-3.5 opacity-70"
|
||||
icon={<Building2 className="size-4" />}
|
||||
onValueChange={setFacility}
|
||||
options={facilityOptions}
|
||||
triggerClassName={contextPill}
|
||||
value={facility}
|
||||
/>
|
||||
<SelectPill
|
||||
ariaLabel={t("chat.input.timeRange")}
|
||||
chevronClassName="size-3.5 opacity-70"
|
||||
icon={<CalendarRange className="size-4" />}
|
||||
onValueChange={setTimeRange}
|
||||
options={timeOptions}
|
||||
triggerClassName={contextPill}
|
||||
value={timeRange}
|
||||
/>
|
||||
<button
|
||||
className={cn(contextPill, "ml-auto")}
|
||||
onClick={() => {
|
||||
setAddKey((k) => k + 1);
|
||||
setAddOpen(true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<UserPlus className="size-4" />
|
||||
<span>{t("chat.input.addPatient")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<PatientFormDialog
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ai-elements/message";
|
||||
import { ChatInput } from "@/components/chat/chat-input";
|
||||
import { PatientResult } from "@/components/chat/patient-cards";
|
||||
import { DEFAULT_EFFORT, DEFAULT_MODEL_ID, type Effort } from "@/lib/ai-models";
|
||||
import { getPatient, type Patient } from "@/lib/patients";
|
||||
|
||||
type ChatMessage =
|
||||
@@ -39,6 +40,10 @@ export function ChatPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [status, setStatus] = useState<ChatStatus>("ready");
|
||||
// Model + effort selected in the input; travels with each send (wired to the
|
||||
// backend agent in a later phase). Defaults come from the shared catalog.
|
||||
const [model, setModel] = useState<string>(DEFAULT_MODEL_ID);
|
||||
const [effort, setEffort] = useState<Effort>(DEFAULT_EFFORT);
|
||||
|
||||
const send = useCallback(async (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
@@ -127,7 +132,15 @@ export function ChatPanel() {
|
||||
}, [requestedPatient, send]);
|
||||
|
||||
const promptInput = (
|
||||
<ChatInput onStop={handleStop} onSubmit={send} status={status} />
|
||||
<ChatInput
|
||||
effort={effort}
|
||||
model={model}
|
||||
onEffortChange={setEffort}
|
||||
onModelChange={setModel}
|
||||
onStop={handleStop}
|
||||
onSubmit={send}
|
||||
status={status}
|
||||
/>
|
||||
);
|
||||
|
||||
if (messages.length === 0) {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
Menu,
|
||||
MenuPopup,
|
||||
MenuRadioGroup,
|
||||
MenuRadioItem,
|
||||
MenuSeparator,
|
||||
MenuSub,
|
||||
MenuSubPopup,
|
||||
MenuSubTrigger,
|
||||
MenuTrigger,
|
||||
} from "@/components/ui/menu";
|
||||
import {
|
||||
type AiModel,
|
||||
type Effort,
|
||||
EFFORT_LEVELS,
|
||||
getModel,
|
||||
MORE_MODELS,
|
||||
PRIMARY_MODELS,
|
||||
} from "@/lib/ai-models";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ModelPickerProps = {
|
||||
model: string;
|
||||
effort: Effort;
|
||||
onModelChange: (model: string) => void;
|
||||
onEffortChange: (effort: Effort) => void;
|
||||
triggerClassName: string;
|
||||
};
|
||||
|
||||
function ModelRow({ model }: { model: AiModel }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-foreground">{model.label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t(model.descriptionKey)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The single model + effort control in the chat input, styled like the
|
||||
* reference design: a list of primary models, an Effort submenu, and a
|
||||
* "More models" submenu. Selection is owned by the chat panel so it travels
|
||||
* with each send.
|
||||
*/
|
||||
export function ModelPicker({
|
||||
model,
|
||||
effort,
|
||||
onModelChange,
|
||||
onEffortChange,
|
||||
triggerClassName,
|
||||
}: ModelPickerProps) {
|
||||
const { t } = useTranslation();
|
||||
const selected = getModel(model);
|
||||
const showEffort = selected?.supportsEffort ?? false;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<button
|
||||
aria-label={t("chat.input.model")}
|
||||
className={triggerClassName}
|
||||
type="button"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{selected?.label ?? t("chat.input.model")}
|
||||
</span>
|
||||
{showEffort ? (
|
||||
<span className="text-muted-foreground">
|
||||
{t(`chat.input.effortOptions.${effort}`)}
|
||||
</span>
|
||||
) : null}
|
||||
<ChevronDown className="size-4 opacity-70" />
|
||||
</MenuTrigger>
|
||||
<MenuPopup align="end" className="min-w-64">
|
||||
<MenuRadioGroup onValueChange={onModelChange} value={model}>
|
||||
{PRIMARY_MODELS.map((m) => (
|
||||
<MenuRadioItem key={m.id} value={m.id}>
|
||||
<ModelRow model={m} />
|
||||
</MenuRadioItem>
|
||||
))}
|
||||
</MenuRadioGroup>
|
||||
|
||||
<MenuSeparator />
|
||||
|
||||
{showEffort ? (
|
||||
<MenuSub>
|
||||
<MenuSubTrigger>
|
||||
<span>{t("chat.input.effort")}</span>
|
||||
<span className="ms-auto text-muted-foreground">
|
||||
{t(`chat.input.effortOptions.${effort}`)}
|
||||
</span>
|
||||
</MenuSubTrigger>
|
||||
<MenuSubPopup>
|
||||
<MenuRadioGroup
|
||||
onValueChange={(value) => onEffortChange(value as Effort)}
|
||||
value={effort}
|
||||
>
|
||||
{EFFORT_LEVELS.map((level) => (
|
||||
<MenuRadioItem key={level} value={level}>
|
||||
{t(`chat.input.effortOptions.${level}`)}
|
||||
</MenuRadioItem>
|
||||
))}
|
||||
</MenuRadioGroup>
|
||||
</MenuSubPopup>
|
||||
</MenuSub>
|
||||
) : null}
|
||||
|
||||
<MenuSub>
|
||||
<MenuSubTrigger>{t("chat.input.moreModels")}</MenuSubTrigger>
|
||||
<MenuSubPopup className={cn("min-w-60")}>
|
||||
<MenuRadioGroup onValueChange={onModelChange} value={model}>
|
||||
{MORE_MODELS.map((m) => (
|
||||
<MenuRadioItem key={m.id} value={m.id}>
|
||||
<ModelRow model={m} />
|
||||
</MenuRadioItem>
|
||||
))}
|
||||
</MenuRadioGroup>
|
||||
</MenuSubPopup>
|
||||
</MenuSub>
|
||||
</MenuPopup>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Catalog of models the chat can run, shared by the chat-input model picker,
|
||||
// the Settings → AI panel, and the chat→backend wiring. The actual provider
|
||||
// call is made by the backend (`backend/src/services/ai/provider.ts`); here we
|
||||
// only describe the menu of choices. `id` is the provider's model id sent to
|
||||
// the backend; `descriptionKey` is an i18n key under `chat.input.models.*`.
|
||||
|
||||
export type Effort = "low" | "medium" | "high";
|
||||
|
||||
export const EFFORT_LEVELS: Effort[] = ["low", "medium", "high"];
|
||||
export const DEFAULT_EFFORT: Effort = "medium";
|
||||
|
||||
// The three supported cloud providers (API key) plus local Ollama.
|
||||
export type AiProvider = "anthropic" | "openai" | "gemini" | "ollama";
|
||||
|
||||
export type AiModel = {
|
||||
/** Provider model id passed to the backend. */
|
||||
id: string;
|
||||
provider: AiProvider;
|
||||
/** Short display name shown in the picker trigger. */
|
||||
label: string;
|
||||
/** i18n key for the one-line description under the label. */
|
||||
descriptionKey: string;
|
||||
/** Shown directly in the model list; non-primary live under "More models". */
|
||||
primary?: boolean;
|
||||
/** Whether the Effort control applies to this model. */
|
||||
supportsEffort?: boolean;
|
||||
};
|
||||
|
||||
// Curated catalog. Cloud model ids are best-effort current ids and can be
|
||||
// adjusted without touching the UI. Ollama is a stand-in for "your local model"
|
||||
// — the concrete tag is configured in Settings → AI.
|
||||
export const AI_MODELS: AiModel[] = [
|
||||
{
|
||||
id: "claude-opus-4-8",
|
||||
provider: "anthropic",
|
||||
label: "Opus 4.8",
|
||||
descriptionKey: "chat.input.models.opus48",
|
||||
primary: true,
|
||||
supportsEffort: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-6",
|
||||
provider: "anthropic",
|
||||
label: "Sonnet 4.6",
|
||||
descriptionKey: "chat.input.models.sonnet46",
|
||||
primary: true,
|
||||
supportsEffort: true,
|
||||
},
|
||||
{
|
||||
id: "claude-haiku-4-5",
|
||||
provider: "anthropic",
|
||||
label: "Haiku 4.5",
|
||||
descriptionKey: "chat.input.models.haiku45",
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-4o",
|
||||
provider: "openai",
|
||||
label: "GPT-4o",
|
||||
descriptionKey: "chat.input.models.gpt4o",
|
||||
supportsEffort: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-4o-mini",
|
||||
provider: "openai",
|
||||
label: "GPT-4o mini",
|
||||
descriptionKey: "chat.input.models.gpt4oMini",
|
||||
},
|
||||
{
|
||||
id: "gemini-2.0-flash",
|
||||
provider: "gemini",
|
||||
label: "Gemini 2.0 Flash",
|
||||
descriptionKey: "chat.input.models.gemini20Flash",
|
||||
},
|
||||
{
|
||||
id: "gemini-1.5-pro",
|
||||
provider: "gemini",
|
||||
label: "Gemini 1.5 Pro",
|
||||
descriptionKey: "chat.input.models.gemini15Pro",
|
||||
supportsEffort: true,
|
||||
},
|
||||
{
|
||||
id: "ollama",
|
||||
provider: "ollama",
|
||||
label: "Local (Ollama)",
|
||||
descriptionKey: "chat.input.models.ollama",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_MODEL_ID = AI_MODELS[0].id;
|
||||
|
||||
export function getModel(id: string): AiModel | undefined {
|
||||
return AI_MODELS.find((m) => m.id === id);
|
||||
}
|
||||
|
||||
export const PRIMARY_MODELS = AI_MODELS.filter((m) => m.primary);
|
||||
export const MORE_MODELS = AI_MODELS.filter((m) => !m.primary);
|
||||
@@ -616,45 +616,29 @@
|
||||
"placeholder": "Look up a patient — try /patient 10293",
|
||||
"message": "Message",
|
||||
"addPatient": "Add patient",
|
||||
"clinical": "Clinical",
|
||||
"attachFile": "Attach file",
|
||||
"attachFiles": "Attach files",
|
||||
"removeFile": "Remove {{name}}",
|
||||
"dictate": "Dictate",
|
||||
"send": "Send",
|
||||
"stop": "Stop",
|
||||
"accessLevel": "Access level",
|
||||
"responseMode": "Response mode",
|
||||
"specialty": "Specialty",
|
||||
"facility": "Facility",
|
||||
"timeRange": "Time range",
|
||||
"access": {
|
||||
"standard": "Standard access",
|
||||
"breakGlass": "Break-glass (emergency)",
|
||||
"readOnly": "Read-only"
|
||||
"model": "Model",
|
||||
"moreModels": "More models",
|
||||
"effort": "Effort",
|
||||
"effortOptions": {
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High"
|
||||
},
|
||||
"response": {
|
||||
"concise": "Concise",
|
||||
"detailed": "Detailed",
|
||||
"comprehensive": "Comprehensive"
|
||||
},
|
||||
"specialtyOptions": {
|
||||
"internalMedicine": "Internal Medicine",
|
||||
"cardiology": "Cardiology",
|
||||
"pediatrics": "Pediatrics",
|
||||
"emergency": "Emergency",
|
||||
"all": "All specialties"
|
||||
},
|
||||
"facilityOptions": {
|
||||
"mainHospital": "Main Hospital",
|
||||
"northClinic": "North Clinic",
|
||||
"telehealth": "Telehealth"
|
||||
},
|
||||
"timeOptions": {
|
||||
"30d": "Last 30 days",
|
||||
"12m": "Last 12 months",
|
||||
"5y": "Last 5 years",
|
||||
"all": "All time"
|
||||
"models": {
|
||||
"opus48": "For complex clinical reasoning",
|
||||
"sonnet46": "Balanced for everyday tasks",
|
||||
"haiku45": "Fastest for quick answers",
|
||||
"gpt4o": "OpenAI multimodal flagship",
|
||||
"gpt4oMini": "Fast, low-cost OpenAI model",
|
||||
"gemini20Flash": "Fast Google model",
|
||||
"gemini15Pro": "Google long-context model",
|
||||
"ollama": "Runs locally on your infrastructure"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -662,7 +646,10 @@
|
||||
"notFound": "No patient found for file #{{number}}.",
|
||||
"overview": "Overview",
|
||||
"edit": "Edit",
|
||||
"sex": { "F": "Female", "M": "Male" },
|
||||
"sex": {
|
||||
"F": "Female",
|
||||
"M": "Male"
|
||||
},
|
||||
"summary": {
|
||||
"fullName": "Full name",
|
||||
"mrn": "MRN",
|
||||
|
||||
Reference in New Issue
Block a user