mirror of
https://github.com/temetro/temetro.git
synced 2026-08-21 15:36:47 +00:00
fix: chat works with any configured provider + surfaces errors
The chat defaulted to a Claude model, so a user who saved only a Gemini key got a silent failure (backend derived Anthropic, found no key, errored — and the UI showed nothing). - Backend: resolveModel now falls back to whichever provider actually has a key (preferring the configured one), so a Gemini key just works regardless of the picked model. Clear 400 if no provider is configured at all. - Frontend: the chat seeds its model/effort from the saved AI config, and now surfaces request failures as both a persistent alert banner and a toast — never silent. - Settings: switching provider auto-selects that provider's default model. - Refreshed the model catalog to current ids (Gemini 2.5 Pro/Flash + 2.0 Flash; dropped the retired gemini-1.5-pro); per-provider default model updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -38,8 +39,10 @@ import {
|
||||
getModel,
|
||||
} from "@/lib/ai-models";
|
||||
import type { TemetroUIMessage } from "@/lib/ai-chat";
|
||||
import { getAiConfig } from "@/lib/ai-settings";
|
||||
import { API_BASE_URL } from "@/lib/api-client";
|
||||
import { getPatient } from "@/lib/patients";
|
||||
import { notify } from "@/lib/toast";
|
||||
|
||||
// Trigger: `/patient 10293` or just `/10293` — a client-side fast-path that
|
||||
// pulls records instantly without the LLM (also works offline).
|
||||
@@ -65,9 +68,36 @@ export function ChatPanel() {
|
||||
[],
|
||||
);
|
||||
|
||||
const { messages, setMessages, sendMessage, status, stop } =
|
||||
const { messages, setMessages, sendMessage, status, stop, error } =
|
||||
useChat<TemetroUIMessage>({ transport });
|
||||
|
||||
// Seed the model + effort from the user's saved AI config so the chat uses the
|
||||
// provider they actually configured (e.g. their Gemini default), not a stale
|
||||
// hardcoded default.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
getAiConfig()
|
||||
.then((cfg) => {
|
||||
if (cancelled) return;
|
||||
setModel(cfg.mode === "local" ? "ollama" : cfg.defaultModel);
|
||||
setEffort(cfg.defaultEffort);
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep defaults; the chat still works and the backend falls back to any
|
||||
// configured provider.
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Pop a toast whenever a request errors, so failures are never silent.
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
notify.error(t("chat.error.title"), error.message || t("chat.error.body"));
|
||||
}
|
||||
}, [error, t]);
|
||||
|
||||
const isCloudModel = (getModel(model)?.provider ?? "ollama") !== "ollama";
|
||||
|
||||
// Run the LLM agent for a message (after any consent gate).
|
||||
@@ -158,6 +188,21 @@ export function ChatPanel() {
|
||||
/>
|
||||
);
|
||||
|
||||
const errorAlert = error ? (
|
||||
<div
|
||||
className="flex w-full items-start gap-2 rounded-2xl border border-destructive/40 bg-destructive/8 px-4 py-3 text-sm text-destructive-foreground"
|
||||
role="alert"
|
||||
>
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
|
||||
<div className="space-y-0.5">
|
||||
<p className="font-medium">{t("chat.error.title")}</p>
|
||||
<p className="text-destructive-foreground/90">
|
||||
{error.message || t("chat.error.body")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const consentDialog = (
|
||||
<Dialog onOpenChange={setConsentOpen} open={consentOpen}>
|
||||
<DialogPopup>
|
||||
@@ -191,7 +236,10 @@ export function ChatPanel() {
|
||||
<h1 className="text-center text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
||||
{t("chat.heading")}
|
||||
</h1>
|
||||
{promptInput}
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
{errorAlert}
|
||||
{promptInput}
|
||||
</div>
|
||||
</div>
|
||||
{consentDialog}
|
||||
</div>
|
||||
@@ -240,7 +288,10 @@ export function ChatPanel() {
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
<div className="mx-auto w-full max-w-3xl px-4 pb-4">{promptInput}</div>
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-3 px-4 pb-4">
|
||||
{errorAlert}
|
||||
{promptInput}
|
||||
</div>
|
||||
{consentDialog}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -71,6 +71,19 @@ export function AIPanel() {
|
||||
const set = <K extends keyof AiConfig>(key: K, value: AiConfig[K]) =>
|
||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
// Switching provider: if the current default model doesn't belong to the new
|
||||
// provider, pick that provider's first model so they never end up mismatched.
|
||||
const setProvider = (provider: ApiProvider) =>
|
||||
setConfig((prev) => {
|
||||
const models = AI_MODELS.filter((m) => m.provider === provider);
|
||||
const stillValid = models.some((m) => m.id === prev.defaultModel);
|
||||
return {
|
||||
...prev,
|
||||
provider,
|
||||
defaultModel: stillValid ? prev.defaultModel : (models[0]?.id ?? prev.defaultModel),
|
||||
};
|
||||
});
|
||||
|
||||
// Models available for the currently selected cloud provider.
|
||||
const providerModels = useMemo(
|
||||
() => AI_MODELS.filter((m) => m.provider === config.provider),
|
||||
@@ -169,9 +182,7 @@ export function AIPanel() {
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel>{t("settings.ai.provider")}</FieldLabel>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
set("provider", value as ApiProvider)
|
||||
}
|
||||
onValueChange={(value) => setProvider(value as ApiProvider)}
|
||||
value={config.provider}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
||||
Reference in New Issue
Block a user