/** * — AI-powered community builder embedded in the rSpace header. * * Rich builder panel with interchangeable AI backend, model selector, * action confirmation, scaffold progress, and role-based UI adaptation. */ import { getAccessToken } from "./rstack-identity"; import { parseMiActions, summariseActions, isDestructiveAction, detailedActionSummary } from "../../lib/mi-actions"; import type { MiAction } from "../../lib/mi-actions"; import { MiActionExecutor } from "../../lib/mi-action-executor"; import { suggestTools, type ToolHint } from "../../lib/mi-tool-schema"; import { SpeechDictation } from "../../lib/speech-dictation"; interface MiMessage { role: "user" | "assistant"; content: string; actionSummary?: string; actionDetails?: string[]; toolHints?: ToolHint[]; } interface MiModelConfig { id: string; provider: string; providerModel: string; label: string; group: string; } export class RStackMi extends HTMLElement { #shadow: ShadowRoot; #messages: MiMessage[] = []; #abortController: AbortController | null = null; #dictation: SpeechDictation | null = null; #interimText = ""; #preferredModel: string = ""; #minimized = false; #availableModels: MiModelConfig[] = []; #pendingConfirm: { actions: MiAction[]; resolve: (ok: boolean) => void } | null = null; #scaffoldProgress: { current: number; total: number; label: string } | null = null; constructor() { super(); this.#shadow = this.attachShadow({ mode: "open" }); this.#preferredModel = localStorage.getItem("mi-preferred-model") || ""; } connectedCallback() { this.#render(); this.#loadModels(); this.#setupKeyboard(); } disconnectedCallback() { document.removeEventListener("keydown", this.#keyHandler); } #keyHandler = (e: KeyboardEvent) => { // Cmd/Ctrl+K opens MI panel if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); const panel = this.#shadow.getElementById("mi-panel"); const bar = this.#shadow.getElementById("mi-bar"); const pill = this.#shadow.getElementById("mi-pill"); if (this.#minimized) { this.#minimized = false; panel?.classList.add("open"); panel?.classList.remove("hidden"); bar?.classList.add("focused"); pill?.classList.remove("visible"); } else { panel?.classList.add("open"); bar?.classList.add("focused"); } const input = this.#shadow.getElementById("mi-input") as HTMLTextAreaElement | null; input?.focus(); } }; #setupKeyboard() { document.addEventListener("keydown", this.#keyHandler); } async #loadModels() { try { const res = await fetch("/api/mi/models"); if (res.ok) { const data = await res.json(); this.#availableModels = data.models || []; if (!this.#preferredModel && data.default) { this.#preferredModel = data.default; } this.#renderModelSelector(); } } catch { /* offline — use whatever's there */ } } #renderModelSelector() { const select = this.#shadow.getElementById("mi-model-select") as HTMLSelectElement | null; if (!select || !this.#availableModels.length) return; // Group models const groups = new Map(); for (const m of this.#availableModels) { const group = groups.get(m.group) || []; group.push(m); groups.set(m.group, group); } select.innerHTML = ""; for (const [group, models] of groups) { const optgroup = document.createElement("optgroup"); optgroup.label = group; for (const m of models) { const opt = document.createElement("option"); opt.value = m.id; opt.textContent = m.label; if (m.id === this.#preferredModel) opt.selected = true; optgroup.appendChild(opt); } select.appendChild(optgroup); } select.addEventListener("change", () => { this.#preferredModel = select.value; localStorage.setItem("mi-preferred-model", select.value); }); } #render() { this.#shadow.innerHTML = `
${SpeechDictation.isSupported() ? '' : ''}
mi

Hi, I'm mi — your mycelial intelligence guide.

I can create content across all rApps, set up spaces, connect knowledge, and help you build.

mi
`; const barInput = this.#shadow.getElementById("mi-bar-input") as HTMLInputElement; const input = this.#shadow.getElementById("mi-input") as HTMLTextAreaElement; const panel = this.#shadow.getElementById("mi-panel")!; const bar = this.#shadow.getElementById("mi-bar")!; const pill = this.#shadow.getElementById("mi-pill")!; const sendBtn = this.#shadow.getElementById("mi-send")!; // Bar input opens the panel barInput.addEventListener("focus", () => { panel.classList.add("open"); bar.classList.add("focused"); // Transfer any text to the panel input if (barInput.value.trim()) { input.value = barInput.value; barInput.value = ""; } setTimeout(() => input.focus(), 50); }); barInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && barInput.value.trim()) { e.preventDefault(); panel.classList.add("open"); bar.classList.add("focused"); input.value = barInput.value; barInput.value = ""; setTimeout(() => { input.focus(); this.#ask(input.value.trim()); input.value = ""; this.#autoResize(input); }, 50); } }); // Panel textarea: Enter sends, Shift+Enter for newline input.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey && input.value.trim()) { e.preventDefault(); this.#ask(input.value.trim()); input.value = ""; this.#autoResize(input); } if (e.key === "Escape") { this.#minimize(); } }); input.addEventListener("input", () => this.#autoResize(input)); sendBtn.addEventListener("click", () => { if (input.value.trim()) { this.#ask(input.value.trim()); input.value = ""; this.#autoResize(input); } }); // Close panel on outside click document.addEventListener("click", (e) => { if (!this.contains(e.target as Node) && !pill.contains(e.target as Node)) { panel.classList.remove("open"); bar.classList.remove("focused"); } }); // Prevent internal clicks from closing panel.addEventListener("click", (e) => e.stopPropagation()); bar.addEventListener("click", (e) => e.stopPropagation()); // Minimize button this.#shadow.getElementById("mi-minimize")!.addEventListener("click", () => this.#minimize()); // Close button this.#shadow.getElementById("mi-close")!.addEventListener("click", () => { panel.classList.remove("open"); bar.classList.remove("focused"); }); // Pill click restores pill.addEventListener("click", (e) => { e.stopPropagation(); this.#minimized = false; pill.classList.remove("visible"); panel.classList.add("open"); panel.classList.remove("hidden"); bar.classList.add("focused"); input.focus(); }); // Confirmation buttons this.#shadow.getElementById("mi-confirm-allow")!.addEventListener("click", () => { if (this.#pendingConfirm) { this.#pendingConfirm.resolve(true); this.#pendingConfirm = null; this.#shadow.getElementById("mi-confirm")!.style.display = "none"; } }); this.#shadow.getElementById("mi-confirm-cancel")!.addEventListener("click", () => { if (this.#pendingConfirm) { this.#pendingConfirm.resolve(false); this.#pendingConfirm = null; this.#shadow.getElementById("mi-confirm")!.style.display = "none"; } }); // Voice dictation const micBtn = this.#shadow.getElementById("mi-mic") as HTMLButtonElement | null; if (micBtn) { let baseText = ""; this.#dictation = new SpeechDictation({ onInterim: (text) => { this.#interimText = text; barInput.value = baseText + (baseText ? " " : "") + text; }, onFinal: (text) => { this.#interimText = ""; baseText += (baseText ? " " : "") + text; barInput.value = baseText; }, onStateChange: (recording) => { micBtn.classList.toggle("recording", recording); if (!recording) { baseText = barInput.value; this.#interimText = ""; } }, onError: (err) => console.warn("MI dictation:", err), }); micBtn.addEventListener("click", (e) => { e.stopPropagation(); if (!this.#dictation!.isRecording) { baseText = barInput.value; } this.#dictation!.toggle(); barInput.focus(); }); } } #minimize() { this.#minimized = true; const panel = this.#shadow.getElementById("mi-panel")!; const pill = this.#shadow.getElementById("mi-pill")!; panel.classList.remove("open"); panel.classList.add("hidden"); this.#shadow.getElementById("mi-bar")!.classList.remove("focused"); pill.classList.add("visible"); } #autoResize(textarea: HTMLTextAreaElement) { textarea.style.height = "auto"; textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px"; } /** Gather page context: open shapes, active module, tabs, canvas state. */ #gatherContext(): Record { const ctx: Record = {}; ctx.space = document.querySelector("rstack-space-switcher")?.getAttribute("current") || ""; ctx.module = document.querySelector("rstack-app-switcher")?.getAttribute("current") || ""; // Deep canvas context from MI bridge const bridge = (window as any).__miCanvasBridge; if (bridge) { const cc = bridge.getCanvasContext(); ctx.openShapes = cc.allShapes.slice(0, 20).map((s: any) => ({ type: s.type, id: s.id, x: Math.round(s.x), y: Math.round(s.y), width: Math.round(s.width), height: Math.round(s.height), ...(s.content ? { snippet: s.content.slice(0, 80) } : {}), ...(s.title ? { title: s.title } : {}), })); if (cc.selectedShapes.length) { ctx.selectedShapes = cc.selectedShapes.map((s: any) => ({ type: s.type, id: s.id, x: Math.round(s.x), y: Math.round(s.y), ...(s.content ? { snippet: s.content.slice(0, 80) } : {}), ...(s.title ? { title: s.title } : {}), })); } if (cc.connections.length) ctx.connections = cc.connections; ctx.viewport = cc.viewport; if (cc.shapeGroups.length) ctx.shapeGroups = cc.shapeGroups; ctx.shapeCountByType = cc.shapeCountByType; } else { const canvasContent = document.getElementById("canvas-content"); if (canvasContent) { const shapes = [...canvasContent.children] .filter((el) => el.tagName?.includes("-") && el.id) .map((el: any) => ({ type: el.tagName.toLowerCase(), id: el.id, ...(el.content ? { snippet: el.content.slice(0, 60) } : {}), ...(el.title ? { title: el.title } : {}), })) .slice(0, 20); if (shapes.length) ctx.openShapes = shapes; } } const tabBar = document.querySelector("rstack-tab-bar"); if (tabBar) { ctx.activeTab = tabBar.getAttribute("active") || ""; } ctx.pageTitle = document.title; return ctx; } async #ask(query: string) { const panel = this.#shadow.getElementById("mi-panel")!; const messagesEl = this.#shadow.getElementById("mi-messages")!; panel.classList.add("open"); this.#shadow.getElementById("mi-bar")!.classList.add("focused"); // Add user message this.#messages.push({ role: "user", content: query }); this.#renderMessages(messagesEl); // Add placeholder for assistant this.#messages.push({ role: "assistant", content: "" }); const assistantIdx = this.#messages.length - 1; this.#renderMessages(messagesEl); // Abort previous this.#abortController?.abort(); this.#abortController = new AbortController(); try { const token = getAccessToken(); const context = this.#gatherContext(); const res = await fetch("/api/mi/ask", { method: "POST", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ query, messages: this.#messages.slice(0, -1).slice(-10), space: context.space, module: context.module, context, model: this.#preferredModel || undefined, }), signal: this.#abortController.signal, }); if (!res.ok) { const err = await res.json().catch(() => ({ error: "Request failed" })); throw new Error(err.error || "Request failed"); } if (!res.body) throw new Error("No response stream"); const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); for (const line of chunk.split("\n").filter(Boolean)) { try { const data = JSON.parse(line); if (data.message?.content) { this.#messages[assistantIdx].content += data.message.content; } if (data.response) { this.#messages[assistantIdx].content = data.response; } } catch { /* skip malformed lines */ } } this.#renderMessages(messagesEl); } if (!this.#messages[assistantIdx].content) { this.#messages[assistantIdx].content = "I couldn't generate a response. Please try again."; this.#renderMessages(messagesEl); } else { // Parse and execute MI actions const rawText = this.#messages[assistantIdx].content; const { displayText, actions } = parseMiActions(rawText); this.#messages[assistantIdx].content = displayText; if (actions.length) { await this.#executeWithConfirmation(actions, context); this.#messages[assistantIdx].actionSummary = summariseActions(actions); this.#messages[assistantIdx].actionDetails = detailedActionSummary(actions); } // Check for tool suggestions const hints = suggestTools(query); if (hints.length) { this.#messages[assistantIdx].toolHints = hints; } this.#renderMessages(messagesEl); } } catch (e: any) { if (e.name !== "AbortError") { this.#messages[this.#messages.length - 1].content = "Sorry, I'm not available right now. Please try again later."; this.#renderMessages(messagesEl); } } } async #executeWithConfirmation(actions: MiAction[], context: Record) { const needsConfirm = actions.some(isDestructiveAction); if (needsConfirm) { const descriptions = actions .filter(isDestructiveAction) .map((a) => { switch (a.type) { case "delete-shape": return `Delete shape ${a.shapeId}`; case "delete-content": return `Delete ${a.contentType} from ${a.module}`; case "disable-module": return `Disable ${a.moduleId}`; case "batch": return `Batch with ${a.actions.length} actions`; default: return a.type; } }); const confirmEl = this.#shadow.getElementById("mi-confirm")!; const textEl = this.#shadow.getElementById("mi-confirm-text")!; textEl.textContent = `MI wants to: ${descriptions.join(", ")}`; confirmEl.style.display = "flex"; const allowed = await new Promise((resolve) => { this.#pendingConfirm = { actions, resolve }; }); if (!allowed) return; } // Validate permissions const token = getAccessToken(); try { const valRes = await fetch("/api/mi/validate-actions", { method: "POST", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ actions, space: context.space }), }); if (valRes.ok) { const { validated } = await valRes.json(); const blocked = validated.filter((v: any) => !v.allowed); if (blocked.length) { // Show blocked actions in the message const blockedMsg = blocked .map((v: any) => `🔒 "${v.action.type}" requires ${v.requiredRole} role`) .join("\n"); this.#messages.push({ role: "assistant", content: blockedMsg }); this.#renderMessages(this.#shadow.getElementById("mi-messages")!); // Only execute allowed actions const allowedActions = validated .filter((v: any) => v.allowed) .map((v: any) => v.action); if (!allowedActions.length) return; return this.#runActions(allowedActions, context); } } } catch { /* if validation fails, proceed anyway — module APIs have their own guards */ } await this.#runActions(actions, context); } async #runActions(actions: MiAction[], context: Record) { const executor = new MiActionExecutor(); const token = getAccessToken() || ""; executor.setContext(context.space || "", token); // Check if any actions need async execution const hasModuleActions = actions.some((a) => ["create-content", "update-content", "delete-content", "scaffold", "batch"].includes(a.type), ); const progressEl = this.#shadow.getElementById("mi-scaffold-progress")!; const fillEl = this.#shadow.getElementById("mi-scaffold-fill")!; const labelEl = this.#shadow.getElementById("mi-scaffold-label")!; const onProgress = (current: number, total: number, label: string) => { this.#scaffoldProgress = { current, total, label }; progressEl.style.display = "flex"; fillEl.style.width = `${(current / total) * 100}%`; labelEl.textContent = `${label} (${current}/${total})`; }; if (hasModuleActions) { await executor.executeAsync(actions, onProgress); } else { executor.execute(actions, onProgress); } // Hide progress progressEl.style.display = "none"; this.#scaffoldProgress = null; } #renderMessages(container: HTMLElement) { container.innerHTML = this.#messages .map( (m) => `
${m.role === "user" ? "You" : "✧ mi"}
${m.content ? this.#formatContent(m.content) : ''}
${m.actionSummary ? `
${this.#escapeHtml(m.actionSummary)}
${(m.actionDetails || []).map((d) => `
→ ${this.#escapeHtml(d)}
`).join("")}
` : ""} ${m.toolHints?.length ? `
${m.toolHints.map((h) => ``).join("")}
` : ""}
`, ) .join(""); // Wire tool chip clicks container.querySelectorAll(".mi-tool-chip").forEach((btn) => { btn.addEventListener("click", () => { const tag = btn.dataset.tag; if (!tag) return; const executor = new MiActionExecutor(); executor.execute([{ type: "create-shape", tagName: tag, props: {} }]); }); }); container.scrollTop = container.scrollHeight; } #escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">"); } #formatContent(s: string): string { let escaped = s .replace(/&/g, "&") .replace(//g, ">"); // Code blocks (```) escaped = escaped.replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
'); // Headers escaped = escaped.replace(/^### (.+)$/gm, '$1'); escaped = escaped.replace(/^## (.+)$/gm, '$1'); escaped = escaped.replace(/^# (.+)$/gm, '$1'); // Bold and inline code escaped = escaped.replace(/\*\*(.+?)\*\*/g, "$1"); escaped = escaped.replace(/`(.+?)`/g, '$1'); // Links escaped = escaped.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Lists (- item) escaped = escaped.replace(/^- (.+)$/gm, '• $1'); // Newlines escaped = escaped.replace(/\n/g, "
"); return escaped; } static define(tag = "rstack-mi") { if (!customElements.get(tag)) customElements.define(tag, RStackMi); } } const STYLES = ` :host { display: contents; } .mi { position: relative; flex: 1; max-width: 480px; min-width: 0; } /* ── Search bar in header ── */ .mi-bar { display: flex; align-items: center; gap: 8px; padding: 6px 14px; border-radius: 10px; transition: all 0.2s; background: var(--rs-btn-secondary-bg); } .mi-bar.focused { background: var(--rs-bg-hover); box-shadow: 0 0 0 1px rgba(6,182,212,0.3); } .mi-icon { font-size: 0.9rem; flex-shrink: 0; background: linear-gradient(135deg, #06b6d4, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .mi-input-bar { flex: 1; border: none; outline: none; background: none; font-size: 0.85rem; min-width: 0; font-family: inherit; color: var(--rs-text-primary); } .mi-input-bar::placeholder { color: var(--rs-text-muted); } .mi-mic-btn { background: none; border: none; cursor: pointer; padding: 2px 4px; font-size: 0.85rem; border-radius: 6px; transition: all 0.2s; flex-shrink: 0; line-height: 1; } .mi-mic-btn:hover { background: var(--rs-bg-hover); } .mi-mic-btn.recording { animation: micPulse 1.5s infinite; filter: saturate(2) brightness(1.1); } @keyframes micPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } } /* ── Rich panel ── */ .mi-panel { position: fixed; top: 56px; right: 16px; width: 520px; max-width: calc(100vw - 32px); height: 65vh; max-height: calc(100vh - 72px); min-height: 300px; border-radius: 14px; overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,0.3); display: none; z-index: 300; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); resize: vertical; } .mi-panel.open { display: flex; flex-direction: column; } .mi-panel.hidden { display: none; } /* ── Panel header ── */ .mi-panel-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--rs-border); flex-shrink: 0; } .mi-panel-icon { font-size: 1rem; background: linear-gradient(135deg, #06b6d4, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .mi-panel-title { font-weight: 700; font-size: 0.85rem; color: var(--rs-text-primary); } .mi-model-select { font-size: 0.75rem; padding: 3px 6px; border-radius: 6px; border: 1px solid var(--rs-border); background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary); font-family: inherit; cursor: pointer; max-width: 150px; } .mi-panel-spacer { flex: 1; } .mi-panel-btn { background: none; border: none; cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 2px 6px; border-radius: 4px; color: var(--rs-text-muted); transition: all 0.15s; } .mi-panel-btn:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); } /* ── Messages ── */ .mi-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; } .mi-welcome { text-align: center; padding: 24px 16px; } .mi-welcome-icon { font-size: 2rem; display: block; margin-bottom: 8px; background: linear-gradient(135deg, #06b6d4, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .mi-welcome p { font-size: 0.9rem; line-height: 1.5; margin: 0; color: var(--rs-text-primary); } .mi-welcome-sub { font-size: 0.8rem; opacity: 0.6; margin-top: 6px !important; } .mi-msg { display: flex; flex-direction: column; gap: 4px; } .mi-msg-who { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; } .mi-msg--user .mi-msg-who { color: #06b6d4; } .mi-msg--assistant .mi-msg-who { background: linear-gradient(135deg, #06b6d4, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .mi-msg-body { font-size: 0.85rem; line-height: 1.6; word-break: break-word; color: var(--rs-text-secondary); } .mi-msg--user .mi-msg-body { color: var(--rs-text-primary); } /* ── Markdown rendering ── */ .mi-code { padding: 1px 5px; border-radius: 4px; font-size: 0.8rem; font-family: 'SF Mono', Monaco, Consolas, monospace; background: var(--rs-btn-secondary-bg); color: #0ea5e9; } .mi-codeblock { background: var(--rs-btn-secondary-bg); border-radius: 8px; padding: 10px 12px; margin: 6px 0; overflow-x: auto; font-size: 0.78rem; line-height: 1.5; font-family: 'SF Mono', Monaco, Consolas, monospace; } .mi-codeblock code { color: var(--rs-text-primary); } .mi-h1 { font-size: 1.1rem; display: block; margin: 8px 0 4px; color: var(--rs-text-primary); } .mi-h2 { font-size: 1rem; display: block; margin: 6px 0 3px; color: var(--rs-text-primary); } .mi-h3 { font-size: 0.9rem; display: block; margin: 4px 0 2px; color: var(--rs-text-primary); } .mi-link { color: #06b6d4; text-decoration: underline; } .mi-list-item { display: block; padding-left: 4px; } /* ── Typing indicator ── */ .mi-typing { display: inline-flex; gap: 4px; padding: 4px 0; } .mi-typing span { width: 6px; height: 6px; border-radius: 50%; animation: miBounce 1.2s ease-in-out infinite; background: var(--rs-text-muted); } .mi-typing span:nth-child(2) { animation-delay: 0.15s; } .mi-typing span:nth-child(3) { animation-delay: 0.3s; } @keyframes miBounce { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-4px); } } /* ── Action chips (collapsible) ── */ .mi-action-details { margin-top: 6px; } .mi-action-chip { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; background: rgba(6,182,212,0.12); color: #06b6d4; cursor: pointer; list-style: none; } .mi-action-chip::marker { display: none; content: ''; } .mi-action-chip::-webkit-details-marker { display: none; } .mi-action-list { padding: 6px 0 0 6px; } .mi-action-item { font-size: 0.75rem; color: var(--rs-text-muted); padding: 1px 0; } .mi-tool-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .mi-tool-chip { padding: 4px 10px; border-radius: 8px; border: none; font-size: 0.75rem; cursor: pointer; transition: background 0.15s; font-family: inherit; background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary); } .mi-tool-chip:hover { background: var(--rs-bg-hover); } /* ── Confirmation bar ── */ .mi-confirm { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: rgba(234,179,8,0.1); border-top: 1px solid rgba(234,179,8,0.3); flex-shrink: 0; } .mi-confirm-icon { font-size: 1rem; } .mi-confirm-text { flex: 1; font-size: 0.8rem; color: var(--rs-text-primary); } .mi-confirm-btns { display: flex; gap: 6px; } .mi-confirm-allow { padding: 4px 12px; border-radius: 6px; border: none; cursor: pointer; font-size: 0.75rem; font-weight: 600; font-family: inherit; background: #06b6d4; color: white; } .mi-confirm-cancel { padding: 4px 12px; border-radius: 6px; border: none; cursor: pointer; font-size: 0.75rem; font-weight: 600; font-family: inherit; background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary); } /* ── Scaffold progress ── */ .mi-scaffold-progress { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-top: 1px solid var(--rs-border); flex-shrink: 0; } .mi-scaffold-bar { flex: 1; height: 4px; border-radius: 2px; background: var(--rs-btn-secondary-bg); overflow: hidden; } .mi-scaffold-fill { height: 100%; border-radius: 2px; transition: width 0.3s; background: linear-gradient(90deg, #06b6d4, #7c3aed); } .mi-scaffold-label { font-size: 0.75rem; color: var(--rs-text-muted); white-space: nowrap; } /* ── Input area ── */ .mi-input-area { display: flex; align-items: flex-end; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--rs-border); flex-shrink: 0; } .mi-input { flex: 1; border: none; outline: none; background: none; font-size: 0.85rem; min-width: 0; resize: none; font-family: inherit; color: var(--rs-text-primary); max-height: 120px; line-height: 1.4; } .mi-input::placeholder { color: var(--rs-text-muted); } .mi-send-btn { background: none; border: none; cursor: pointer; font-size: 0.9rem; padding: 4px 8px; border-radius: 6px; color: #06b6d4; transition: all 0.15s; flex-shrink: 0; } .mi-send-btn:hover { background: rgba(6,182,212,0.12); } /* ── Minimized pill ── */ .mi-pill { position: fixed; bottom: 16px; right: 16px; padding: 6px 14px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; cursor: pointer; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); box-shadow: 0 4px 16px rgba(0,0,0,0.2); z-index: 300; display: none; align-items: center; gap: 6px; color: var(--rs-text-primary); transition: all 0.2s; } .mi-pill:hover { box-shadow: 0 4px 20px rgba(6,182,212,0.3); } .mi-pill.visible { display: flex; } .mi-pill-icon { background: linear-gradient(135deg, #06b6d4, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } @media (max-width: 640px) { .mi { max-width: none; width: 100%; } .mi-panel { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; max-width: 100%; height: 100%; max-height: 100%; border-radius: 0; resize: none; } } `;