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 { 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) => {chunks} const strong = (chunks: React.ReactNode) => {chunks} const em = (chunks: React.ReactNode) => {chunks} const detailLink = (chunks: React.ReactNode) => ( {chunks} ) const notifLink = (chunks: React.ReactNode) => ( {chunks} ) const providerLink = (href: string) => (chunks: React.ReactNode) => ( {chunks} ) return (
{t.rich("intro.body", { em })}

{t("howItWorks.heading")}

{t("howItWorks.intro")}

    {howSteps.map((_, idx) => (
  1. {t.rich(`howItWorks.steps.${idx}`, { strong, code, em })}
  2. ))}

{t("howItWorks.notesIntro")}

    {howNotes.map((_, idx) => (
  • {t.rich(`howItWorks.notes.${idx}`, { strong })}
  • ))}

{t("enabling.heading")}

{t.rich("enabling.intro", { em })}

{t("enabling.collapsedAlt")}
{t("enabling.collapsedCaption")}
{t("enabling.panelAlt")}
{t("enabling.panelCaption")}

{t.rich("enabling.outro", { em })}

{t("context.heading")}

{t("context.intro")}

{contextRows.map((row, idx) => ( ))}
{t("context.headerBlock")} {t("context.headerWhen")} {t("context.headerWhat")}
{row.block} {t.rich(`context.rows.${idx}.when`, { code })} {t.rich(`context.rows.${idx}.what`, { code })}

{t("context.afterBlocks")}

{t("context.calloutBody")}

{t("tokens.heading")}

{t.rich("tokens.intro1", { em })}

{t("tokens.intro2")}

    {tokensItems.map((_, idx) => (
  • {t.rich(`tokens.items.${idx}`, { strong, em, code })}
  • ))}

{t.rich("tokens.capsIntro", { code })}

{tokensCapRows.map((row, idx) => ( ))}
{t("tokens.headerLevel")} {t("tokens.headerCap")} {t("tokens.headerConsumption")}
{row.level} {row.cap} {row.consumption}

{t("tokens.customNote")}

{t.rich("tokens.sizingBody", { code, link: detailLink })}

{t("providers.heading")}

{t("providers.intro")}

{t("providers.imageAlt")}
{t("providers.imageCaption")}

{t("providers.groq.heading")}

{t("providers.groq.tagline")}

    {groqItems.map((_, idx) => (
  • {t.rich(`providers.groq.items.${idx}`, { code, strong, a: providerLink("https://console.groq.com/keys") })}
  • ))}

{t("providers.openai.heading")}

{t("providers.openai.tagline")}

    {openaiItems.map((_, idx) => (
  • {t.rich(`providers.openai.items.${idx}`, { code, strong, a: providerLink("https://platform.openai.com/api-keys") })}
  • ))}
{t.rich("providers.openai.baseUrlBody", { em, strong, code })}

{t("providers.anthropic.heading")}

{t("providers.anthropic.tagline")}

    {anthropicItems.map((_, idx) => (
  • {t.rich(`providers.anthropic.items.${idx}`, { code, strong, a: providerLink("https://console.anthropic.com/settings/keys") })}
  • ))}

{t("providers.gemini.heading")}

{t("providers.gemini.tagline")}

    {geminiItems.map((_, idx) => (
  • {t.rich(`providers.gemini.items.${idx}`, { code, strong, a: providerLink("https://aistudio.google.com/app/apikey") })}
  • ))}

{t("providers.openrouter.heading")}

{t("providers.openrouter.tagline")}

    {openrouterItems.map((_, idx) => (
  • {t.rich(`providers.openrouter.items.${idx}`, { code, strong, a: providerLink("https://openrouter.ai/keys") })}
  • ))}

{t("providers.ollama.heading")}

{t("providers.ollama.tagline")}

    {ollamaItems.map((_, idx) => (
  • {t.rich(`providers.ollama.items.${idx}`, { code, strong, em, a: providerLink("https://ollama.com/download") })}
  • ))}

{t("models.heading")}

{t.rich("models.intro", { code })}

{t("models.consequencesIntro")}

    {modelsConsequences.map((_, idx) => (
  • {t.rich(`models.consequences.${idx}`, { strong })}
  • ))}
{t("models.ollamaBody")}

{t("defaultPrompt.heading")}

{t.rich("defaultPrompt.intro", { em, code })}

{t("defaultPrompt.showFullSummary")}
{DEFAULT_SYSTEM_PROMPT}
          

{t.rich("defaultPrompt.passagesIntro", { em })}

    {defaultPassages.map((_, idx) => (
  • {t.rich(`defaultPrompt.passages.${idx}`, { strong })}
  • ))}

{t.rich("defaultPrompt.suggestionsPlaceholder", { code })}

{t("defaultPrompt.showAddonSummary")}
{SUGGESTIONS_ADDON}
          

{t("customPrompt.heading")}

{t.rich("customPrompt.intro", { em })}

{t("customPrompt.imageAlt")}
{t.rich("customPrompt.imageCaption", { em })}

{t("customPrompt.changesTitle")}

    {customChanges.map((_, idx) => (
  • {t.rich(`customPrompt.changes.${idx}`, { strong, em, code })}
  • ))}

{t("customPrompt.starterTitle")}

{t.rich("customPrompt.starterIntro", { em })}

{t("customPrompt.showStarterSummary")}
{EXAMPLE_CUSTOM_PROMPT}
          

{t("customPrompt.shareTitle")}

{t.rich("customPrompt.shareIntro", { em, code })}

{t("customPrompt.shareOutro")}

{t("suggestions.heading")}

{t.rich("suggestions.intro", { strong, em })}

{t("suggestions.formatIntro")}

{t("suggestions.rulesIntro")}

    {suggestionsRules.map((_, idx) => (
  • {t.rich(`suggestions.rules.${idx}`, { em })}
  • ))}
{t("suggestions.betaBody")}

{t("detailLevel.heading")}

{t("detailLevel.intro")}

{detailLevelRows.map((row, idx) => ( ))}
{t("detailLevel.headerLevel")} {t("detailLevel.headerLabel")} {t("detailLevel.headerCap")} {t("detailLevel.headerProduce")}
{row.level} {row.label} {row.cap} {row.produce}

{t("detailLevel.defaultsIntro")}

    {detailLevelDefaults.map((_, idx) => (
  • {t.rich(`detailLevel.defaults.${idx}`, { strong, code })}
  • ))}
{t.rich("detailLevel.emailBody", { code })}

{t("language.heading")}

{t.rich("language.intro", { code, em })}

{t.rich("language.list", { code })}

{t("language.rulesIntro")}

    {languageRules.map((_, idx) => (
  • {t.rich(`language.rules.${idx}`, { strong, code })}
  • ))}

{t.rich("language.customNote", { strong })}

{t("templates.heading")}

{t.rich("templates.body1", { code })}

{t.rich("templates.body2", { link: notifLink })}

{t("privacy.heading")}

{t("privacy.intro")}

{privacyRows.map((row, idx) => ( ))}
{t("privacy.headerProvider")} {t("privacy.headerDestination")}
{row.provider} {t.rich(`privacy.rows.${idx}.destination`, { code })}
{t("privacy.calloutBody")}

{t("whereNext.heading")}

) }