mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
frontend: one-time AI setup notice above the chat input
Show a single, dismissible heads-up on a fresh chat when no AI provider (an API key or a local Ollama endpoint) is configured, linking to Settings → AI. It only renders on the empty state, so it clears itself once the first message is sent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Sparkles, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { getAiConfig } from "@/lib/ai-settings";
|
||||||
|
|
||||||
|
// A single, dismissible heads-up shown above the chat input on a fresh chat when
|
||||||
|
// no AI provider is configured yet (no API key, and no local Ollama). It only
|
||||||
|
// renders on the empty state, so it naturally disappears once a message is sent.
|
||||||
|
export function AiSetupNotice() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [needsSetup, setNeedsSetup] = useState(false);
|
||||||
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
getAiConfig()
|
||||||
|
.then((cfg) => {
|
||||||
|
if (!active) return;
|
||||||
|
// Configured = an API key for any provider, or a local Ollama endpoint.
|
||||||
|
const hasApiKey = Object.values(cfg.apiKeySet).some(Boolean);
|
||||||
|
const hasLocal =
|
||||||
|
cfg.mode === "local" && cfg.ollamaBaseUrl.trim().length > 0;
|
||||||
|
setNeedsSetup(!(hasApiKey || hasLocal));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// If we can't read the config, don't nag — the chat still works.
|
||||||
|
if (active) setNeedsSetup(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!needsSetup || dismissed) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex w-full items-start gap-3 rounded-2xl border border-info/30 bg-info/8 px-4 py-3 text-sm dark:bg-info/12"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<Sparkles className="mt-0.5 size-4 shrink-0 text-info-foreground" />
|
||||||
|
<div className="flex-1 space-y-0.5">
|
||||||
|
<p className="font-medium text-foreground">
|
||||||
|
{t("chat.setupNotice.title")}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground">{t("chat.setupNotice.body")}</p>
|
||||||
|
<Button
|
||||||
|
className="mt-1 px-0 text-info-foreground"
|
||||||
|
render={<Link href="/settings?tab=ai" />}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
>
|
||||||
|
{t("chat.setupNotice.action")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label={t("chat.setupNotice.dismiss")}
|
||||||
|
className="-mr-1 shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||||
|
onClick={() => setDismissed(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
ToolOutput,
|
ToolOutput,
|
||||||
} from "@/components/ai-elements/tool";
|
} from "@/components/ai-elements/tool";
|
||||||
import { ActionPreviewCard } from "@/components/chat/action-preview-card";
|
import { ActionPreviewCard } from "@/components/chat/action-preview-card";
|
||||||
|
import { AiSetupNotice } from "@/components/chat/ai-setup-notice";
|
||||||
import { AnalyticsCard } from "@/components/chat/analytics-card";
|
import { AnalyticsCard } from "@/components/chat/analytics-card";
|
||||||
import { BatchActionPreviewCard } from "@/components/chat/batch-action-preview-card";
|
import { BatchActionPreviewCard } from "@/components/chat/batch-action-preview-card";
|
||||||
import { ChatHistoryPanel } from "@/components/chat/chat-history-panel";
|
import { ChatHistoryPanel } from "@/components/chat/chat-history-panel";
|
||||||
@@ -722,6 +723,9 @@ export function ChatPanel() {
|
|||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
{errorAlert}
|
{errorAlert}
|
||||||
{veilGate}
|
{veilGate}
|
||||||
|
{/* One-time setup heads-up — only on the empty state, so it clears
|
||||||
|
itself once the first message is sent. */}
|
||||||
|
<AiSetupNotice />
|
||||||
{promptInput}
|
{promptInput}
|
||||||
<Suggestions className="justify-center pt-1">
|
<Suggestions className="justify-center pt-1">
|
||||||
{suggestions.map((s) => (
|
{suggestions.map((s) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user