Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions v5/messages/ar.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "إبلاغ",
"reportAria": "الإبلاغ عن مشكلة",
"reportSeed": "أريد الإبلاغ عن مشكلة.",
"tools": "الأدوات",
"projects": "المشاريع",
"about": "حول",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/en.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/es.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "REPORTAR",
"reportAria": "Reportar un problema",
"reportSeed": "Quiero reportar un problema.",
"tools": "HERRAMIENTAS",
"projects": "PROYECTOS",
"about": "ACERCA DE",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/fr.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/he.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "דיווח",
"reportAria": "דיווח על תקלה",
"reportSeed": "אני רוצה לדווח על תקלה.",
"tools": "כלים",
"projects": "פרויקטים",
"about": "אודות",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/hi.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "रिपोर्ट करें",
"reportAria": "समस्या की रिपोर्ट करें",
"reportSeed": "मैं एक समस्या की रिपोर्ट करना चाहता हूँ।",
"tools": "उपकरण",
"projects": "परियोजनाएँ",
"about": "परिचय",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/ja.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "報告",
"reportAria": "問題を報告",
"reportSeed": "問題を報告したいです。",
"tools": "ツール",
"projects": "プロジェクト",
"about": "概要",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/ko.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "신고",
"reportAria": "문제 신고",
"reportSeed": "문제를 신고하고 싶어요.",
"tools": "도구",
"projects": "프로젝트",
"about": "소개",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/pt-BR.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "RELATAR",
"reportAria": "Relatar um problema",
"reportSeed": "Quero relatar um problema.",
"tools": "FERRAMENTAS",
"projects": "PROJETOS",
"about": "SOBRE",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/ru.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "СООБЩИТЬ",
"reportAria": "Сообщить о проблеме",
"reportSeed": "Я хочу сообщить о проблеме.",
"tools": "ИНСТРУМЕНТЫ",
"projects": "ПРОЕКТЫ",
"about": "О ПРОЕКТЕ",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/tr.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "BİLDİR",
"reportAria": "Sorun bildir",
"reportSeed": "Bir sorun bildirmek istiyorum.",
"tools": "ARAÇLAR",
"projects": "PROJELER",
"about": "HAKKINDA",
Expand Down
3 changes: 3 additions & 0 deletions v5/messages/zh-CN.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"nav": {
"report": "报告",
"reportAria": "报告问题",
"reportSeed": "我想报告一个问题。",
"tools": "工具",
"projects": "项目",
"about": "关于",
Expand Down
13 changes: 8 additions & 5 deletions v5/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -53,11 +54,13 @@ async function LocalizedTree({
}) {
return (
<NextIntlClientProvider>
<GlobalChrome stats={catalogStats} />
{children}
<Suspense fallback={null}>
<ChatFab />
</Suspense>
<ChatLauncherProvider>
<GlobalChrome stats={catalogStats} />
{children}
<Suspense fallback={null}>
<ChatFab />
</Suspense>
</ChatLauncherProvider>
</NextIntlClientProvider>
);
}
26 changes: 19 additions & 7 deletions v5/src/components/ChatFab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -259,10 +260,6 @@ export function ChatFab() {
}
}, [messages]);

function close() {
setIsOpen(false);
}

const markdownComponents = useMemo<Components>(
() => ({
a({ href, children }) {
Expand All @@ -282,7 +279,7 @@ export function ChatFab() {
);
},
}),
[]
[close]
);

// Send a message and clear the stale "Reading: …manuals…" indicator from any
Expand All @@ -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<number | null>(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);
Expand Down Expand Up @@ -326,7 +338,7 @@ export function ChatFab() {
aria-expanded={isOpen}
aria-controls="makerlab-chat-sheet"
aria-label={t("openAria")}
onClick={() => setIsOpen(true)}
onClick={() => open()}
>
&gt;_
</button>
Expand Down
80 changes: 80 additions & 0 deletions v5/src/components/ChatLauncherContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ChatLauncher | null>(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<ChatSeed | null>(null);

const open = useCallback((seedText?: string) => {
setIsOpen(true);
if (seedText) {
setPendingSeed((prev) => ({
text: seedText,
nonce: (prev?.nonce ?? 0) + 1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve seed nonce across consumed seeds

After ChatFab consumes a seed, pendingSeed is set back to null, so the next open(t("reportSeed")) call recomputes the nonce as 1 again. Since ChatFab keeps lastSeedNonce.current at 1, a user who clicks REPORT a second time after the first seeded turn has been sent will only open the sheet; the seed is skipped and no report prompt is sent. Keep a monotonic counter outside the consumed seed state (or otherwise make each seed unique after consumption).

Useful? React with 👍 / 👎.

}));
}
}, []);

const close = useCallback(() => setIsOpen(false), []);
const consumeSeed = useCallback(() => setPendingSeed(null), []);

const value = useMemo(
() => ({ isOpen, pendingSeed, open, close, consumeSeed }),
[isOpen, pendingSeed, open, close, consumeSeed]
);

return (
<ChatLauncherContext.Provider value={value}>
{children}
</ChatLauncherContext.Provider>
);
}

export function useChatLauncher(): ChatLauncher {
const ctx = useContext(ChatLauncherContext);
if (!ctx) {
throw new Error("useChatLauncher must be used within a ChatLauncherProvider");
}
return ctx;
}
10 changes: 10 additions & 0 deletions v5/src/components/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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") },
Expand All @@ -13,6 +14,7 @@ const LINKS = [
export function PrimaryNav() {
const pathname = usePathname() || "/";
const t = useTranslations("nav");
const { open } = useChatLauncher();

return (
<nav className="primary-nav" aria-label={t("primaryNavLabel")}>
Expand All @@ -25,6 +27,14 @@ export function PrimaryNav() {
{t(link.key)}
</Link>
))}
<button
type="button"
className="primary-nav-report"
onClick={() => open(t("reportSeed"))}
aria-label={t("reportAria")}
>
{t("report")}
</button>
</nav>
);
}
19 changes: 19 additions & 0 deletions v5/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading