Files
ProxMenux/web/app/[locale]/docs/monitor/ai-assistant/page.tsx
MacRimi 5ca3463bf6 complete i18n migration to /[locale]/ with EN+ES content
Full rewrite of the docs site under app/[locale]/ with next-intl
in localePrefix:"always" mode. Every page now exists at both
/en/<path> and /es/<path>; the root / shows a meta-refresh + JS
redirect to /<defaultLocale>/ so GitHub Pages serves something
on the apex URL.

Highlights:
- 107 doc pages migrated to file-per-page JSON namespaces under
  messages/en/ and messages/es/. Spanish content is fully
  translated (no copy-of-English placeholders).
- New documentation for the Active Suppressions section in the
  Settings tab and the per-event Dismiss dropdown in the Health
  Monitor modal.
- New screenshots: dismiss-duration-dropdown.png and an updated
  health-suppression-settings.png.
- Pagefind integrated for client-side search; index is built on
  every CI deploy (not committed).
- RSS feeds: per-locale at /<locale>/rss.xml plus root /rss.xml
  for backward compat.
- Removed the dead app/[locale]/guides/[slug]/ route — every
  guide now has its own static page and no markdown source
  remains.
- Fixed orphan link /guides/nvidia -> /guides/nvidia-manual in
  docs/hardware/nvidia-host.
- Removed obsolete components (footer2, calendar, drawer).

Verified locally with `npm ci && npm run build`: 2804 files in
out/, 231 pages indexed by pagefind, root redirect intact, both
locale roots and the new Active Suppressions docs render OK.
2026-05-31 12:41:10 +02:00

727 lines
32 KiB
TypeScript

import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import Image from "next/image"
import { ExternalLink } from "lucide-react"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.aiAssistant.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox ai",
"proxmox openai integration",
"proxmox claude",
"proxmox gemini",
"proxmox ollama",
"proxmox local ai",
"proxmox groq",
"proxmox openrouter",
"proxmox notification rewrite",
"proxmox llm",
"proxmenux ai assistant",
"proxmox ai prompt",
],
alternates: { canonical: "https://proxmenux.com/docs/monitor/ai-assistant" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/monitor/ai-assistant",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
const DEFAULT_SYSTEM_PROMPT = `You are a notification FORMATTER for ProxMenux Monitor (Proxmox VE).
Your job: translate alerts into {language} and enrich them with context when provided.
═══ ABSOLUTE CONSTRAINTS (NO EXCEPTIONS) ═══
- NO HALLUCINATIONS: Do not invent causes, solutions, or facts not present in the provided data
- NO SPECULATION: If something is unclear, state what IS known, not what MIGHT be
- NO CONVERSATIONAL TEXT: Never write "Here is...", "I've translated...", "Let me explain..."
- ONLY use information from: the message, journal context, and known error database (if provided)
═══ WHAT TO TRANSLATE ═══
Translate: labels, descriptions, status words, units (GB→Go in French, etc.)
DO NOT translate: hostnames, IPs, paths, VM/CT IDs, device names (/dev/sdX), technical identifiers
═══ CORE RULES ═══
1. Plain text only — NO markdown, no **bold**, no \`code\`, no bullet lists (use "• " for packages only)
2. Preserve severity: "failed" stays "failed", "warning" stays "warning" — never soften errors
3. Preserve structure: keep same fields and line order, only translate content
4. Detail level "{detail_level}" - controls AMOUNT OF EVENT INFO (not tips/suggestions):
- brief: 1-2 lines max. Only: what happened + where
- standard: 3-6 lines. Include: what, where, cause, affected devices
- detailed: Full report with ALL info: what, where, cause, affected, logs, SMART data, history
5. DEDUPLICATION: merge duplicate facts from multiple sources into one clear statement
6. EMPTY LISTS: write translated "none" after label, never leave blank
7. Keep "hostname:" prefix in title — translate only the descriptive part
8. DO NOT add recommendations or suggestions UNLESS AI Suggestions mode is enabled below
9. ENRICHED CONTEXT: You may receive additional context data including:
- "System uptime: X days (stable system)" → helps distinguish startup issues from runtime failures
- "Event frequency: N occurrences, first seen X ago" → indicates recurring vs one-time issues
- "SMART Health: PASSED/FAILED" with disk attributes → critical for disk errors
- "KNOWN PROXMOX ERROR DETECTED" with cause/solution → YOU MUST USE this exact information
How to use enriched context:
- If uptime is <10min and error is service-related → mention "occurred shortly after boot"
- If frequency shows recurring pattern → mention "recurring issue (N times in X hours)"
- If SMART shows FAILED → treat as CRITICAL: "Disk failing - immediate attention required"
- If KNOWN ERROR is provided → YOU MUST incorporate its Cause and Solution (translate, don't copy verbatim)
10. JOURNAL CONTEXT EXTRACTION: When journal logs are provided:
- Extract specific IDs (VM/CT numbers, disk devices, service names)
- Include relevant timestamps if they help explain the timeline
- Identify root cause when logs clearly show it (e.g., "exit-code 255" -> "process crashed")
- Translate technical terms: "Emask 0x10" -> "ATA bus error", "DRDY ERR" -> "drive not ready"
- If logs show the same error repeating, state frequency: "occurred 15 times in 10 minutes"
- IGNORE journal entries unrelated to the main event
11. OUTPUT ONLY the final result — no "Original:", no before/after comparisons
12. Unknown input: preserve as closely as possible, translate what you can
13. REDUNDANCY: Never repeat the same information twice. If title says "CT 103 failed", body should not start with "Container 103 failed"
{suggestions_addon}
═══ PROXMOX MAPPINGS (use directly, never explain) ═══
pve-container@XXXX → "CT XXXX" | qemu-server@XXXX → "VM XXXX" | vzdump → "backup"
pveproxy/pvedaemon/pvestatd → "Proxmox service" | corosync → "cluster service"
"ata8.00: exception Emask..." → "ATA error on port 8"
"blk_update_request: I/O error, dev sdX" → "I/O error on /dev/sdX"
{emoji_instructions}
═══ MESSAGE FORMATS ═══
BACKUP: List each VM/CT with status/size/duration/storage. End with summary.
- Partial failure (some OK, some failed) = "Backup partially failed", not "failed"
- NEVER collapse multi-VM backup into one line — show each VM separately
- ALWAYS include storage path and summary line
UPDATES: Counts on own lines. Packages use "• " under header. No redundant summary.
DISK/SMART: Device + specific error. Deduplicate repeated info.
HEALTH: Category + severity + what changed. Duration if resolved.
VM/CT LIFECYCLE: Confirm event with key facts (1-2 lines).
═══ OUTPUT FORMAT (CRITICAL - MUST FOLLOW EXACTLY) ═══
Your response MUST have EXACTLY this structure:
[TITLE]
your translated title text
[BODY]
your translated body text
ABSOLUTE RULES (violations break the parser):
1. [TITLE] and [BODY] are INVISIBLE PARSING MARKERS — they separate title from body
2. Your actual title/body content must NEVER contain the words "[TITLE]" or "[BODY]"
3. Your actual title/body content must NEVER contain "Title:" or "Body:" prefixes
4. Line 1: write exactly [TITLE]
5. Line 2: write your title text (emoji + hostname: description)
6. Line 3: write exactly [BODY]
7. Line 4+: write your body text
- Output ONLY the formatted result — no explanations, no "Original:", no commentary`
const SUGGESTIONS_ADDON = `═══ AI SUGGESTIONS MODE (ENABLED) ═══
You MAY add ONE brief, actionable tip at the END of the body using this exact format:
💡 Tip: [your concise suggestion here]
Rules for the tip:
- ONLY include if the log context or Known Error database clearly points to a specific fix
- Keep under 100 characters
- Be specific: "Run 'pvecm status' to check quorum" NOT "Check cluster status"
- If Known Error provides a solution, YOU MUST USE IT (don't invent your own)
- Never guess — skip the tip if the cause/solution is unclear`
const EXAMPLE_CUSTOM_PROMPT = `You are a notification formatter for ProxMenux Monitor.
Your task is to translate and format server notifications.
RULES:
1. Translate to the user's preferred language
2. Use plain text only (no markdown, no bold, no italic)
3. Be concise and factual
4. Do not add recommendations or suggestions
5. Present only the facts from the input
6. Keep hostname prefix in titles (e.g., "pve01: ")
OUTPUT FORMAT:
[TITLE]
your translated title here
[BODY]
your translated message here
Detail levels:
- brief: 2-3 lines, essential only
- standard: short paragraph with key details
- detailed: full technical breakdown`
type ContextRow = { block: string; when: string; what: string }
type CapRow = { level: string; cap: string; consumption: string }
type DetailRow = { level: string; label: string; cap: string; produce: string }
type PrivacyRow = { provider: string; destination: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function AIAssistantPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.aiAssistant" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { aiAssistant: {
howItWorks: { steps: string[]; notes: string[] }
context: { rows: ContextRow[] }
tokens: { items: string[]; capRows: CapRow[] }
providers: {
groq: { items: string[] }
openai: { items: string[] }
anthropic: { items: string[] }
gemini: { items: string[] }
openrouter: { items: string[] }
ollama: { items: string[] }
}
models: { consequences: string[] }
defaultPrompt: { passages: string[] }
customPrompt: { changes: string[] }
suggestions: { rules: string[] }
detailLevel: { rows: DetailRow[]; defaults: string[] }
language: { rules: string[] }
privacy: { rows: PrivacyRow[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const ai = messages.docs.monitor.aiAssistant
const howSteps = ai.howItWorks.steps
const howNotes = ai.howItWorks.notes
const contextRows = ai.context.rows
const tokensItems = ai.tokens.items
const tokensCapRows = ai.tokens.capRows
const groqItems = ai.providers.groq.items
const openaiItems = ai.providers.openai.items
const anthropicItems = ai.providers.anthropic.items
const geminiItems = ai.providers.gemini.items
const openrouterItems = ai.providers.openrouter.items
const ollamaItems = ai.providers.ollama.items
const modelsConsequences = ai.models.consequences
const defaultPassages = ai.defaultPrompt.passages
const customChanges = ai.customPrompt.changes
const suggestionsRules = ai.suggestions.rules
const detailLevelRows = ai.detailLevel.rows
const detailLevelDefaults = ai.detailLevel.defaults
const languageRules = ai.language.rules
const privacyRows = ai.privacy.rows
const whereNextItems = ai.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const detailLink = (chunks: React.ReactNode) => (
<Link href="#detail-level-per-channel" className="text-blue-600 hover:underline">{chunks}</Link>
)
const notifLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/notifications" className="text-blue-600 hover:underline">{chunks}</Link>
)
const providerLink = (href: string) => (chunks: React.ReactNode) =>
(
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
{chunks}
<ExternalLink className="w-3 h-3" />
</a>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={25}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("howItWorks.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("howItWorks.intro")}</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
{howSteps.map((_, idx) => (
<li key={idx}>{t.rich(`howItWorks.steps.${idx}`, { strong, code, em })}</li>
))}
</ol>
<p className="mb-4 text-gray-800 leading-relaxed">{t("howItWorks.notesIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{howNotes.map((_, idx) => (
<li key={idx}>{t.rich(`howItWorks.notes.${idx}`, { strong })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("enabling.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("enabling.intro", { em })}
</p>
<figure className="my-4">
<Image src="/monitor/settings/ai-enhancement-collapsed.png" alt={t("enabling.collapsedAlt")} width={2000} height={244} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("enabling.collapsedCaption")}</figcaption>
</figure>
<figure className="my-4">
<Image src="/monitor/settings/ai-enhancement-panel.png" alt={t("enabling.panelAlt")} width={2000} height={1854} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("enabling.panelCaption")}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("enabling.outro", { em })}
</p>
<h2 id="what-context-the-ai-receives" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("context.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("context.intro")}</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("context.headerBlock")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("context.headerWhen")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("context.headerWhat")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{contextRows.map((row, idx) => (
<tr key={row.block} className={idx < contextRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.block}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`context.rows.${idx}.when`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`context.rows.${idx}.what`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">{t("context.afterBlocks")}</p>
<CopyableCode
code={`Severity: WARNING
Title: pve01: Disk I/O error
Message:
I/O error on /dev/sda — 1 sector pending reallocation.
Journal log context:
Event frequency: 5 occurrences, first seen 2h ago, recurring
SMART Health: PASSED
SMART attribute Reallocated_Sector_Ct: 1 (raw 1)
SMART attribute Current_Pending_Sector: 1 (raw 1)
Journal logs:
ata8.00: exception Emask 0x10 SAct 0x0 SErr 0x400000 action 0x6
blk_update_request: I/O error, dev sda, sector 4205312
ata8.00: error: { ICRC ABRT }`}
className="my-4"
/>
<Callout variant="info" title={t("context.calloutTitle")}>
{t("context.calloutBody")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("tokens.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("tokens.intro1", { em })}</p>
<p className="mb-4 text-gray-800 leading-relaxed">{t("tokens.intro2")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{tokensItems.map((_, idx) => (
<li key={idx}>{t.rich(`tokens.items.${idx}`, { strong, em, code })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("tokens.capsIntro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("tokens.headerLevel")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("tokens.headerCap")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("tokens.headerConsumption")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{tokensCapRows.map((row, idx) => (
<tr key={row.level} className={idx < tokensCapRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><code>{row.level}</code></td>
<td className="px-3 py-2 align-top">{row.cap}</td>
<td className="px-3 py-2 align-top">{row.consumption}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">{t("tokens.customNote")}</p>
<Callout variant="tip" title={t("tokens.sizingTitle")}>
{t.rich("tokens.sizingBody", { code, link: detailLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("providers.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("providers.intro")}</p>
<figure className="my-4">
<Image src="/monitor/settings/ai-providers-information.png" alt={t("providers.imageAlt")} width={1602} height={2138} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto mx-auto max-w-2xl" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("providers.imageCaption")}</figcaption>
</figure>
<h3 id="provider-groq" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("providers.groq.heading")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed"><em>{t("providers.groq.tagline")}</em></p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{groqItems.map((_, idx) => (
<li key={idx}>{t.rich(`providers.groq.items.${idx}`, { code, strong, a: providerLink("https://console.groq.com/keys") })}</li>
))}
</ul>
<h3 id="provider-openai" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("providers.openai.heading")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed"><em>{t("providers.openai.tagline")}</em></p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{openaiItems.map((_, idx) => (
<li key={idx}>{t.rich(`providers.openai.items.${idx}`, { code, strong, a: providerLink("https://platform.openai.com/api-keys") })}</li>
))}
</ul>
<Callout variant="tip" title={t("providers.openai.baseUrlTitle")}>
{t.rich("providers.openai.baseUrlBody", { em, strong, code })}
</Callout>
<h3 id="provider-anthropic" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("providers.anthropic.heading")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed"><em>{t("providers.anthropic.tagline")}</em></p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{anthropicItems.map((_, idx) => (
<li key={idx}>{t.rich(`providers.anthropic.items.${idx}`, { code, strong, a: providerLink("https://console.anthropic.com/settings/keys") })}</li>
))}
</ul>
<h3 id="provider-gemini" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("providers.gemini.heading")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed"><em>{t("providers.gemini.tagline")}</em></p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{geminiItems.map((_, idx) => (
<li key={idx}>{t.rich(`providers.gemini.items.${idx}`, { code, strong, a: providerLink("https://aistudio.google.com/app/apikey") })}</li>
))}
</ul>
<h3 id="provider-openrouter" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("providers.openrouter.heading")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed"><em>{t("providers.openrouter.tagline")}</em></p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{openrouterItems.map((_, idx) => (
<li key={idx}>{t.rich(`providers.openrouter.items.${idx}`, { code, strong, a: providerLink("https://openrouter.ai/keys") })}</li>
))}
</ul>
<h3 id="provider-ollama" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("providers.ollama.heading")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed"><em>{t("providers.ollama.tagline")}</em></p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{ollamaItems.map((_, idx) => (
<li key={idx}>{t.rich(`providers.ollama.items.${idx}`, { code, strong, em, a: providerLink("https://ollama.com/download") })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("models.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("models.intro", { code })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">{t("models.consequencesIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{modelsConsequences.map((_, idx) => (
<li key={idx}>{t.rich(`models.consequences.${idx}`, { strong })}</li>
))}
</ul>
<Callout variant="info" title={t("models.ollamaTitle")}>
{t("models.ollamaBody")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("defaultPrompt.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("defaultPrompt.intro", { em, code })}
</p>
<details className="mb-4 rounded-md border border-gray-200 bg-gray-50">
<summary className="cursor-pointer px-4 py-3 font-medium text-gray-900 hover:bg-gray-100 rounded-md">
{t("defaultPrompt.showFullSummary")}
</summary>
<div className="px-4 pb-4">
<pre className="text-xs font-mono text-gray-800 whitespace-pre-wrap leading-relaxed bg-white border border-gray-200 rounded p-3 overflow-x-auto">
{DEFAULT_SYSTEM_PROMPT}
</pre>
</div>
</details>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("defaultPrompt.passagesIntro", { em })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{defaultPassages.map((_, idx) => (
<li key={idx}>{t.rich(`defaultPrompt.passages.${idx}`, { strong })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("defaultPrompt.suggestionsPlaceholder", { code })}
</p>
<details className="mb-4 rounded-md border border-gray-200 bg-gray-50">
<summary className="cursor-pointer px-4 py-3 font-medium text-gray-900 hover:bg-gray-100 rounded-md">
{t("defaultPrompt.showAddonSummary")}
</summary>
<div className="px-4 pb-4">
<pre className="text-xs font-mono text-gray-800 whitespace-pre-wrap leading-relaxed bg-white border border-gray-200 rounded p-3 overflow-x-auto">
{SUGGESTIONS_ADDON}
</pre>
</div>
</details>
<h2 id="custom-prompt-mode" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("customPrompt.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("customPrompt.intro", { em })}
</p>
<figure className="my-4">
<Image src="/monitor/settings/ai-custom-prompt.png" alt={t("customPrompt.imageAlt")} width={2000} height={987} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("customPrompt.imageCaption", { em })}</figcaption>
</figure>
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("customPrompt.changesTitle")}</h3>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
{customChanges.map((_, idx) => (
<li key={idx}>{t.rich(`customPrompt.changes.${idx}`, { strong, em, code })}</li>
))}
</ul>
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("customPrompt.starterTitle")}</h3>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("customPrompt.starterIntro", { em })}
</p>
<details className="mb-4 rounded-md border border-gray-200 bg-gray-50">
<summary className="cursor-pointer px-4 py-3 font-medium text-gray-900 hover:bg-gray-100 rounded-md">
{t("customPrompt.showStarterSummary")}
</summary>
<div className="px-4 pb-4">
<pre className="text-xs font-mono text-gray-800 whitespace-pre-wrap leading-relaxed bg-white border border-gray-200 rounded p-3 overflow-x-auto">
{EXAMPLE_CUSTOM_PROMPT}
</pre>
</div>
</details>
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("customPrompt.shareTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("customPrompt.shareIntro", { em, code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
<li>
<a
href="https://github.com/MacRimi/ProxMenux/discussions/categories/share-custom-prompts-for-ai-notifications"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1"
>
{t("customPrompt.shareLinkLabel")}
<ExternalLink className="w-3 h-3" />
</a>
</li>
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">{t("customPrompt.shareOutro")}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("suggestions.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("suggestions.intro", { strong, em })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">{t("suggestions.formatIntro")}</p>
<CopyableCode
code={`💡 Tip: Run 'pvecm status' to check quorum`}
className="my-4"
/>
<p className="mb-4 text-gray-800 leading-relaxed">{t("suggestions.rulesIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{suggestionsRules.map((_, idx) => (
<li key={idx}>{t.rich(`suggestions.rules.${idx}`, { em })}</li>
))}
</ul>
<Callout variant="warning" title={t("suggestions.betaTitle")}>
{t("suggestions.betaBody")}
</Callout>
<h2 id="detail-level-per-channel" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("detailLevel.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("detailLevel.intro")}</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("detailLevel.headerLevel")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("detailLevel.headerLabel")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("detailLevel.headerCap")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("detailLevel.headerProduce")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{detailLevelRows.map((row, idx) => (
<tr key={row.level} className={idx < detailLevelRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><code>{row.level}</code></td>
<td className="px-3 py-2 align-top">{row.label}</td>
<td className="px-3 py-2 align-top">{row.cap}</td>
<td className="px-3 py-2 align-top">{row.produce}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">{t("detailLevel.defaultsIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{detailLevelDefaults.map((_, idx) => (
<li key={idx}>{t.rich(`detailLevel.defaults.${idx}`, { strong, code })}</li>
))}
</ul>
<Callout variant="info" title={t("detailLevel.emailTitle")}>
{t.rich("detailLevel.emailBody", { code })}
</Callout>
<h2 id="language" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("language.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("language.intro", { code, em })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("language.list", { code })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">{t("language.rulesIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{languageRules.map((_, idx) => (
<li key={idx}>{t.rich(`language.rules.${idx}`, { strong, code })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("language.customNote", { strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("templates.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("templates.body1", { code })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("templates.body2", { link: notifLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("privacy.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("privacy.intro")}</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("privacy.headerProvider")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("privacy.headerDestination")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{privacyRows.map((row, idx) => (
<tr key={row.provider} className={idx < privacyRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.provider}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`privacy.rows.${idx}.destination`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="warning" title={t("privacy.calloutTitle")}>
{t("privacy.calloutBody")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.map((item) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tail}
</li>
))}
<li>
<a
href="https://github.com/MacRimi/ProxMenux/discussions/categories/share-custom-prompts-for-ai-notifications"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1"
>
{t("whereNext.communityLabel")}
<ExternalLink className="w-3 h-3" />
</a>
{t("whereNext.communityTail")}
</li>
</ul>
</div>
)
}