mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
frontend: dismissible chat error alert + compact import card
- the inline chat error now has an X to dismiss it, and is the single surface for errors (dropped the duplicate toast) so failures show once and clearly - the import preview card caps the skipped-rows list (scrolls, one line each with a "+N more" tail) so a file full of validation errors no longer renders a giant card Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { AlertTriangle, ShieldCheck } from "lucide-react";
|
||||
import { AlertTriangle, ShieldCheck, X } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -47,7 +47,6 @@ 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).
|
||||
@@ -96,12 +95,13 @@ export function ChatPanel() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Pop a toast whenever a request errors, so failures are never silent.
|
||||
// Surface errors inline (and dismissible) instead of as a toast, so a failure
|
||||
// stays visible until acknowledged and isn't duplicated. Reset the dismissed
|
||||
// flag whenever a fresh error arrives.
|
||||
const [errorDismissed, setErrorDismissed] = useState(false);
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
notify.error(t("chat.error.title"), error.message || t("chat.error.body"));
|
||||
}
|
||||
}, [error, t]);
|
||||
if (error) setErrorDismissed(false);
|
||||
}, [error]);
|
||||
|
||||
const isCloudModel = (getModel(model)?.provider ?? "ollama") !== "ollama";
|
||||
|
||||
@@ -210,20 +210,29 @@ export function ChatPanel() {
|
||||
/>
|
||||
) : null;
|
||||
|
||||
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-destructive-foreground text-sm"
|
||||
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>
|
||||
const errorAlert =
|
||||
error && !errorDismissed ? (
|
||||
<div
|
||||
className="flex w-full items-start gap-2 rounded-2xl border border-destructive/40 bg-destructive/8 px-4 py-3 text-destructive-foreground text-sm"
|
||||
role="alert"
|
||||
>
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
|
||||
<div className="flex-1 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>
|
||||
<button
|
||||
aria-label={t("chat.error.dismiss")}
|
||||
className="-mr-1 shrink-0 rounded-md p-1 text-destructive-foreground/70 transition-colors hover:bg-destructive/10 hover:text-destructive-foreground"
|
||||
onClick={() => setErrorDismissed(true)}
|
||||
type="button"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
) : null;
|
||||
|
||||
// Render one assistant/user message: a Chain-of-Thought trace built from any
|
||||
// `data-step` parts, then the rest of the parts (text + record cards) in order.
|
||||
|
||||
@@ -67,13 +67,19 @@ export function ImportPreviewCard({ data }: { data: ImportPreviewData }) {
|
||||
</div>
|
||||
|
||||
{data.invalid.length > 0 ? (
|
||||
<ul className="space-y-1 rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground">
|
||||
<ul className="max-h-40 space-y-1 overflow-y-auto rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground">
|
||||
{data.invalid.slice(0, 5).map((issue) => (
|
||||
<li key={issue.index}>
|
||||
<li className="truncate" key={issue.index}>
|
||||
{t("chat.importCard.row", { index: issue.index + 1 })}:{" "}
|
||||
{issue.errors.join("; ")}
|
||||
{issue.errors[0]}
|
||||
{issue.errors.length > 1 ? ` (+${issue.errors.length - 1})` : ""}
|
||||
</li>
|
||||
))}
|
||||
{data.invalid.length > 5 ? (
|
||||
<li className="text-muted-foreground/70">
|
||||
{t("chat.importCard.more", { count: data.invalid.length - 5 })}
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -745,6 +745,7 @@
|
||||
"skipped": "Skipped",
|
||||
"total": "Parsed",
|
||||
"row": "Row {{index}}",
|
||||
"more": "+{{count}} more",
|
||||
"approve": "Import {{count}} record(s)",
|
||||
"approve_one": "Import 1 record",
|
||||
"reject": "Discard",
|
||||
@@ -758,7 +759,8 @@
|
||||
},
|
||||
"error": {
|
||||
"title": "AI request failed",
|
||||
"body": "Something went wrong reaching the AI. Check your provider and API key in Settings → AI."
|
||||
"body": "Something went wrong reaching the AI. Check your provider and API key in Settings → AI.",
|
||||
"dismiss": "Dismiss"
|
||||
}
|
||||
},
|
||||
"patientCard": {
|
||||
|
||||
Reference in New Issue
Block a user