// ── Meridian AI Engine ───────────────────────────────────────────────────── // Single shared AI backed by an AMD vLLM Endpoint // Each view passes a `role` so the system prompt is context-aware. // // La URL del endpoint se configura en Settings → AI & Integrations // y se persiste en localStorage bajo la clave 'meridian-amd-endpoint'. const AMD_MODEL = "llama-3.3-70b-versatile"; const LS_AMD_ENDPOINT = 'meridian-amd-endpoint'; const getAmdUrl = () => (localStorage.getItem(LS_AMD_ENDPOINT) || '').trim(); const setAmdUrl = (url) => localStorage.setItem(LS_AMD_ENDPOINT, url.trim()); // Exponer globalmente para que todos los views puedan acceder window.getAmdUrl = getAmdUrl; window.setAmdUrl = setAmdUrl; // ── System prompts per role ──────────────────────────────────────────────── const ROLE_PROMPTS = { general: `You are VaultMind AI, the intelligent assistant embedded in Meridian — a modern project management platform for engineering teams. You have awareness of the team's issues, PRs, sprints, roadmap, docs, and compute jobs. Be concise, insightful, and proactive. Respond in plain text or markdown.`, code: `You are the Meridian Code Agent embedded in the Code Editor. Your job is to help users create, edit, and run web files (HTML, CSS, JavaScript). When asked to create a file, output only clean, complete, functional code. Always suggest running the file after creating it. Mention the run command in terminal syntax (e.g. "open index.html" for HTML, "node app.js" for JS). Keep explanations short — the user wants working code fast.`, pr: `You are the Meridian PR Analyst. You analyze pull requests for engineering teams. When given PR data, evaluate: - Merge readiness (checks, reviewers, conflicts) - Risk level (High / Medium / Low) based on size, scope, and affected areas - Suggested reviewers or missing context - Potential issues: missing tests, breaking changes, security concerns - Concrete action items for the author Format your response with clear sections. Be direct — teams are busy.`, sprint: `You are the Meridian Sprint Coach. You analyze sprint health for engineering teams. When given sprint data (issues, burndown, velocity), provide: - Sprint health score and trend - At-risk items that may not complete - Blocking issues and suggested unblocking actions - Retrospective themes and talking points - Velocity comparison with previous sprints Be concise and actionable. Teams read this during standups.`, issues: `You are the Meridian Issue Triage Assistant. You help engineering teams manage their backlog. When given a list of issues, identify: - Duplicates or related issues that should be linked - Priority mismatches (high priority with no assignee, etc.) - Issues blocking other issues - Stale issues that need attention - Recommended sprint candidates Output a structured triage report.`, roadmap: `You are the Meridian Roadmap Analyst. You analyze quarterly roadmaps for engineering teams. When given roadmap items and their progress, provide: - Delivery confidence per milestone (%) - Items at risk of slipping to next quarter - Dependency bottlenecks - Suggested reprioritization - Executive summary paragraph Be direct. Skip the caveats.`, team: `You are the Meridian Team Intelligence assistant. You analyze team workload and capacity. When given team data, provide: - Overloaded team members (load > 80%) - Under-allocated capacity available for new work - Suggested task reassignments for balance - Who to pull in for specific tasks based on skills/current load - Team health observations Keep it practical and respectful.`, docs: `You are the Meridian Docs Writer. You help engineering teams write, summarize, and improve technical documentation. You can: - Summarize long documents into bullet points - Expand brief notes into full docs - Improve clarity and structure of existing content - Generate ADRs (Architecture Decision Records) from descriptions - Write meeting notes, retrospectives, and engineering specs Match the tone of the existing content.`, compute: `You are the Meridian Compute Analyst. You analyze GPU compute jobs and infrastructure usage. When given job and quota data, provide: - Current spend efficiency analysis - Jobs that can be optimized (region, GPU tier, duration) - Spot instance opportunities for cost savings - Queue wait time predictions - Recommendations for GPU selection based on workload type Output cost estimates where possible.`, }; // ── Core call function ───────────────────────────────────────────────────── async function amdChat({ role = "general", messages, contextData = null, onChunk = null }) { const url = getAmdUrl(); if (!url) throw new Error("AMD_URL_NOT_SET"); const systemPrompt = ROLE_PROMPTS[role] || ROLE_PROMPTS.general; const contextBlock = contextData ? `\n\n--- LIVE WORKSPACE DATA ---\n${JSON.stringify(contextData, null, 2)}\n--- END DATA ---` : ""; const body = { model: AMD_MODEL, max_tokens: 1024, stream: !!onChunk, messages: [ { role: "system", content: systemPrompt + contextBlock }, ...messages, ], }; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer dummy-key`, }, body: JSON.stringify(body), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || `AMD API HTTP ${res.status}`); } // Streaming if (onChunk) { const reader = res.body.getReader(); const decoder = new TextDecoder(); let fullText = ""; let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop(); // keep the last incomplete line in the buffer for (const line of lines) { const l = line.trim(); if (l.startsWith("data: ") && l !== "data: [DONE]") { try { const data = JSON.parse(l.slice(6)); let delta = data.choices?.[0]?.delta?.content || ""; if (delta) { // Fix tokenizer artifacts for some models delta = delta.replace(/Ġ/g, ' ').replace(/Ċ/g, '\n').replace(/ď/g, "'").replace(/č/g, "c"); fullText += delta; onChunk(delta, fullText); } } catch (_) { } } } } return fullText; } // Non-streaming const data = await res.json(); let text = data.choices?.[0]?.message?.content || ""; text = text.replace(/Ġ/g, ' ').replace(/Ċ/g, '\n'); return text; } // ── Floating AI Panel ────────────────────────────────────────────────────── const AIPanelContext = React.createContext(null); const AIPanelProvider = ({ children }) => { const [open, setOpen] = React.useState(false); const [role, setRole] = React.useState("general"); const [ctxData, setCtxData] = React.useState(null); const [messages, setMessages] = React.useState([]); const [loading, setLoading] = React.useState(false); const [streamText, setStreamText] = React.useState(""); const scrollRef = React.useRef(null); const openAI = React.useCallback((initialPrompt, aiRole = "general", data = null) => { setRole(aiRole); setCtxData(data); setOpen(true); if (initialPrompt) { // fire off immediately setTimeout(() => sendAI(initialPrompt, aiRole, data, []), 80); } }, []); React.useEffect(() => { window.openAI = openAI; }, [openAI]); React.useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [messages, streamText]); const sendAI = async (text, overrideRole, overrideData, prevMessages) => { const r = overrideRole ?? role; const ctx = overrideData ?? ctxData; const hist = prevMessages ?? messages; if (!text.trim() || loading) return; setLoading(true); setStreamText(""); const userMsg = { role: "user", content: text }; const newHist = [...hist, userMsg]; setMessages(newHist); try { let accumulated = ""; await amdChat({ role: r, messages: newHist, contextData: ctx, onChunk: (delta, full) => { accumulated = full; setStreamText(full); }, }); setMessages(m => [...m, { role: "assistant", content: accumulated }]); setStreamText(""); } catch (e) { const msg = `⚠ API Error: ${e?.message || String(e)}`; setMessages(m => [...m, { role: "assistant", content: msg }]); setStreamText(""); } finally { setLoading(false); } }; const handleSend = (e) => { e.preventDefault(); const el = document.getElementById("ai-panel-input"); if (!el) return; const text = el.value.trim(); if (!text) return; el.value = ""; sendAI(text); }; const ROLE_LABELS = { general: "VaultMind AI", code: "Code Agent", pr: "PR Analyst", sprint: "Sprint Coach", issues: "Issue Triage", roadmap: "Roadmap Analyst", team: "Team Intel", docs: "Docs Writer", compute: "Compute Analyst", }; const renderMd = (text) => { if (!text) return ""; // Simple markdown: bold, code, line breaks return text .replace(/&/g, "&").replace(//g, ">") .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/`([^`]+)`/g, "$1") .replace(/\n/g, "
"); }; if (!open) return ( {children} ); return ( {children} {/* Backdrop */}
setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 299, background: "oklch(0 0 0 / 0.3)", backdropFilter: "blur(2px)", }} /> {/* Panel */}
{/* Header */}
{ROLE_LABELS[role] || "AI"}
{role} mode
))}
{/* Messages */}
{messages.length === 0 && !loading && (
Ask me anything about your {role === "general" ? "workspace" : role}.
{[ role === "pr" && "Analyze all open PRs", role === "sprint" && "Summarize sprint 42 health", role === "issues" && "Which issues are blocking the sprint?", role === "team" && "Who has capacity for a new task?", role === "code" && "Create a landing page with a hero section", role === "compute" && "How can I reduce my compute costs?", role === "general" && "Give me today's standup brief", ].filter(Boolean).map((s, i) => ( ))}
)} {messages.map((m, i) => (
{m.role === "user" ? : }
))} {/* Streaming */} {loading && (
{streamText ? : }
)}
{/* Input */}
); }; window.amdChat = amdChat; window.AIPanelProvider = AIPanelProvider;