/* ============================================================ MMaxAI Hosting Panel — AI chat overlay ------------------------------------------------------------ Floating overlay that opens from either the dedicated "Ask AI" button in the topbar or the inline cmd-bar Enter-press. Layout uses the same dark Copilot tokens already defined in copilot.css, so it picks up theme changes automatically. ============================================================ */ /* AI trigger button (topbar) — sits between Cmd+K and the language toggle. Mirrors the .search-trigger shape. */ .ai-trigger { display: inline-flex; align-items: center; gap: 10px; width: auto; height: 36px; padding: 0 14px; border-radius: var(--radius-pill); background: var(--surface); border: 1px solid var(--border); color: var(--text-faint); font-size: 13px; cursor: pointer; transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); text-align: left; } .ai-trigger:hover { background: var(--surface-hover); color: var(--text); border-color: var(--border-strong); } .ai-trigger svg { width: 14px; height: 14px; flex: 0 0 auto; color: var(--accent-2); } .ai-trigger .label { flex: 1; } .ai-trigger kbd { padding: 2px 6px; border-radius: 4px; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted); font-family: var(--font); font-size: 11px; font-weight: 600; } /* AI trigger — a touch more eye-catching so people actually notice it. Slightly stronger border, accent-tinted icon, and a soft glow on hover. */ .ai-trigger-primary { background: linear-gradient(180deg, rgba(124,108,255,0.10), rgba(124,108,255,0.04)); border-color: rgba(124,108,255,0.45); color: var(--text); } .ai-trigger-primary:hover { background: linear-gradient(180deg, rgba(124,108,255,0.18), rgba(124,108,255,0.06)); border-color: rgba(124,108,255,0.75); box-shadow: 0 0 0 3px rgba(124,108,255,0.12); } .ai-trigger-primary svg { color: var(--accent); } /* Hide the AI trigger when no API key is set. The JS toggles ``[data-ai-disabled]`` on based on /api/ai/status. */ body[data-ai-disabled="true"] .ai-trigger { display: none; } /* ---- Overlay shell ---- */ .ai-overlay { position: fixed; inset: 0; z-index: 200; display: none; align-items: center; justify-content: center; pointer-events: none; } .ai-overlay[data-open="true"] { display: flex; pointer-events: auto; animation: ai-fade 140ms var(--ease); } @keyframes ai-fade { from { opacity: 0; } to { opacity: 1; } } .ai-backdrop { position: absolute; inset: 0; background: rgba(4, 6, 14, 0.55); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } .ai-panel { position: relative; width: min(720px, calc(100% - 32px)); height: min(720px, calc(100vh - 80px)); background: var(--bg-elev-1); border: 1px solid var(--border-strong); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg), 0 0 0 1px rgba(255,255,255,0.03); display: flex; flex-direction: column; overflow: hidden; animation: ai-pop 180ms var(--ease); } @keyframes ai-pop { from { transform: translateY(8px) scale(0.98); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } @media (max-width: 720px) { .ai-panel { height: calc(100vh - 24px); border-radius: var(--radius); } } /* ---- Header ---- */ .ai-head { display: flex; align-items: center; gap: 12px; padding: 14px 18px; border-bottom: 1px solid var(--border); background: rgba(10, 14, 28, 0.4); } .ai-head-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; } .ai-spark { color: var(--accent-2); flex: 0 0 auto; } .ai-title { font-size: 15px; font-weight: 600; color: var(--text); letter-spacing: -0.005em; } .ai-sub { margin-top: 1px; font-size: 11.5px; color: var(--text-muted); } .ai-head-right { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; } .ai-model-tag { font-size: 11px; padding: 3px 9px; border-radius: var(--radius-pill); background: var(--surface-2); color: var(--text-muted); border: 1px solid var(--border); font-family: var(--font-mono); margin-right: 4px; } .ai-icon { width: 32px; height: 32px; border-radius: 8px; background: var(--surface); border: 1px solid var(--border); color: var(--text-muted); display: grid; place-items: center; cursor: pointer; transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease); } .ai-icon:hover { background: var(--surface-hover); color: var(--text); } /* ---- Transcript scroll ---- */ .ai-transcript { flex: 1; overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.18) transparent; /* Reserve room for the scrollbar so messages don't get a 1-frame jump when content overflows vs when it doesn't. */ scrollbar-gutter: stable; } /* WebKit (Chrome / Edge / Safari) — slim, themed scrollbar */ .ai-transcript::-webkit-scrollbar { width: 8px; height: 8px; } .ai-transcript::-webkit-scrollbar-track { background: transparent; } .ai-transcript::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.14); border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; } .ai-transcript::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.24); background-clip: padding-box; border: 2px solid transparent; } /* ---- Message bubbles ---- */ .ai-msg { display: flex; width: 100%; animation: ai-bubble-in 160ms var(--ease); } @keyframes ai-bubble-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } .ai-msg-user { justify-content: flex-end; } .ai-msg-assistant { justify-content: flex-start; } .ai-bubble { max-width: 86%; padding: 10px 14px; border-radius: 14px; line-height: 1.5; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: anywhere; } .ai-msg-user .ai-bubble { background: var(--surface-2); color: var(--text); border-bottom-right-radius: 4px; } .ai-msg-assistant .ai-bubble { background: var(--surface); color: var(--text); border: 1px solid var(--border); border-bottom-left-radius: 4px; } .ai-msg-error .ai-bubble { background: rgba(248, 113, 113, 0.10); color: var(--status-error); border: 1px solid rgba(248, 113, 113, 0.32); } /* Markdown-ish styling inside assistant bubbles. Light touch — the upstream is instructed to keep replies short, so we don't need a full markdown renderer. */ .ai-bubble code { background: var(--bg); padding: 1px 6px; border-radius: 5px; font-family: var(--font-mono); font-size: 12.5px; color: var(--accent-2); } .ai-bubble pre { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55; } .ai-bubble pre code { background: transparent; padding: 0; color: var(--text); } .ai-bubble strong { font-weight: 600; } .ai-bubble em { font-style: italic; color: var(--text-muted); } .ai-msg-meta { margin-top: 4px; font-size: 11px; color: var(--text-faint); } /* Quick-reply chips on the empty-state bubble. */ .ai-hint { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; } .ai-quick { padding: 5px 10px; font-size: 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-pill); color: var(--text-muted); cursor: pointer; transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); } .ai-quick:hover { background: var(--surface); color: var(--text); border-color: var(--border-strong); } /* Typing indicator — three pulsing dots. */ .ai-typing { display: inline-flex; gap: 4px; align-items: center; height: 1em; } .ai-typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); animation: ai-bounce 1.2s infinite ease-in-out; } .ai-typing span:nth-child(2) { animation-delay: 0.15s; } .ai-typing span:nth-child(3) { animation-delay: 0.3s; } @keyframes ai-bounce { 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } } /* ---- Input form ---- */ .ai-form { display: flex; gap: 10px; align-items: flex-end; padding: 14px 16px; border-top: 1px solid var(--border); background: rgba(10, 14, 28, 0.4); } .ai-form textarea { flex: 1; resize: none; min-height: 44px; max-height: 200px; line-height: 1.45; padding: 12px 14px; border-radius: 12px; background: var(--bg); border: 1px solid var(--border); color: var(--text); font: inherit; font-size: 14px; outline: none; transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease); } .ai-form textarea:focus { border-color: var(--border-strong); box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.18); } .ai-form textarea::placeholder { color: var(--text-faint); } .ai-send { width: 44px; height: 44px; border-radius: 12px; background: var(--text); color: var(--bg); display: grid; place-items: center; border: 0; cursor: pointer; transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); flex: 0 0 auto; } .ai-send:hover { opacity: 0.9; } .ai-send:disabled { opacity: 0.4; cursor: not-allowed; } .ai-send svg { width: 14px; height: 14px; } .ai-send.is-streaming { background: var(--status-error); color: #1f0405; } /* Disabled state — overlay still renders the empty hint but the input doesn't accept content. Toggled by JS when /api/ai/status reports `enabled: false`. */ body[data-ai-disabled="true"] .ai-form textarea { display: none; } body[data-ai-disabled="true"] .ai-send { display: none; } body[data-ai-disabled="true"] .ai-form { padding: 18px; justify-content: center; color: var(--text-muted); font-size: 13px; } body[data-ai-disabled="true"] .ai-form::before { content: "AI assistant is not configured on this panel. Set NVIDIA_API_KEY in env."; } /* Empty-state message is hidden once there's any conversation. */ .ai-transcript[data-has-messages="true"] [data-empty] { display: none; } /* Mobile stacking — chat bubble spans more of the width on small screens, signature stays legible. */ @media (max-width: 540px) { .ai-bubble { max-width: 95%; } .ai-form { padding: 10px; } .ai-form textarea { font-size: 15px; } } /* ---- Cmd-bar → AI button trigger (topbar) is hidden on mobile to leave room for the Cmd+K shortcut. ---- */ @media (max-width: 720px) { .ai-trigger .label { display: none; } }