diff --git a/v5/messages/ar.json b/v5/messages/ar.json
index 16c21e5..986ef66 100644
--- a/v5/messages/ar.json
+++ b/v5/messages/ar.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "إبلاغ",
+ "reportAria": "الإبلاغ عن مشكلة",
+ "reportSeed": "أريد الإبلاغ عن مشكلة.",
"tools": "الأدوات",
"projects": "المشاريع",
"about": "حول",
diff --git a/v5/messages/en.json b/v5/messages/en.json
index 5f359df..5d4389d 100644
--- a/v5/messages/en.json
+++ b/v5/messages/en.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "REPORT",
+ "reportAria": "Report a problem",
+ "reportSeed": "I'd like to report a problem.",
"tools": "TOOLS",
"projects": "PROJECTS",
"about": "ABOUT",
diff --git a/v5/messages/es.json b/v5/messages/es.json
index f9a7d58..22a7c67 100644
--- a/v5/messages/es.json
+++ b/v5/messages/es.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "REPORTAR",
+ "reportAria": "Reportar un problema",
+ "reportSeed": "Quiero reportar un problema.",
"tools": "HERRAMIENTAS",
"projects": "PROYECTOS",
"about": "ACERCA DE",
diff --git a/v5/messages/fr.json b/v5/messages/fr.json
index 2568804..0780a96 100644
--- a/v5/messages/fr.json
+++ b/v5/messages/fr.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "SIGNALER",
+ "reportAria": "Signaler un problème",
+ "reportSeed": "Je voudrais signaler un problème.",
"tools": "OUTILS",
"projects": "PROJETS",
"about": "À PROPOS",
diff --git a/v5/messages/he.json b/v5/messages/he.json
index 0befa99..f126d43 100644
--- a/v5/messages/he.json
+++ b/v5/messages/he.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "דיווח",
+ "reportAria": "דיווח על תקלה",
+ "reportSeed": "אני רוצה לדווח על תקלה.",
"tools": "כלים",
"projects": "פרויקטים",
"about": "אודות",
diff --git a/v5/messages/hi.json b/v5/messages/hi.json
index bb6a007..a9cb2a6 100644
--- a/v5/messages/hi.json
+++ b/v5/messages/hi.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "रिपोर्ट करें",
+ "reportAria": "समस्या की रिपोर्ट करें",
+ "reportSeed": "मैं एक समस्या की रिपोर्ट करना चाहता हूँ।",
"tools": "उपकरण",
"projects": "परियोजनाएँ",
"about": "परिचय",
diff --git a/v5/messages/ja.json b/v5/messages/ja.json
index 38308e2..53c8a2f 100644
--- a/v5/messages/ja.json
+++ b/v5/messages/ja.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "報告",
+ "reportAria": "問題を報告",
+ "reportSeed": "問題を報告したいです。",
"tools": "ツール",
"projects": "プロジェクト",
"about": "概要",
diff --git a/v5/messages/ko.json b/v5/messages/ko.json
index 1950d6a..8635c90 100644
--- a/v5/messages/ko.json
+++ b/v5/messages/ko.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "신고",
+ "reportAria": "문제 신고",
+ "reportSeed": "문제를 신고하고 싶어요.",
"tools": "도구",
"projects": "프로젝트",
"about": "소개",
diff --git a/v5/messages/pt-BR.json b/v5/messages/pt-BR.json
index 42be387..ce91fb8 100644
--- a/v5/messages/pt-BR.json
+++ b/v5/messages/pt-BR.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "RELATAR",
+ "reportAria": "Relatar um problema",
+ "reportSeed": "Quero relatar um problema.",
"tools": "FERRAMENTAS",
"projects": "PROJETOS",
"about": "SOBRE",
diff --git a/v5/messages/ru.json b/v5/messages/ru.json
index 38e4721..f36aaca 100644
--- a/v5/messages/ru.json
+++ b/v5/messages/ru.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "СООБЩИТЬ",
+ "reportAria": "Сообщить о проблеме",
+ "reportSeed": "Я хочу сообщить о проблеме.",
"tools": "ИНСТРУМЕНТЫ",
"projects": "ПРОЕКТЫ",
"about": "О ПРОЕКТЕ",
diff --git a/v5/messages/tr.json b/v5/messages/tr.json
index 633b91e..657765e 100644
--- a/v5/messages/tr.json
+++ b/v5/messages/tr.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "BİLDİR",
+ "reportAria": "Sorun bildir",
+ "reportSeed": "Bir sorun bildirmek istiyorum.",
"tools": "ARAÇLAR",
"projects": "PROJELER",
"about": "HAKKINDA",
diff --git a/v5/messages/zh-CN.json b/v5/messages/zh-CN.json
index 0a8768b..79c087a 100644
--- a/v5/messages/zh-CN.json
+++ b/v5/messages/zh-CN.json
@@ -1,5 +1,8 @@
{
"nav": {
+ "report": "报告",
+ "reportAria": "报告问题",
+ "reportSeed": "我想报告一个问题。",
"tools": "工具",
"projects": "项目",
"about": "关于",
diff --git a/v5/src/app/layout.tsx b/v5/src/app/layout.tsx
index 0d921b9..866ee3d 100644
--- a/v5/src/app/layout.tsx
+++ b/v5/src/app/layout.tsx
@@ -3,6 +3,7 @@ import { Suspense } from "react";
import { NextIntlClientProvider } from "next-intl";
import "../styles/globals.css";
import { ChatFab } from "../components/ChatFab";
+import { ChatLauncherProvider } from "../components/ChatLauncherContext";
import { GlobalChrome } from "../components/GlobalChrome";
import { ThemeScript } from "../components/ThemeScript";
import { LocaleHtmlScript } from "../components/LocaleHtmlScript";
@@ -53,11 +54,13 @@ async function LocalizedTree({
}) {
return (
-
- {children}
-
-
-
+
+
+ {children}
+
+
+
+
);
}
diff --git a/v5/src/components/ChatFab.tsx b/v5/src/components/ChatFab.tsx
index 18de337..5c022e6 100644
--- a/v5/src/components/ChatFab.tsx
+++ b/v5/src/components/ChatFab.tsx
@@ -8,6 +8,7 @@ import { usePathname } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
+import { useChatLauncher } from "./ChatLauncherContext";
interface Suggestion {
icon: "search" | "clipboard" | "pin";
@@ -101,7 +102,7 @@ interface PendingPhoto {
export function ChatFab() {
const t = useTranslations("chat");
const locale = useLocale();
- const [isOpen, setIsOpen] = useState(false);
+ const { isOpen, open, close, pendingSeed, consumeSeed } = useChatLauncher();
const [draft, setDraft] = useState("");
const pathname = usePathname() || "/";
const toolId = useMemo(() => {
@@ -259,10 +260,6 @@ export function ChatFab() {
}
}, [messages]);
- function close() {
- setIsOpen(false);
- }
-
const markdownComponents = useMemo(
() => ({
a({ href, children }) {
@@ -282,7 +279,7 @@ export function ChatFab() {
);
},
}),
- []
+ [close]
);
// Send a message and clear the stale "Reading: …manuals…" indicator from any
@@ -294,6 +291,21 @@ export function ChatFab() {
sendMessage({ text });
}
+ // Auto-send a seeded message when something outside ChatFab (e.g. the nav
+ // "Report" button) opens the chat with an intent. The nonce guard makes this
+ // idempotent so a re-render never resends, and we wait until any in-flight
+ // turn finishes before sending.
+ const lastSeedNonce = useRef(null);
+ useEffect(() => {
+ if (!pendingSeed || isLoading) return;
+ if (lastSeedNonce.current !== pendingSeed.nonce) {
+ lastSeedNonce.current = pendingSeed.nonce;
+ send(pendingSeed.text);
+ }
+ consumeSeed();
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `send`/`consumeSeed` are stable for this purpose; the nonce ref guards against resends.
+ }, [pendingSeed, isLoading]);
+
function handleSuggestion(label: string) {
if (isLoading) return;
send(label);
@@ -326,7 +338,7 @@ export function ChatFab() {
aria-expanded={isOpen}
aria-controls="makerlab-chat-sheet"
aria-label={t("openAria")}
- onClick={() => setIsOpen(true)}
+ onClick={() => open()}
>
>_
diff --git a/v5/src/components/ChatLauncherContext.tsx b/v5/src/components/ChatLauncherContext.tsx
new file mode 100644
index 0000000..4d0ca77
--- /dev/null
+++ b/v5/src/components/ChatLauncherContext.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+} from "react";
+
+/**
+ * A one-shot message to auto-send into the chat. The nonce lets the same text
+ * re-trigger a send on repeat clicks — a plain string wouldn't change, so the
+ * consumer's effect wouldn't fire again.
+ */
+interface ChatSeed {
+ text: string;
+ nonce: number;
+}
+
+interface ChatLauncher {
+ /** Whether the chat sheet is open. */
+ isOpen: boolean;
+ /** A pending message to auto-send, or null. `ChatFab` consumes it. */
+ pendingSeed: ChatSeed | null;
+ /** Open the chat. Pass `seedText` to also auto-send an opening message. */
+ open: (seedText?: string) => void;
+ /** Close the chat (the conversation is preserved). */
+ close: () => void;
+ /** Clear the pending seed once it has been sent. */
+ consumeSeed: () => void;
+}
+
+const ChatLauncherContext = createContext(null);
+
+/**
+ * Owns just the chat launcher's open state and any one-shot seed message, so
+ * that entry points outside `ChatFab` (e.g. the nav "Report" button) can open
+ * and seed the chat. All conversation/message state stays inside `ChatFab`.
+ */
+export function ChatLauncherProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [pendingSeed, setPendingSeed] = useState(null);
+
+ const open = useCallback((seedText?: string) => {
+ setIsOpen(true);
+ if (seedText) {
+ setPendingSeed((prev) => ({
+ text: seedText,
+ nonce: (prev?.nonce ?? 0) + 1,
+ }));
+ }
+ }, []);
+
+ const close = useCallback(() => setIsOpen(false), []);
+ const consumeSeed = useCallback(() => setPendingSeed(null), []);
+
+ const value = useMemo(
+ () => ({ isOpen, pendingSeed, open, close, consumeSeed }),
+ [isOpen, pendingSeed, open, close, consumeSeed]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useChatLauncher(): ChatLauncher {
+ const ctx = useContext(ChatLauncherContext);
+ if (!ctx) {
+ throw new Error("useChatLauncher must be used within a ChatLauncherProvider");
+ }
+ return ctx;
+}
diff --git a/v5/src/components/PrimaryNav.tsx b/v5/src/components/PrimaryNav.tsx
index d86e96d..24463fb 100644
--- a/v5/src/components/PrimaryNav.tsx
+++ b/v5/src/components/PrimaryNav.tsx
@@ -3,6 +3,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslations } from "next-intl";
+import { useChatLauncher } from "./ChatLauncherContext";
const LINKS = [
{ href: "/", key: "tools", match: (path: string) => path === "/" || path.startsWith("/tools") },
@@ -13,6 +14,7 @@ const LINKS = [
export function PrimaryNav() {
const pathname = usePathname() || "/";
const t = useTranslations("nav");
+ const { open } = useChatLauncher();
return (
);
}
diff --git a/v5/src/styles/globals.css b/v5/src/styles/globals.css
index 3fbd808..a3dfc58 100644
--- a/v5/src/styles/globals.css
+++ b/v5/src/styles/globals.css
@@ -180,6 +180,25 @@ img {
border-color: var(--primary);
}
+/* "Report" is an action, not a page — accent-colored to stand out, but it
+ inherits the nav's mono/uppercase/size so it sits inline with the links. */
+.primary-nav-report {
+ padding-block: 8px;
+ border: none;
+ border-bottom: 2px solid transparent;
+ background: transparent;
+ color: var(--primary);
+ font: inherit;
+ cursor: pointer;
+ transition: color 160ms ease-in, border-color 160ms ease-in;
+}
+
+.primary-nav-report:hover,
+.primary-nav-report:focus-visible {
+ border-color: var(--primary);
+ outline: none;
+}
+
.nav-actions {
display: flex;
align-items: center;