Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SoniCoder v2 β AI Code Agent</title> | |
| <meta name="description" content="AI-powered code agent with MCP support, multiple 1B models, and Gemini CLI patterns"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SONICODER V2 - DEEPSEEK-STYLE CLEAN UI | |
| Professional AI Code Agent Interface | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* βββ CSS VARIABLES - LIGHT THEME βββ */ | |
| :root { | |
| /* Core Colors */ | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f7f8fa; | |
| --bg-tertiary: #f0f1f3; | |
| --bg-elevated: #ffffff; | |
| --bg-hover: #eef0f3; | |
| /* Text Colors */ | |
| --text-primary: #1a1a1a; | |
| --text-secondary: #666666; | |
| --text-muted: #999999; | |
| --text-accent: #4d6bfe; | |
| /* Accent Color (DeepSeek-like blue) */ | |
| --accent: #4d6bfe; | |
| --accent-hover: #3b5ae6; | |
| --accent-light: rgba(77, 107, 254, 0.08); | |
| --accent-border: rgba(77, 107, 254, 0.2); | |
| /* Border Colors */ | |
| --border-light: #e5e5e5; | |
| --border-default: #d9d9d9; | |
| --border-focus: var(--accent); | |
| /* Status Colors */ | |
| --success: #22c55e; | |
| --success-bg: rgba(34, 197, 94, 0.1); | |
| --error: #ef4444; | |
| --error-bg: rgba(239, 68, 68, 0.1); | |
| --warning: #f59e0b; | |
| --warning-bg: rgba(245, 158, 11, 0.1); | |
| --info: #4d6bfe; | |
| --info-bg: rgba(77, 107, 254, 0.1); | |
| /* Message Bubble Colors */ | |
| --user-bubble: #4d6bfe; | |
| --user-bubble-text: #ffffff; | |
| --ai-bubble: #f0f0f0; | |
| --ai-bubble-text: #1a1a1a; | |
| /* Typography */ | |
| --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; | |
| --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| /* Spacing & Sizing */ | |
| --radius-sm: 6px; | |
| --radius-md: 10px; | |
| --radius-lg: 16px; | |
| --radius-xl: 24px; | |
| --header-height: 56px; | |
| --sidebar-width: 280px; | |
| /* Transitions */ | |
| --transition-fast: 150ms ease; | |
| --transition-base: 200ms ease; | |
| --transition-slow: 300ms ease; | |
| /* Shadows */ | |
| --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); | |
| --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); | |
| --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); | |
| /* Z-Index Scale */ | |
| --z-base: 1; | |
| --z-panel: 10; | |
| --z-overlay: 100; | |
| --z-modal: 1000; | |
| --z-toast: 2000; | |
| } | |
| /* Dark Theme Variables */ | |
| [data-theme="dark"] { | |
| --bg-primary: #1a1a1a; | |
| --bg-secondary: #202020; | |
| --bg-tertiary: #2a2a2a; | |
| --bg-elevated: #262626; | |
| --bg-hover: #333333; | |
| --text-primary: #f5f5f5; | |
| --text-secondary: #a0a0a0; | |
| --text-muted: #707070; | |
| --user-bubble: #4d6bfe; | |
| --ai-bubble: #2a2a2a; | |
| --ai-bubble-text: #f5f5f5; | |
| --border-light: #333333; | |
| --border-default: #404040; | |
| --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); | |
| --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.4); | |
| } | |
| /* βββ RESET & BASE βββ */ | |
| *, *::before, *::after { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| html { | |
| height: 100%; | |
| font-size: 14px; | |
| } | |
| body { | |
| height: 100%; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| font-family: var(--font-sans); | |
| line-height: 1.6; | |
| overflow: hidden; | |
| } | |
| ::selection { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border-default); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-muted); | |
| } | |
| /* βββ MAIN LAYOUT βββ */ | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| position: relative; | |
| z-index: var(--z-base); | |
| } | |
| /* βββ HEADER βββ */ | |
| .header { | |
| height: var(--header-height); | |
| min-height: var(--header-height); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border-light); | |
| position: relative; | |
| z-index: var(--z-panel); | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| cursor: pointer; | |
| transition: opacity var(--transition-fast); | |
| } | |
| .logo:hover { | |
| opacity: 0.85; | |
| } | |
| .logo-icon { | |
| width: 32px; | |
| height: 32px; | |
| background: linear-gradient(135deg, var(--accent), #6366f1); | |
| border-radius: var(--radius-md); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 16px; | |
| font-weight: 700; | |
| color: white; | |
| } | |
| .logo-text { | |
| display: flex; | |
| flex-direction: column; | |
| line-height: 1.2; | |
| } | |
| .logo-title { | |
| font-size: 15px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| letter-spacing: -0.3px; | |
| } | |
| .logo-version { | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| font-weight: 500; | |
| } | |
| .header-center { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| flex: 1; | |
| justify-content: center; | |
| max-width: 600px; | |
| margin: 0 20px; | |
| } | |
| .search-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| width: 100%; | |
| max-width: 400px; | |
| padding: 8px 14px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-xl); | |
| transition: all var(--transition-fast); | |
| } | |
| .search-bar:focus-within { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px var(--accent-light); | |
| } | |
| .search-bar input { | |
| flex: 1; | |
| border: none; | |
| outline: none; | |
| background: transparent; | |
| font-size: 13px; | |
| color: var(--text-primary); | |
| font-family: var(--font-sans); | |
| } | |
| .search-bar input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| .search-bar .search-icon { | |
| color: var(--text-muted); | |
| font-size: 14px; | |
| } | |
| .model-selector { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 7px 14px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-xl); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| white-space: nowrap; | |
| position: relative; | |
| } | |
| .model-selector:hover { | |
| border-color: var(--accent); | |
| background: var(--accent-light); | |
| } | |
| .model-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| transition: all var(--transition-base); | |
| } | |
| .model-dot.ready { | |
| background: var(--success); | |
| } | |
| .model-dot.loading { | |
| background: var(--warning); | |
| animation: spin 1s linear infinite; | |
| } | |
| .model-dot.error { | |
| background: var(--error); | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .model-text { | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| } | |
| .model-dropdown { | |
| position: absolute; | |
| top: calc(100% + 4px); | |
| left: 0; | |
| right: 0; | |
| background: var(--bg-elevated); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-md); | |
| box-shadow: var(--shadow-lg); | |
| z-index: var(--z-modal); | |
| display: none; | |
| overflow: hidden; | |
| } | |
| .model-dropdown.show { | |
| display: block; | |
| } | |
| .model-option { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 14px; | |
| cursor: pointer; | |
| transition: background var(--transition-fast); | |
| } | |
| .model-option:hover { | |
| background: var(--bg-hover); | |
| } | |
| .model-option.active { | |
| background: var(--accent-light); | |
| color: var(--accent); | |
| } | |
| .model-option .model-name { | |
| font-weight: 500; | |
| font-size: 13px; | |
| } | |
| .model-option .model-desc { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .status-indicators { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .status-pill { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 12px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-xl); | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| transition: all var(--transition-fast); | |
| } | |
| .status-pill:hover { | |
| border-color: var(--border-default); | |
| } | |
| .status-pill .dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| } | |
| .status-pill .dot.connected, | |
| .status-pill .dot.ready { | |
| background: var(--success); | |
| } | |
| .status-pill .dot.disconnected, | |
| .status-pill .dot.error { | |
| background: var(--error); | |
| } | |
| .status-pill .dot.loading { | |
| background: var(--warning); | |
| animation: spin 1s linear infinite; | |
| } | |
| .btn-icon { | |
| width: 36px; | |
| height: 36px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-md); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| font-size: 15px; | |
| } | |
| .btn-icon:hover { | |
| background: var(--bg-hover); | |
| border-color: var(--border-default); | |
| color: var(--text-primary); | |
| } | |
| .user-avatar { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, var(--accent), #8b5cf6); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: transform var(--transition-fast); | |
| } | |
| .user-avatar:hover { | |
| transform: scale(1.05); | |
| } | |
| /* βββ MAIN CONTENT AREA βββ */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* βββ SIDEBAR βββ */ | |
| .sidebar { | |
| width: var(--sidebar-width); | |
| min-width: var(--sidebar-width); | |
| background: var(--bg-secondary); | |
| border-right: 1px solid var(--border-light); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| z-index: var(--z-panel); | |
| } | |
| .sidebar-header { | |
| padding: 16px; | |
| border-bottom: 1px solid var(--border-light); | |
| } | |
| .new-chat-btn { | |
| width: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 12px 20px; | |
| background: var(--accent); | |
| color: white; | |
| border: none; | |
| border-radius: var(--radius-md); | |
| font-size: 14px; | |
| font-weight: 600; | |
| font-family: var(--font-sans); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| } | |
| .new-chat-btn:hover { | |
| background: var(--accent-hover); | |
| transform: translateY(-1px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| .new-chat-btn:active { | |
| transform: translateY(0); | |
| } | |
| .sidebar-section { | |
| padding: 16px; | |
| border-bottom: 1px solid var(--border-light); | |
| } | |
| .sidebar-section-title { | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.8px; | |
| color: var(--text-muted); | |
| padding: 0 4px 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .chat-history-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .chat-history-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 12px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| } | |
| .chat-history-item:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| .chat-history-item.active { | |
| background: var(--accent-light); | |
| color: var(--accent); | |
| } | |
| .chat-history-item .chat-icon { | |
| font-size: 14px; | |
| opacity: 0.7; | |
| } | |
| .chat-history-item .chat-title { | |
| flex: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .chat-history-empty { | |
| padding: 20px 12px; | |
| text-align: center; | |
| color: var(--text-muted); | |
| font-size: 12px; | |
| } | |
| .sidebar-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 12px; | |
| } | |
| /* File Tree Styles */ | |
| .file-tree { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| } | |
| .ws-dir { | |
| margin-bottom: 4px; | |
| } | |
| .ws-dir-name { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 8px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: all var(--transition-fast); | |
| user-select: none; | |
| } | |
| .ws-dir-name:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| .ws-dir-name .dir-icon { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| .ws-children { | |
| margin-left: 16px; | |
| padding-left: 12px; | |
| border-left: 1px solid var(--border-light); | |
| } | |
| .ws-file { | |
| margin-bottom: 2px; | |
| } | |
| .ws-file > span { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 5px 8px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: all var(--transition-fast); | |
| font-size: 12px; | |
| } | |
| .ws-file > span:hover { | |
| background: var(--bg-hover); | |
| color: var(--accent); | |
| padding-left: 12px; | |
| } | |
| .ws-file > span .file-icon { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| } | |
| .ws-empty { | |
| padding: 24px 12px; | |
| text-align: center; | |
| color: var(--text-muted); | |
| font-size: 12px; | |
| } | |
| /* File Viewer */ | |
| .file-viewer-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 12px; | |
| background: var(--bg-tertiary); | |
| border-bottom: 1px solid var(--border-light); | |
| } | |
| .file-viewer-header button { | |
| padding: 6px 12px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| transition: all var(--transition-fast); | |
| } | |
| .file-viewer-header button:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| .file-viewer-header span { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .file-viewer-content { | |
| padding: 16px; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| line-height: 1.7; | |
| color: var(--text-secondary); | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| } | |
| /* MCP Section */ | |
| .mcp-server { | |
| padding: 10px 12px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-md); | |
| margin-bottom: 8px; | |
| transition: all var(--transition-fast); | |
| } | |
| .mcp-server:hover { | |
| border-color: var(--border-default); | |
| } | |
| .mcp-server-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 6px; | |
| } | |
| .mcp-server-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| } | |
| .mcp-server-dot.connected { | |
| background: var(--success); | |
| } | |
| .mcp-server-dot.disconnected { | |
| background: var(--error); | |
| } | |
| .mcp-server-name { | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| flex: 1; | |
| } | |
| .tool-badge { | |
| font-size: 9px; | |
| font-weight: 600; | |
| padding: 2px 6px; | |
| border-radius: var(--radius-sm); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .tool-badge.success { | |
| background: var(--success-bg); | |
| color: var(--success); | |
| } | |
| .tool-badge.error { | |
| background: var(--error-bg); | |
| color: var(--error); | |
| } | |
| .tool-badge.running { | |
| background: var(--warning-bg); | |
| color: var(--warning); | |
| } | |
| .mcp-tools { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| } | |
| .mcp-tool-tag { | |
| font-size: 10px; | |
| padding: 2px 8px; | |
| background: var(--bg-tertiary); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| } | |
| .mcp-empty { | |
| padding: 20px 12px; | |
| text-align: center; | |
| color: var(--text-muted); | |
| font-size: 12px; | |
| line-height: 1.6; | |
| } | |
| /* Sidebar Footer / Settings */ | |
| .sidebar-footer { | |
| padding: 12px 16px; | |
| border-top: 1px solid var(--border-light); | |
| background: var(--bg-secondary); | |
| } | |
| .sidebar-footer-actions { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .storage-mode { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| padding: 4px 8px; | |
| background: var(--bg-primary); | |
| border-radius: var(--radius-sm); | |
| } | |
| .storage-mode .storage-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: var(--success); | |
| } | |
| .storage-mode.hf-hub .storage-dot { | |
| background: var(--accent); | |
| } | |
| .storage-mode.bucket .storage-dot { | |
| background: var(--warning); | |
| } | |
| .settings-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 32px; | |
| height: 32px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| font-size: 14px; | |
| } | |
| .settings-btn:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| /* Settings Panel */ | |
| .settings-panel { | |
| position: fixed; | |
| bottom: 60px; | |
| left: 280px; | |
| width: 320px; | |
| background: var(--bg-elevated); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-lg); | |
| box-shadow: var(--shadow-lg); | |
| z-index: var(--z-modal); | |
| display: none; | |
| overflow: hidden; | |
| } | |
| .settings-panel.show { | |
| display: block; | |
| animation: slideUp 0.2s ease-out; | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .settings-panel-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--border-light); | |
| } | |
| .settings-panel-title { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .settings-close-btn { | |
| width: 28px; | |
| height: 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: transparent; | |
| border: none; | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: all var(--transition-fast); | |
| } | |
| .settings-close-btn:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| .settings-panel-body { | |
| padding: 16px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| } | |
| .setting-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .setting-label { | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .setting-value { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| font-family: var(--font-mono); | |
| } | |
| .setting-select { | |
| width: 100%; | |
| padding: 10px 12px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-primary); | |
| font-size: 13px; | |
| font-family: var(--font-sans); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| appearance: none; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; | |
| background-position: right 12px center; | |
| padding-right: 32px; | |
| } | |
| .setting-select:focus { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px var(--accent-light); | |
| outline: none; | |
| } | |
| .setting-slider { | |
| width: 100%; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: var(--bg-tertiary); | |
| appearance: none; | |
| cursor: pointer; | |
| } | |
| .setting-slider::-webkit-slider-thumb { | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| cursor: pointer; | |
| transition: transform var(--transition-fast); | |
| } | |
| .setting-slider::-webkit-slider-thumb:hover { | |
| transform: scale(1.1); | |
| } | |
| .setting-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .setting-toggle { | |
| position: relative; | |
| width: 44px; | |
| height: 24px; | |
| background: var(--bg-tertiary); | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: background var(--transition-fast); | |
| } | |
| .setting-toggle.active { | |
| background: var(--accent); | |
| } | |
| .setting-toggle::after { | |
| content: ''; | |
| position: absolute; | |
| top: 2px; | |
| left: 2px; | |
| width: 20px; | |
| height: 20px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: transform var(--transition-fast); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .setting-toggle.active::after { | |
| transform: translateX(20px); | |
| } | |
| .export-buttons { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .export-btn { | |
| flex: 1; | |
| padding: 10px 12px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-secondary); | |
| font-size: 12px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| } | |
| .export-btn:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| background: var(--accent-light); | |
| } | |
| /* βββ CHAT AREA βββ */ | |
| .chat-area { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| background: var(--bg-primary); | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| scroll-behavior: smooth; | |
| } | |
| .chat-messages::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| /* Welcome Banner */ | |
| .welcome-banner { | |
| max-width: 680px; | |
| margin: 60px auto; | |
| text-align: center; | |
| padding: 48px 40px; | |
| } | |
| .welcome-icon { | |
| font-size: 56px; | |
| margin-bottom: 20px; | |
| display: block; | |
| } | |
| .welcome-title { | |
| font-size: 28px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-bottom: 12px; | |
| letter-spacing: -0.5px; | |
| } | |
| .welcome-subtitle { | |
| font-size: 16px; | |
| color: var(--text-secondary); | |
| margin-bottom: 32px; | |
| line-height: 1.6; | |
| } | |
| .examples-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 10px; | |
| max-width: 520px; | |
| margin: 0 auto; | |
| } | |
| .example-card { | |
| padding: 14px 18px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-md); | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| text-align: left; | |
| } | |
| .example-card:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| background: var(--accent-light); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| /* Message Styles */ | |
| .msg { | |
| margin-bottom: 24px; | |
| animation: msgSlideIn 0.25s ease-out; | |
| position: relative; | |
| } | |
| @keyframes msgSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(8px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .msg-user { | |
| display: flex; | |
| justify-content: flex-end; | |
| } | |
| .msg-user .msg-content-wrapper { | |
| max-width: 75%; | |
| background: var(--user-bubble); | |
| color: var(--user-bubble-text); | |
| border-radius: var(--radius-lg) var(--radius-lg) 4px var(--radius-lg); | |
| padding: 14px 18px; | |
| position: relative; | |
| } | |
| .msg-assistant { | |
| display: flex; | |
| justify-content: flex-start; | |
| } | |
| .msg-assistant .msg-content-wrapper { | |
| max-width: 85%; | |
| background: var(--ai-bubble); | |
| color: var(--ai-bubble-text); | |
| border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) 4px; | |
| padding: 14px 18px; | |
| position: relative; | |
| } | |
| .msg-system { | |
| display: flex; | |
| justify-content: center; | |
| } | |
| .msg-system .msg-content-wrapper { | |
| max-width: 80%; | |
| background: var(--bg-secondary); | |
| border: 1px dashed var(--border-default); | |
| border-radius: var(--radius-md); | |
| padding: 12px 20px; | |
| text-align: center; | |
| color: var(--text-secondary); | |
| } | |
| .msg-prefix { | |
| font-weight: 600; | |
| margin-right: 6px; | |
| } | |
| .msg-user .msg-prefix { | |
| color: rgba(255,255,255,0.8); | |
| } | |
| .msg-assistant .msg-prefix { | |
| color: var(--accent); | |
| } | |
| .msg-system .msg-prefix { | |
| color: var(--warning); | |
| } | |
| .msg-content { | |
| color: inherit; | |
| line-height: 1.7; | |
| word-wrap: break-word; | |
| } | |
| .msg-body { | |
| margin-top: 8px; | |
| } | |
| .iteration-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 10px; | |
| background: var(--info-bg); | |
| border: 1px solid var(--accent-border); | |
| border-radius: var(--radius-sm); | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: var(--accent); | |
| margin-bottom: 10px; | |
| } | |
| /* Code Block Styles */ | |
| .code-block-wrap { | |
| margin: 12px 0; | |
| background: #1e1e1e; | |
| border: 1px solid var(--border-default); | |
| border-radius: var(--radius-md); | |
| overflow: hidden; | |
| } | |
| .code-block-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 14px; | |
| background: #2d2d2d; | |
| border-bottom: 1px solid #3d3d3d; | |
| } | |
| .code-lang { | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: #9ca3af; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| font-family: var(--font-mono); | |
| } | |
| .btn-copy { | |
| padding: 4px 10px; | |
| background: transparent; | |
| border: 1px solid #4a4a4a; | |
| border-radius: 4px; | |
| color: #9ca3af; | |
| font-size: 11px; | |
| cursor: pointer; | |
| font-family: var(--font-mono); | |
| transition: all var(--transition-fast); | |
| } | |
| .btn-copy:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| .btn-copy.copied { | |
| background: var(--success-bg); | |
| border-color: var(--success); | |
| color: var(--success); | |
| } | |
| .code-block-wrap pre { | |
| margin: 0; | |
| padding: 14px; | |
| overflow-x: auto; | |
| } | |
| .code-block-wrap code { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| line-height: 1.7; | |
| color: #e0e0e0; | |
| } | |
| /* Inline code */ | |
| .msg-content code:not([id]) { | |
| padding: 2px 6px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-light); | |
| border-radius: 4px; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| color: var(--accent); | |
| } | |
| /* Headings in messages */ | |
| .msg-content h1, .msg-content h2, .msg-content h3 { | |
| margin: 16px 0 8px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .msg-content h1 { font-size: 18px; } | |
| .msg-content h2 { font-size: 16px; } | |
| .msg-content h3 { font-size: 14px; } | |
| .msg-content strong { | |
| color: var(--text-primary); | |
| font-weight: 600; | |
| } | |
| .msg-content em { | |
| color: var(--accent); | |
| font-style: normal; | |
| } | |
| .msg-content li { | |
| margin-left: 20px; | |
| margin-bottom: 4px; | |
| list-style: disc; | |
| } | |
| .msg-content ul, .msg-content ol { | |
| margin: 8px 0; | |
| padding-left: 20px; | |
| } | |
| /* Streaming Cursor */ | |
| .streaming-cursor { | |
| display: inline-block; | |
| width: 8px; | |
| height: 18px; | |
| background: var(--accent); | |
| margin-left: 2px; | |
| vertical-align: text-bottom; | |
| animation: cursorBlink 1s step-end infinite; | |
| border-radius: 1px; | |
| } | |
| @keyframes cursorBlink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0; } | |
| } | |
| /* Tool Call Block */ | |
| .tool-block { | |
| margin: 12px 0; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-md); | |
| overflow: hidden; | |
| animation: toolSlideIn 0.25s ease-out; | |
| } | |
| @keyframes toolSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-8px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| .tool-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 12px 14px; | |
| background: var(--bg-tertiary); | |
| border-bottom: 1px solid var(--border-light); | |
| cursor: pointer; | |
| transition: background var(--transition-fast); | |
| } | |
| .tool-header:hover { | |
| background: var(--bg-hover); | |
| } | |
| .tool-icon { | |
| font-size: 14px; | |
| } | |
| .tool-name { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| flex: 1; | |
| } | |
| .tool-args { | |
| padding: 12px 14px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| border-bottom: 1px solid var(--border-light); | |
| background: var(--bg-primary); | |
| } | |
| .tool-result { | |
| padding: 12px 14px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .tool-result.error-output { | |
| color: var(--error); | |
| } | |
| /* βββ INPUT AREA βββ */ | |
| .input-area { | |
| padding: 16px 24px 20px; | |
| background: var(--bg-primary); | |
| border-top: 1px solid var(--border-light); | |
| } | |
| .options-bar { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| align-items: center; | |
| } | |
| .option-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .option-group label { | |
| font-size: 11px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| color: var(--text-muted); | |
| } | |
| .option-select { | |
| padding: 6px 10px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-secondary); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| outline: none; | |
| } | |
| .option-select:hover, .option-select:focus { | |
| border-color: var(--accent); | |
| } | |
| .option-checkbox { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| cursor: pointer; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| padding: 6px 10px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| transition: all var(--transition-fast); | |
| } | |
| .option-checkbox:hover { | |
| border-color: var(--border-default); | |
| } | |
| .option-checkbox input[type="checkbox"] { | |
| appearance: none; | |
| -webkit-appearance: none; | |
| width: 14px; | |
| height: 14px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-default); | |
| border-radius: 3px; | |
| cursor: pointer; | |
| position: relative; | |
| transition: all var(--transition-fast); | |
| } | |
| .option-checkbox input[type="checkbox"]:checked { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| .option-checkbox input[type="checkbox"]:checked::after { | |
| content: 'β'; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 10px; | |
| color: white; | |
| font-weight: bold; | |
| } | |
| .image-upload-btn { | |
| padding: 6px 12px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| font-size: 11px; | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .image-upload-btn:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| #opt-image-input { | |
| display: none; | |
| } | |
| .image-preview-container { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .img-preview { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 10px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-default); | |
| border-radius: var(--radius-sm); | |
| animation: imgAppear 0.2s ease-out; | |
| } | |
| @keyframes imgAppear { | |
| from { opacity: 0; transform: scale(0.95); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| .img-preview img { | |
| width: 28px; | |
| height: 28px; | |
| object-fit: cover; | |
| border-radius: 4px; | |
| } | |
| .img-preview span { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| max-width: 100px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .img-preview .remove { | |
| cursor: pointer; | |
| color: var(--text-muted); | |
| font-size: 16px; | |
| line-height: 1; | |
| transition: color var(--transition-fast); | |
| } | |
| .img-preview .remove:hover { | |
| color: var(--error); | |
| } | |
| .input-row { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| } | |
| .input-wrapper { | |
| flex: 1; | |
| position: relative; | |
| } | |
| .chat-input { | |
| width: 100%; | |
| min-height: 48px; | |
| max-height: 150px; | |
| padding: 14px 18px; | |
| padding-right: 50px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-lg); | |
| color: var(--text-primary); | |
| font-family: var(--font-sans); | |
| font-size: 14px; | |
| line-height: 1.5; | |
| resize: none; | |
| outline: none; | |
| transition: all var(--transition-fast); | |
| } | |
| .chat-input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| .chat-input:focus { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px var(--accent-light); | |
| } | |
| .chat-input:not(:placeholder-shown) { | |
| border-color: var(--border-default); | |
| } | |
| .input-hint { | |
| position: absolute; | |
| right: 14px; | |
| bottom: 12px; | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| font-family: var(--font-mono); | |
| } | |
| .send-buttons { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .btn-send, .btn-stop { | |
| width: 48px; | |
| height: 48px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border: none; | |
| border-radius: var(--radius-lg); | |
| cursor: pointer; | |
| font-size: 18px; | |
| transition: all var(--transition-base); | |
| } | |
| .btn-send { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| .btn-send:hover { | |
| background: var(--accent-hover); | |
| transform: translateY(-1px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| .btn-send:active { | |
| transform: translateY(0); | |
| } | |
| .btn-stop { | |
| background: var(--error); | |
| color: white; | |
| display: none; | |
| } | |
| .btn-stop:hover { | |
| background: #dc2626; | |
| } | |
| /* βββ OUTPUT PANEL (Right Side) βββ */ | |
| .output-panel { | |
| width: 380px; | |
| min-width: 380px; | |
| background: var(--bg-secondary); | |
| border-left: 1px solid var(--border-light); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| z-index: var(--z-panel); | |
| } | |
| .output-tabs { | |
| display: flex; | |
| padding: 8px; | |
| gap: 4px; | |
| border-bottom: 1px solid var(--border-light); | |
| background: var(--bg-secondary); | |
| } | |
| .output-tab { | |
| flex: 1; | |
| padding: 8px 12px; | |
| background: transparent; | |
| border: none; | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| text-align: center; | |
| } | |
| .output-tab:hover { | |
| color: var(--text-secondary); | |
| background: var(--bg-hover); | |
| } | |
| .output-tab.active { | |
| background: var(--bg-primary); | |
| color: var(--accent); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .tab-pane { | |
| display: none; | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 12px; | |
| } | |
| .tab-pane.active { | |
| display: block; | |
| } | |
| /* Preview Pane */ | |
| .preview-container { | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| background: var(--bg-primary); | |
| border-radius: var(--radius-md); | |
| overflow: hidden; | |
| border: 1px solid var(--border-light); | |
| } | |
| .preview-placeholder { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--text-muted); | |
| text-align: center; | |
| padding: 24px; | |
| } | |
| .preview-placeholder-icon { | |
| font-size: 36px; | |
| margin-bottom: 12px; | |
| opacity: 0.5; | |
| } | |
| .preview-placeholder-text { | |
| font-size: 13px; | |
| } | |
| .preview-iframe { | |
| flex: 1; | |
| width: 100%; | |
| border: none; | |
| background: white; | |
| display: none; | |
| } | |
| .preview-toolbar { | |
| display: flex; | |
| justify-content: flex-end; | |
| padding: 8px; | |
| background: var(--bg-tertiary); | |
| border-top: 1px solid var(--border-light); | |
| } | |
| .btn-fullscreen { | |
| padding: 6px 12px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| font-size: 11px; | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| display: none; | |
| } | |
| .btn-fullscreen:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| /* Code Pane */ | |
| .code-container { | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .code-toolbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 12px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-md) var(--radius-md) 0 0; | |
| } | |
| .code-tab-btn { | |
| padding: 5px 10px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| font-size: 10px; | |
| font-family: var(--font-mono); | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| } | |
| .code-tab-btn:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| .code-content { | |
| flex: 1; | |
| background: #1e1e1e; | |
| border: 1px solid var(--border-light); | |
| border-top: none; | |
| border-radius: 0 0 var(--radius-md) var(--radius-md); | |
| padding: 14px; | |
| overflow: auto; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| line-height: 1.7; | |
| color: #e0e0e0; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| } | |
| /* Console Pane */ | |
| .console-container { | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .console-section { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 0; | |
| } | |
| .console-label { | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| color: var(--text-muted); | |
| padding: 6px 8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .console-label.stdout { color: var(--success); } | |
| .console-label.stderr { color: var(--error); } | |
| .console-output { | |
| flex: 1; | |
| background: #1e1e1e; | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-sm); | |
| padding: 10px; | |
| overflow: auto; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| line-height: 1.6; | |
| color: #9ca3af; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| } | |
| /* βββ STATUS BAR βββ */ | |
| .status-bar { | |
| height: 32px; | |
| min-height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 16px; | |
| background: var(--bg-secondary); | |
| border-top: 1px solid var(--border-light); | |
| font-size: 11px; | |
| font-family: var(--font-mono); | |
| } | |
| .status-left, .status-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| } | |
| .status-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| color: var(--text-muted); | |
| } | |
| .status-item .indicator { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| } | |
| .status-idle { color: var(--text-muted); } | |
| .status-working { color: var(--warning); } | |
| .status-success { color: var(--success); } | |
| .status-error { color: var(--error); } | |
| .status-info { color: var(--accent); } | |
| .status-working .indicator { | |
| animation: statusPulse 1s ease-in-out infinite; | |
| } | |
| @keyframes statusPulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.5; transform: scale(0.8); } | |
| } | |
| /* βββ FULLSCREEN OVERLAY βββ */ | |
| .fullscreen-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.9); | |
| backdrop-filter: blur(8px); | |
| z-index: var(--z-modal); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| } | |
| .fullscreen-overlay.active { | |
| display: flex; | |
| animation: fadeIn 0.2s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .fullscreen-close { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 40px; | |
| height: 40px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-md); | |
| color: var(--text-secondary); | |
| font-size: 20px; | |
| cursor: pointer; | |
| transition: all var(--transition-fast); | |
| } | |
| .fullscreen-close:hover { | |
| border-color: var(--error); | |
| color: var(--error); | |
| } | |
| .fullscreen-iframe { | |
| width: 90vw; | |
| height: 90vh; | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-lg); | |
| background: white; | |
| box-shadow: var(--shadow-lg); | |
| } | |
| /* βββ RESPONSIVE ADJUSTMENTS βββ */ | |
| @media (max-width: 1200px) { | |
| .output-panel { | |
| width: 300px; | |
| min-width: 300px; | |
| } | |
| .sidebar { | |
| width: 240px; | |
| min-width: 240px; | |
| } | |
| .settings-panel { | |
| left: 240px; | |
| } | |
| } | |
| @media (max-width: 900px) { | |
| .sidebar { | |
| display: none; | |
| } | |
| .settings-panel { | |
| left: 16px; | |
| right: 16px; | |
| width: auto; | |
| bottom: 50px; | |
| } | |
| .output-panel { | |
| width: 250px; | |
| min-width: 250px; | |
| } | |
| .header-center { | |
| display: none; | |
| } | |
| .examples-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| .status-indicators { | |
| display: none; | |
| } | |
| .options-bar { | |
| display: none; | |
| } | |
| .output-panel { | |
| position: fixed; | |
| right: 0; | |
| top: var(--header-height); | |
| bottom: 32px; | |
| width: 100%; | |
| min-width: 100%; | |
| transform: translateX(100%); | |
| transition: transform var(--transition-base); | |
| z-index: var(--z-overlay); | |
| } | |
| .output-panel.show { | |
| transform: translateX(0); | |
| } | |
| .welcome-banner { | |
| padding: 30px 20px; | |
| margin: 30px auto; | |
| } | |
| .welcome-title { | |
| font-size: 22px; | |
| } | |
| } | |
| /* βββ LOADING/SPINNER ANIMATIONS βββ */ | |
| .spinner { | |
| width: 20px; | |
| height: 20px; | |
| border: 2px solid var(--border-light); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| .typing-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 8px 12px; | |
| } | |
| .typing-indicator span { | |
| width: 6px; | |
| height: 6px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| animation: typingBounce 1.4s ease-in-out infinite; | |
| } | |
| .typing-indicator span:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .typing-indicator span:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| @keyframes typingBounce { | |
| 0%, 80%, 100% { | |
| transform: scale(0.6); | |
| opacity: 0.4; | |
| } | |
| 40% { | |
| transform: scale(1); | |
| opacity: 1; | |
| } | |
| } | |
| /* βββ UTILITY CLASSES βββ */ | |
| .hidden { display: none ; } | |
| .visible { display: block ; } | |
| /* Mobile sidebar toggle */ | |
| .mobile-sidebar-toggle { | |
| display: none; | |
| } | |
| @media (max-width: 900px) { | |
| .mobile-sidebar-toggle { | |
| display: flex; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Main App Container --> | |
| <div class="app-container"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <button class="btn-icon mobile-sidebar-toggle" onclick="toggleSidebar()">β°</button> | |
| <div class="logo" onclick="newChat()"> | |
| <div class="logo-icon">β‘</div> | |
| <div class="logo-text"> | |
| <span class="logo-title">SoniCoder</span> | |
| <span class="logo-version">v2.0</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="header-center"> | |
| <div class="search-bar"> | |
| <span class="search-icon">π</span> | |
| <input type="text" placeholder="Search conversations..." id="search-input"> | |
| </div> | |
| <div class="model-selector" onclick="toggleModelMenu()"> | |
| <span class="model-dot" id="model-dot"></span> | |
| <span class="model-text" id="model-pill-text">Loading...</span> | |
| <span style="font-size: 10px; color: var(--text-muted);">βΌ</span> | |
| <div class="model-dropdown" id="model-dropdown"> | |
| <!-- Model options populated by JS --> | |
| </div> | |
| </div> | |
| <div class="status-pill"> | |
| <span class="dot" id="mcp-dot"></span> | |
| <span id="mcp-pill-text">MCP</span> | |
| </div> | |
| </div> | |
| <div class="header-right"> | |
| <button class="btn-icon" onclick="refreshWorkspace()" title="Refresh Workspace">β»</button> | |
| <button class="btn-icon" onclick="newChat()" title="New Chat">+</button> | |
| <div class="user-avatar" onclick="toggleUserMenu()" title="User Menu">U</div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="main-content"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <button class="new-chat-btn" onclick="newChat()"> | |
| <span>+ New Chat</span> | |
| </button> | |
| </div> | |
| <!-- Chat History Section --> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-section-title"> | |
| <span>Recent Chats</span> | |
| </div> | |
| <div class="chat-history-list" id="chat-history-list"> | |
| <div class="chat-history-empty">No recent chats</div> | |
| </div> | |
| </div> | |
| <!-- Workspace Section --> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-section-title"> | |
| <span>π Workspace</span> | |
| <button class="btn-sidebar-action" onclick="refreshWorkspace()" title="Refresh" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:12px;">β»</button> | |
| </div> | |
| <div class="sidebar-content" style="padding: 0 12px;"> | |
| <div class="file-tree" id="files-tree"></div> | |
| <div class="file-viewer" id="file-viewer"></div> | |
| </div> | |
| </div> | |
| <!-- MCP Section --> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-section-title"> | |
| <span>π MCP Servers</span> | |
| </div> | |
| <div class="sidebar-content" style="padding: 0 12px;"> | |
| <div id="mcp-content"></div> | |
| </div> | |
| </div> | |
| <!-- Sidebar Footer with Storage Mode & Settings --> | |
| <div class="sidebar-footer"> | |
| <div class="sidebar-footer-actions"> | |
| <div class="storage-mode" id="storage-mode"> | |
| <span class="storage-dot"></span> | |
| <span>Local</span> | |
| </div> | |
| <button class="settings-btn" onclick="toggleSettings()" title="Settings">βοΈ</button> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Settings Panel --> | |
| <div class="settings-panel" id="settings-panel"> | |
| <div class="settings-panel-header"> | |
| <span class="settings-panel-title">Settings</span> | |
| <button class="settings-close-btn" onclick="toggleSettings()">β</button> | |
| </div> | |
| <div class="settings-panel-body"> | |
| <!-- Model Selection --> | |
| <div class="setting-group"> | |
| <label class="setting-label"> | |
| <span>Model</span> | |
| </label> | |
| <select class="setting-select" id="setting-model" onchange="onSettingModelChange(this.value)"> | |
| <option value="qwen2.5-coder">Qwen2.5-Coder</option> | |
| <option value="minicpm5">MiniCPM5</option> | |
| <option value="deepseek">DeepSeek</option> | |
| <option value="codestral">Codestral</option> | |
| </select> | |
| </div> | |
| <!-- Temperature --> | |
| <div class="setting-group"> | |
| <label class="setting-label"> | |
| <span>Temperature</span> | |
| <span class="setting-value" id="temp-value">0.7</span> | |
| </label> | |
| <input type="range" class="setting-slider" id="setting-temperature" | |
| min="0" max="2" step="0.1" value="0.7" | |
| oninput="document.getElementById('temp-value').textContent = this.value"> | |
| </div> | |
| <!-- Max Tokens --> | |
| <div class="setting-group"> | |
| <label class="setting-label"> | |
| <span>Max Tokens</span> | |
| <span class="setting-value" id="tokens-value">8192</span> | |
| </label> | |
| <input type="range" class="setting-slider" id="setting-maxtokens" | |
| min="1024" max="32768" step="1024" value="8192" | |
| oninput="document.getElementById('tokens-value').textContent = this.value"> | |
| </div> | |
| <!-- Storage Mode --> | |
| <div class="setting-group"> | |
| <label class="setting-label"> | |
| <span>Storage Mode</span> | |
| </label> | |
| <select class="setting-select" id="setting-storage" onchange="onStorageModeChange(this.value)"> | |
| <option value="local">Local</option> | |
| <option value="hf-hub">HuggingFace Hub</option> | |
| <option value="bucket">Cloud Bucket</option> | |
| </select> | |
| </div> | |
| <!-- Theme Toggle --> | |
| <div class="setting-group"> | |
| <div class="setting-row"> | |
| <label class="setting-label" style="margin:0;"><span>Dark Mode</span></label> | |
| <div class="setting-toggle" id="theme-toggle" onclick="toggleTheme()"></div> | |
| </div> | |
| </div> | |
| <!-- Export Buttons --> | |
| <div class="setting-group"> | |
| <label class="setting-label"><span>Export</span></label> | |
| <div class="export-buttons"> | |
| <button class="export-btn" onclick="exportChat('json')">π JSON</button> | |
| <button class="export-btn" onclick="exportChat('markdown')">π MD</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Area --> | |
| <section class="chat-area"> | |
| <div class="chat-messages" id="chat-messages"> | |
| <!-- Welcome message shown when no messages --> | |
| <div class="welcome-banner" id="welcome-banner"> | |
| <span class="welcome-icon">π</span> | |
| <h1 class="welcome-title">Hi, I'm SoniCoder</h1> | |
| <p class="welcome-subtitle">How can I help you code today? I can build apps, write scripts, debug code, and more.</p> | |
| <div class="examples-grid" id="examples-row"> | |
| <!-- Examples populated by JS --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="input-area"> | |
| <div class="options-bar"> | |
| <div class="option-group"> | |
| <label>Lang</label> | |
| <select class="option-select" id="opt-language" onchange="updateFrameworkOptions()"> | |
| <option value="">Auto</option> | |
| </select> | |
| </div> | |
| <div class="option-group"> | |
| <label>FW</label> | |
| <select class="option-select" id="opt-framework"> | |
| <option value="">Auto</option> | |
| </select> | |
| </div> | |
| <div class="option-group"> | |
| <label>Skill</label> | |
| <select class="option-select" id="opt-skill"> | |
| <option value="">None</option> | |
| </select> | |
| </div> | |
| <div class="option-group"> | |
| <label>Agent</label> | |
| <select class="option-select" id="opt-agent" onchange="setActiveAgent(this.value)"> | |
| <option value="">Default</option> | |
| </select> | |
| </div> | |
| <label class="option-checkbox"> | |
| <input type="checkbox" id="opt-search"> | |
| <span>Web Search</span> | |
| </label> | |
| <label class="image-upload-btn"> | |
| πΌοΈ Image | |
| <input type="file" id="opt-image-input" accept="image/*" onchange="handleImageSelect(event)"> | |
| </label> | |
| <div class="image-preview-container" id="image-preview-container"></div> | |
| </div> | |
| <div class="input-row"> | |
| <div class="input-wrapper"> | |
| <textarea | |
| class="chat-input" | |
| id="chat-input" | |
| placeholder="Ask SoniCoder to build something amazing..." | |
| rows="1" | |
| onkeydown="handleKeyDown(event)" | |
| oninput="autoResize(this)" | |
| ></textarea> | |
| <span class="input-hint">Ctrl+Enter</span> | |
| </div> | |
| <div class="send-buttons"> | |
| <button class="btn-send" id="btn-send" onclick="handleSend()">β€</button> | |
| <button class="btn-stop" id="btn-stop" onclick="stopGeneration()">β </button> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Output Panel --> | |
| <aside class="output-panel" id="output-panel"> | |
| <div class="output-tabs"> | |
| <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button> | |
| <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button> | |
| <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button> | |
| </div> | |
| <div class="tab-pane active" id="pane-preview"> | |
| <div class="preview-container"> | |
| <div class="preview-placeholder" id="preview-placeholder"> | |
| <div class="preview-placeholder-icon">π₯οΈ</div> | |
| <div class="preview-placeholder-text">Preview will appear here<br><small>when HTML is generated</small></div> | |
| </div> | |
| <iframe class="preview-iframe" id="preview-iframe" sandbox="allow-scripts allow-same-origin"></iframe> | |
| <div class="preview-toolbar"> | |
| <button class="btn-fullscreen" id="btn-fullscreen" onclick="openFullscreen()">βΆ Fullscreen</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="tab-pane" id="pane-code"> | |
| <div class="code-container"> | |
| <div class="code-toolbar"> | |
| <span style="font-size: 11px; color: var(--text-muted);">Generated Code</span> | |
| <button class="code-tab-btn" onclick="copyCode()">π Copy</button> | |
| </div> | |
| <pre class="code-content" id="generated-code">// Code will appear here...</pre> | |
| </div> | |
| </div> | |
| <div class="tab-pane" id="pane-console"> | |
| <div class="console-container"> | |
| <div class="console-section"> | |
| <div class="console-label stdout">β STDOUT</div> | |
| <div class="console-output" id="console-stdout">No output yet.</div> | |
| </div> | |
| <div class="console-section"> | |
| <div class="console-label stderr">β STDERR</div> | |
| <div class="console-output" id="console-stderr">No errors.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </main> | |
| <!-- Status Bar --> | |
| <footer class="status-bar"> | |
| <div class="status-left"> | |
| <span class="status-item status-idle" id="status-display"> | |
| <span class="indicator"></span> | |
| <span id="status-text">IDLE</span> | |
| </span> | |
| </div> | |
| <div class="status-right"> | |
| <span class="status-item"> | |
| <span id="header-model-name"></span> | |
| </span> | |
| <span class="status-item" id="status-storage">Local Mode</span> | |
| <span class="status-item">SoniCoder v2</span> | |
| </div> | |
| </footer> | |
| </div> | |
| <!-- Fullscreen Overlay --> | |
| <div class="fullscreen-overlay" id="fullscreen-overlay"> | |
| <button class="fullscreen-close" onclick="closeFullscreen()">β</button> | |
| <iframe class="fullscreen-iframe" id="fullscreen-iframe" sandbox="allow-scripts allow-same-origin"></iframe> | |
| </div> | |
| <script> | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SONICODER V2 - APPLICATION LOGIC | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // State Management | |
| var state = { | |
| sessionId: '', | |
| isGenerating: false, | |
| modelReady: false, | |
| config: null, | |
| toolCalls: {}, | |
| currentStreamDiv: null, | |
| lastCode: '', | |
| consoleStdout: '', | |
| consoleStderr: '', | |
| attachedImage: null, | |
| activeTab: 'preview', | |
| chatHistory: [], | |
| settingsOpen: false, | |
| modelMenuOpen: false, | |
| theme: 'light' | |
| }; | |
| // βββ INITIALIZATION βββ | |
| document.addEventListener('DOMContentLoaded', function() { | |
| loadConfig(); | |
| pollModelStatus(); | |
| loadMcpStatus(); | |
| refreshWorkspace(); | |
| initTheme(); | |
| }); | |
| function initTheme() { | |
| var savedTheme = localStorage.getItem('sonicoder-theme') || 'light'; | |
| state.theme = savedTheme; | |
| document.documentElement.setAttribute('data-theme', savedTheme); | |
| var toggle = document.getElementById('theme-toggle'); | |
| if (toggle && savedTheme === 'dark') { | |
| toggle.classList.add('active'); | |
| } | |
| } | |
| function toggleTheme() { | |
| var toggle = document.getElementById('theme-toggle'); | |
| if (state.theme === 'light') { | |
| state.theme = 'dark'; | |
| document.documentElement.setAttribute('data-theme', 'dark'); | |
| localStorage.setItem('sonicoder-theme', 'dark'); | |
| if (toggle) toggle.classList.add('active'); | |
| } else { | |
| state.theme = 'light'; | |
| document.documentElement.setAttribute('data-theme', ''); | |
| localStorage.setItem('sonicoder-theme', 'light'); | |
| if (toggle) toggle.classList.remove('active'); | |
| } | |
| } | |
| async function loadConfig() { | |
| try { | |
| var resp = await fetch('/api/config'); | |
| state.config = await resp.json(); | |
| var config = state.config; | |
| // Populate language selector | |
| var langSel = document.getElementById('opt-language'); | |
| langSel.innerHTML = '<option value="">Auto</option>'; | |
| (config.languages || []).forEach(function(pair) { | |
| var lang = Array.isArray(pair) ? pair[0] : (pair.language || pair.name || pair); | |
| var opt = document.createElement('option'); | |
| opt.value = lang; | |
| opt.textContent = lang; | |
| langSel.appendChild(opt); | |
| }); | |
| updateFrameworkOptions(); | |
| // Populate skills selector | |
| var skillSel = document.getElementById('opt-skill'); | |
| skillSel.innerHTML = '<option value="">None</option>'; | |
| (config.skills || []).forEach(function(s) { | |
| var name = typeof s === 'string' ? s : (s.name || s.id || ''); | |
| if (!name) return; | |
| var opt = document.createElement('option'); | |
| opt.value = name; | |
| opt.textContent = name; | |
| skillSel.appendChild(opt); | |
| }); | |
| // Populate agents selector | |
| var agentSel = document.getElementById('opt-agent'); | |
| agentSel.innerHTML = '<option value="">Default</option>'; | |
| (config.agents || []).forEach(function(a) { | |
| var name = typeof a === 'string' ? a : (a.name || a.id || ''); | |
| if (!name) return; | |
| var opt = document.createElement('option'); | |
| opt.value = name; | |
| opt.textContent = name + (a.active ? ' (active)' : ''); | |
| if (a.active) opt.selected = true; | |
| agentSel.appendChild(opt); | |
| }); | |
| // Populate model dropdown in header | |
| populateModelDropdown(config); | |
| renderExamples(config); | |
| } catch(e) { console.error('Config load failed:', e); } | |
| } | |
| function populateModelDropdown(config) { | |
| var dropdown = document.getElementById('model-dropdown'); | |
| if (!dropdown) return; | |
| var models = config.models || [ | |
| { key: 'qwen2.5-coder', name: 'Qwen2.5-Coder', desc: '1.5B β’ Code focused' }, | |
| { key: 'minicpm5', name: 'MiniCPM5', desc: '1B β’ Versatile' }, | |
| { key: 'deepseek', name: 'DeepSeek-Coder', desc: '1B β’ DeepSeek quality' }, | |
| { key: 'codestral', name: 'Codestral', desc: '1B β’ Fast inference' } | |
| ]; | |
| dropdown.innerHTML = ''; | |
| models.forEach(function(m) { | |
| var div = document.createElement('div'); | |
| div.className = 'model-option'; | |
| div.dataset.key = m.key; | |
| div.innerHTML = '<div><div class="model-name">' + m.name + '</div><div class="model-desc">' + (m.desc || '') + '</div></div>'; | |
| div.onclick = function(e) { | |
| e.stopPropagation(); | |
| switchModel(m.key); | |
| closeModelMenu(); | |
| }; | |
| dropdown.appendChild(div); | |
| }); | |
| } | |
| function updateFrameworkOptions() { | |
| if (!state.config) return; | |
| var langSel = document.getElementById('opt-language'); | |
| var fwSel = document.getElementById('opt-framework'); | |
| var lang = langSel.value; | |
| fwSel.innerHTML = '<option value="">Auto</option>'; | |
| if (!lang) return; | |
| var match = (state.config.languages || []).find(function(pair) { | |
| var l = Array.isArray(pair) ? pair[0] : (pair.language || pair.name || pair); | |
| return l === lang; | |
| }); | |
| if (!match) return; | |
| var fws = Array.isArray(match) ? match[1] : (match.frameworks || match.fws || []); | |
| (fws || []).forEach(function(fw) { | |
| var opt = document.createElement('option'); | |
| opt.value = fw; | |
| opt.textContent = fw; | |
| fwSel.appendChild(opt); | |
| }); | |
| } | |
| async function setActiveAgent(name) { | |
| try { | |
| await fetch('/api/set_active_agent', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({name: name || ''}), | |
| }); | |
| } catch(e) { console.error('Set agent failed:', e); } | |
| } | |
| function handleImageSelect(event) { | |
| var file = event.target.files && event.target.files[0]; | |
| if (!file) return; | |
| var reader = new FileReader(); | |
| reader.onload = function(ev) { | |
| state.attachedImage = {dataUrl: ev.target.result, name: file.name}; | |
| renderImagePreview(); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function renderImagePreview() { | |
| var container = document.getElementById('image-preview-container'); | |
| container.innerHTML = ''; | |
| if (!state.attachedImage) return; | |
| var div = document.createElement('span'); | |
| div.className = 'img-preview'; | |
| var img = document.createElement('img'); | |
| img.src = state.attachedImage.dataUrl; | |
| div.appendChild(img); | |
| var name = document.createElement('span'); | |
| name.textContent = state.attachedImage.name.substring(0, 16); | |
| div.appendChild(name); | |
| var rm = document.createElement('span'); | |
| rm.className = 'remove'; | |
| rm.textContent = 'Γ'; | |
| rm.onclick = function() { | |
| state.attachedImage = null; | |
| document.getElementById('opt-image-input').value = ''; | |
| renderImagePreview(); | |
| }; | |
| div.appendChild(rm); | |
| container.appendChild(div); | |
| } | |
| async function pollModelStatus() { | |
| try { | |
| var resp = await fetch('/api/model_status'); | |
| var data = await resp.json(); | |
| var dot = document.getElementById('model-dot'); | |
| var pillText = document.getElementById('model-pill-text'); | |
| if (data.status === 'ready') { | |
| state.modelReady = true; | |
| dot.className = 'model-dot ready'; | |
| pillText.textContent = data.model_name || 'Ready'; | |
| document.getElementById('header-model-name').textContent = data.model_name || ''; | |
| setStatus('status-success', 'MODEL READY'); | |
| setTimeout(function() { | |
| if (!state.isGenerating) setStatus('status-idle', 'IDLE'); | |
| }, 3000); | |
| return; | |
| } else if (data.status === 'loading') { | |
| dot.className = 'model-dot loading'; | |
| pillText.textContent = 'Loading...'; | |
| setStatus('status-working', 'LOADING MODEL...'); | |
| } else { | |
| dot.className = 'model-dot error'; | |
| pillText.textContent = 'Error'; | |
| setStatus('status-error', 'MODEL ERROR'); | |
| } | |
| setTimeout(pollModelStatus, 3000); | |
| } catch(e) { setTimeout(pollModelStatus, 5000); } | |
| } | |
| async function switchModel(key) { | |
| try { | |
| document.getElementById('model-dot').className = 'model-dot loading'; | |
| document.getElementById('model-pill-text').textContent = 'Switching...'; | |
| setStatus('status-working', 'SWITCHING MODEL...'); | |
| // Update setting select too | |
| var settingSelect = document.getElementById('setting-model'); | |
| if (settingSelect) settingSelect.value = key; | |
| await fetch('/api/switch_model', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ model_key: key }), | |
| }); | |
| setTimeout(pollModelStatus, 2000); | |
| } catch(e) { console.error(e); } | |
| } | |
| function onSettingModelChange(value) { | |
| switchModel(value); | |
| closeModelMenu(); | |
| } | |
| function onStorageModeChange(value) { | |
| var modeEl = document.getElementById('storage-mode'); | |
| var statusEl = document.getElementById('status-storage'); | |
| modeEl.className = 'storage-mode ' + value; | |
| var labels = { | |
| 'local': 'Local Mode', | |
| 'hf-hub': 'HF Hub Mode', | |
| 'bucket': 'Bucket Mode' | |
| }; | |
| var storageLabels = { | |
| 'local': 'Local', | |
| 'hf-hub': 'HF Hub', | |
| 'bucket': 'Bucket' | |
| }; | |
| if (modeEl) modeEl.querySelector('span:last-child').textContent = storageLabels[value] || value; | |
| if (statusEl) statusEl.textContent = labels[value] || value; | |
| } | |
| function toggleModelMenu() { | |
| var dropdown = document.getElementById('model-dropdown'); | |
| state.modelMenuOpen = !state.modelMenuOpen; | |
| if (dropdown) { | |
| dropdown.classList.toggle('show', state.modelMenuOpen); | |
| } | |
| } | |
| function closeModelMenu() { | |
| state.modelMenuOpen = false; | |
| var dropdown = document.getElementById('model-dropdown'); | |
| if (dropdown) dropdown.classList.remove('show'); | |
| } | |
| function toggleSettings() { | |
| var panel = document.getElementById('settings-panel'); | |
| state.settingsOpen = !state.settingsOpen; | |
| if (panel) { | |
| panel.classList.toggle('show', state.settingsOpen); | |
| } | |
| } | |
| function toggleSidebar() { | |
| var sidebar = document.getElementById('sidebar'); | |
| if (sidebar) { | |
| sidebar.style.display = sidebar.style.display === 'none' ? 'flex' : 'none'; | |
| } | |
| } | |
| function toggleUserMenu() { | |
| // Could implement user menu here | |
| console.log('User menu clicked'); | |
| } | |
| // Close menus when clicking outside | |
| document.addEventListener('click', function(e) { | |
| if (state.modelMenuOpen && !e.target.closest('.model-selector')) { | |
| closeModelMenu(); | |
| } | |
| if (state.settingsOpen && !e.target.closest('.settings-panel') && !e.target.closest('.settings-btn')) { | |
| var panel = document.getElementById('settings-panel'); | |
| if (panel) panel.classList.remove('show'); | |
| state.settingsOpen = false; | |
| } | |
| }); | |
| async function refreshWorkspace() { | |
| try { | |
| var resp = await fetch('/api/workspace_tree?max_depth=4'); | |
| var data = await resp.json(); | |
| var tree = document.getElementById('files-tree'); | |
| tree.style.display = 'block'; | |
| document.getElementById('file-viewer').style.display = 'none'; | |
| var rootNode = data.tree; | |
| if (!rootNode || typeof rootNode === 'string') { | |
| tree.innerHTML = '<div class="ws-empty">' + escHtml(rootNode || '(empty workspace)') + '</div>'; | |
| return; | |
| } | |
| function renderNode(node) { | |
| if (!node) return ''; | |
| var name = escHtml(node.name || ''); | |
| if (node.type === 'dir') { | |
| var html = '<div class="ws-dir"><span class="ws-dir-name"><span class="dir-icon">π</span>' + name + '</span>'; | |
| if (node.children && node.children.length > 0) { | |
| html += '<div class="ws-children">'; | |
| for (var i = 0; i < node.children.length; i++) { | |
| html += renderNode(node.children[i]); | |
| } | |
| html += '</div>'; | |
| } | |
| html += '</div>'; | |
| return html; | |
| } else { | |
| var size = node.size ? ' (' + formatSize(node.size) + ')' : ''; | |
| return '<div class="ws-file" data-path="' + name + '" onclick="openWorkspaceFile(\'' + name.replace(/'/g, "\\'") + '\')"><span><span class="file-icon">π</span>' + name + size + '</span></div>'; | |
| } | |
| } | |
| var html = renderNode(rootNode); | |
| if (!html || html === '') html = '<div class="ws-empty">(empty workspace)</div>'; | |
| tree.innerHTML = html; | |
| } catch(e) { | |
| console.error(e); | |
| var tree = document.getElementById('files-tree'); | |
| if (tree) tree.innerHTML = '<div class="ws-empty">(failed to load)</div>'; | |
| } | |
| } | |
| function formatSize(bytes) { | |
| if (bytes < 1024) return bytes + 'B'; | |
| if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'K'; | |
| return (bytes / (1024 * 1024)).toFixed(1) + 'M'; | |
| } | |
| async function openWorkspaceFile(path) { | |
| try { | |
| var resp = await fetch('/api/workspace_read', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({path: path}), | |
| }); | |
| var data = await resp.json(); | |
| if (!data.success) { | |
| alert(data.error || 'Failed to read file'); | |
| return; | |
| } | |
| var tree = document.getElementById('files-tree'); | |
| tree.style.display = 'none'; | |
| var viewer = document.getElementById('file-viewer'); | |
| viewer.style.display = 'block'; | |
| viewer.innerHTML = '<div class="file-viewer-header"><button onclick="refreshWorkspace()">β» Back</button><span>' + escHtml(path) + '</span></div><pre class="file-viewer-content">' + escHtml(data.content || '') + '</pre>'; | |
| } catch(e) { | |
| alert('Error reading file: ' + e.message); | |
| } | |
| } | |
| async function loadMcpStatus() { | |
| try { | |
| var resp = await fetch('/api/mcp_status'); | |
| var data = await resp.json(); | |
| var dot = document.getElementById('mcp-dot'); | |
| var text = document.getElementById('mcp-pill-text'); | |
| var content = document.getElementById('mcp-content'); | |
| var servers = data.servers || {}; | |
| var keys = Object.keys(servers); | |
| if (keys.length === 0) { | |
| dot.className = 'dot disconnected'; | |
| text.textContent = 'MCP'; | |
| content.innerHTML = '<div class="mcp-empty">No MCP servers configured.<br><br>Add MCP servers in your config to extend SoniCoder with external tools.</div>'; | |
| return; | |
| } | |
| var anyConnected = keys.some(function(k) { return servers[k].status === 'connected'; }); | |
| dot.className = anyConnected ? 'dot connected' : 'dot disconnected'; | |
| text.textContent = 'MCP (' + keys.length + ')'; | |
| var html = ''; | |
| keys.forEach(function(name) { | |
| var s = servers[name]; | |
| var isConn = s.status === 'connected'; | |
| html += '<div class="mcp-server">'; | |
| html += '<div class="mcp-server-header">'; | |
| html += '<span class="mcp-server-dot ' + (isConn ? 'connected' : 'disconnected') + '"></span>'; | |
| html += '<span class="mcp-server-name">' + escHtml(name) + '</span>'; | |
| html += '<span class="tool-badge ' + (isConn ? 'success' : 'error') + '">' + s.status + '</span>'; | |
| html += '</div>'; | |
| if (s.tools && s.tools.length > 0) { | |
| html += '<div class="mcp-tools">'; | |
| s.tools.forEach(function(t) { html += '<span class="mcp-tool-tag">' + escHtml(t) + '</span>'; }); | |
| html += '</div>'; | |
| } | |
| html += '</div>'; | |
| }); | |
| content.innerHTML = html; | |
| } catch(e) { | |
| document.getElementById('mcp-dot').className = 'dot disconnected'; | |
| document.getElementById('mcp-content').innerHTML = '<div class="mcp-empty">MCP status unavailable.</div>'; | |
| } | |
| } | |
| function renderExamples(config) { | |
| var examples = []; | |
| if (config && Array.isArray(config.examples) && config.examples.length) { | |
| examples = config.examples.map(function(e) { | |
| return typeof e === 'string' ? e : (e.label || e.prompt || ''); | |
| }).filter(Boolean); | |
| } | |
| if (!examples.length) { | |
| examples = [ | |
| 'Build a Flask REST API with user CRUD', | |
| 'Create a todo app with localStorage', | |
| 'Write a Python web scraper', | |
| 'Build a Markdown to HTML CLI tool', | |
| 'Create FastAPI with JWT auth', | |
| 'Make a React calculator component', | |
| ]; | |
| } | |
| examples = examples.slice(0, 6); | |
| var row = document.getElementById('examples-row'); | |
| if (!row) return; | |
| row.innerHTML = ''; | |
| examples.forEach(function(ex) { | |
| var card = document.createElement('div'); | |
| card.className = 'example-card'; | |
| card.textContent = ex; | |
| card.onclick = function() { | |
| document.getElementById('chat-input').value = ex; | |
| document.getElementById('chat-input').dispatchEvent(new Event('input')); | |
| handleSend(); | |
| }; | |
| row.appendChild(card); | |
| }); | |
| } | |
| // βββ EXPORT FUNCTIONS βββ | |
| function exportChat(format) { | |
| var messages = document.querySelectorAll('.chat-messages .msg'); | |
| if (!messages.length) { | |
| alert('No chat to export'); | |
| return; | |
| } | |
| var content = ''; | |
| var filename = ''; | |
| var mimeType = ''; | |
| if (format === 'json') { | |
| var chatData = []; | |
| messages.forEach(function(msg) { | |
| var role = msg.classList.contains('msg-user') ? 'user' : | |
| msg.classList.contains('msg-assistant') ? 'assistant' : 'system'; | |
| var text = msg.querySelector('.msg-content')?.textContent || ''; | |
| chatData.push({ role: role, content: text }); | |
| }); | |
| content = JSON.stringify(chatData, null, 2); | |
| filename = 'sonicoder-chat.json'; | |
| mimeType = 'application/json'; | |
| } else { | |
| messages.forEach(function(msg) { | |
| var prefix = msg.classList.contains('msg-user') ? '**You:** ' : | |
| msg.classList.contains('msg-assistant') ? '**SoniCoder:** ' : | |
| '> '; | |
| content += prefix + (msg.querySelector('.msg-content')?.textContent || '') + '\n\n'; | |
| }); | |
| filename = 'sonicoder-chat.md'; | |
| mimeType = 'text/markdown'; | |
| } | |
| var blob = new Blob([content], { type: mimeType }); | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| // βββ TABS βββ | |
| function switchTab(tab) { | |
| state.activeTab = tab; | |
| document.querySelectorAll('.output-tab').forEach(function(t) { | |
| t.classList.toggle('active', t.dataset.tab === tab); | |
| }); | |
| document.querySelectorAll('.tab-pane').forEach(function(p) { | |
| p.classList.toggle('active', p.id === 'pane-' + tab); | |
| }); | |
| } | |
| // βββ FULLSCREEN βββ | |
| function openFullscreen() { | |
| var iframe = document.getElementById('fullscreen-iframe'); | |
| var src = document.getElementById('preview-iframe').src; | |
| if (src) iframe.src = src; | |
| document.getElementById('fullscreen-overlay').classList.add('active'); | |
| } | |
| function closeFullscreen() { | |
| document.getElementById('fullscreen-overlay').classList.remove('active'); | |
| document.getElementById('fullscreen-iframe').src = 'about:blank'; | |
| } | |
| document.addEventListener('keydown', function(e) { | |
| if (e.key === 'Escape') { | |
| closeFullscreen(); | |
| closeModelMenu(); | |
| } | |
| }); | |
| // βββ INPUT HANDLING βββ | |
| function handleKeyDown(event) { | |
| if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { | |
| event.preventDefault(); | |
| handleSend(); | |
| } | |
| } | |
| function autoResize(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px'; | |
| } | |
| // βββ MESSAGES βββ | |
| function escHtml(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; } | |
| function addMessage(role, content, extra) { | |
| extra = extra || {}; | |
| // Hide welcome banner when first message is added | |
| var welcomeBanner = document.getElementById('welcome-banner'); | |
| if (welcomeBanner) welcomeBanner.style.display = 'none'; | |
| var container = document.getElementById('chat-messages'); | |
| var div = document.createElement('div'); | |
| div.className = 'msg msg-' + role; | |
| div.id = extra.msgId || ''; | |
| var wrapper = document.createElement('div'); | |
| wrapper.className = 'msg-content-wrapper'; | |
| var prefix = ''; | |
| if (role === 'user') prefix = '<span class="msg-prefix">You</span>'; | |
| else if (role === 'assistant') prefix = '<span class="msg-prefix">SoniCoder</span>'; | |
| else if (role === 'system') prefix = '<span class="msg-prefix">β</span>'; | |
| var iterationHtml = extra.iteration ? '<div class="iteration-badge">Iteration ' + extra.iteration + '/' + extra.max_iterations + '</div>' : ''; | |
| wrapper.innerHTML = iterationHtml + prefix + '<div class="msg-content">' + renderMd(content) + '</div>'; | |
| div.appendChild(wrapper); | |
| container.appendChild(div); | |
| container.scrollTop = container.scrollHeight; | |
| return div; | |
| } | |
| function renderMd(text) { | |
| if (!text) return ''; | |
| text = escHtml(text); | |
| // Code blocks | |
| text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) { | |
| var id = 'cb_' + Date.now() + Math.random().toString(36).substr(2,4); | |
| return '<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">' + (lang||'code') + '</span><button class="btn-copy" onclick="copyBlock(\'' + id + '\')">π Copy</button></div><pre><code id="' + id + '">' + code.trim() + '</code></pre></div>'; | |
| }); | |
| text = text.replace(/`([^`]+)`/g, '<code>$1</code>'); | |
| text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); | |
| text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>'); | |
| text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>'); | |
| text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>'); | |
| text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>'); | |
| text = text.replace(/^[-*] (.+)$/gm, '<li>$1</li>'); | |
| text = text.replace(/\n/g, '<br>'); | |
| return text; | |
| } | |
| function copyBlock(id) { | |
| var el = document.getElementById(id); | |
| if (!el) return; | |
| navigator.clipboard.writeText(el.textContent).then(function() { | |
| var btn = el.closest('.code-block-wrap').querySelector('.btn-copy'); | |
| btn.textContent = 'β Copied!'; | |
| btn.classList.add('copied'); | |
| setTimeout(function() { btn.innerHTML = 'π Copy'; btn.classList.remove('copied'); }, 2000); | |
| }); | |
| } | |
| function copyCode() { | |
| if (!state.lastCode) return; | |
| navigator.clipboard.writeText(state.lastCode).then(function() { | |
| var btn = document.querySelector('#pane-code .code-tab-btn'); | |
| btn.textContent = 'β Copied!'; | |
| setTimeout(function() { btn.innerHTML = 'π Copy'; }, 2000); | |
| }); | |
| } | |
| // βββ TOOL CALLS βββ | |
| function addToolCall(toolName, args) { | |
| var container = document.getElementById('chat-messages'); | |
| var id = 'tc_' + Date.now(); | |
| var div = document.createElement('div'); | |
| div.className = 'tool-block open'; | |
| div.id = id; | |
| var argsStr = ''; | |
| if (args) { | |
| Object.entries(args).forEach(function(e) { | |
| var v = typeof e[1] === 'string' ? (e[1].length > 80 ? e[1].slice(0,80) + '...' : e[1]) : JSON.stringify(e[1]); | |
| argsStr += escHtml(e[0] + ': ' + v) + '\n'; | |
| }); | |
| } | |
| div.innerHTML = | |
| '<div class="tool-header">' + | |
| '<span class="tool-icon">β‘</span>' + | |
| '<span class="tool-name">' + escHtml(toolName) + '</span>' + | |
| '<span class="tool-badge running">RUNNING</span>' + | |
| '</div>' + | |
| '<div class="tool-args">' + argsStr + '</div>' + | |
| '<div class="tool-result"></div>'; | |
| container.appendChild(div); | |
| container.scrollTop = container.scrollHeight; | |
| state.toolCalls[id] = div; | |
| return id; | |
| } | |
| function updateToolCall(id, success, output) { | |
| var div = state.toolCalls[id]; | |
| if (!div) return; | |
| var badge = div.querySelector('.tool-badge'); | |
| var result = div.querySelector('.tool-result'); | |
| if (success) { | |
| badge.className = 'tool-badge success'; | |
| badge.textContent = 'DONE'; | |
| } else { | |
| badge.className = 'tool-badge error'; | |
| badge.textContent = 'ERROR'; | |
| } | |
| if (output) { | |
| result.textContent = output; | |
| result.className = success ? 'tool-result' : 'tool-result error-output'; | |
| } | |
| document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight; | |
| } | |
| // βββ STREAMING βββ | |
| function startStreaming() { | |
| state.currentStreamDiv = addMessage('assistant', '', {msgId: 'streaming-msg'}); | |
| state.currentStreamDiv.querySelector('.msg-content').innerHTML = '<span class="streaming-cursor"></span>'; | |
| document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight; | |
| } | |
| function updateStreaming(text) { | |
| if (!state.currentStreamDiv) return; | |
| var el = state.currentStreamDiv.querySelector('.msg-content'); | |
| var clean = text.replace(/```tool[\s\S]*?```/g, '').trim(); | |
| el.innerHTML = renderMd(clean) + '<span class="streaming-cursor"></span>'; | |
| document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight; | |
| } | |
| function finalizeStream(text) { | |
| if (!state.currentStreamDiv) return; | |
| var el = state.currentStreamDiv.querySelector('.msg-content'); | |
| el.innerHTML = renderMd(text || 'Done.'); | |
| state.currentStreamDiv.removeAttribute('id'); | |
| state.currentStreamDiv = null; | |
| } | |
| // βββ SEND MESSAGE βββ | |
| async function handleSend() { | |
| var input = document.getElementById('chat-input'); | |
| var message = input.value.trim(); | |
| if (!message || state.isGenerating) return; | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| state.isGenerating = true; | |
| document.getElementById('btn-send').style.display = 'none'; | |
| document.getElementById('btn-stop').style.display = 'flex'; | |
| addMessage('user', message); | |
| startStreaming(); | |
| setStatus('status-working', 'GENERATING...'); | |
| // Collect option values | |
| var targetLanguage = (document.getElementById('opt-language') || {}).value || ''; | |
| var targetFramework = (document.getElementById('opt-framework') || {}).value || ''; | |
| var skill = (document.getElementById('opt-skill') || {}).value || ''; | |
| var agent = (document.getElementById('opt-agent') || {}).value || ''; | |
| var searchEnabled = (document.getElementById('opt-search') || {}).checked || false; | |
| // Prepend /skill command if a skill is selected and not already in the message | |
| var finalMessage = message; | |
| if (skill && !message.startsWith('/skill')) { | |
| finalMessage = '/skill ' + skill + '\n\n' + message; | |
| } | |
| // Attach image if present | |
| var imageUrl = ''; | |
| if (state.attachedImage && state.attachedImage.dataUrl) { | |
| imageUrl = state.attachedImage.dataUrl; | |
| } | |
| try { | |
| var resp = await fetch('/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| message: finalMessage, | |
| session_id: state.sessionId, | |
| target_language: targetLanguage, | |
| target_framework: targetFramework, | |
| search_enabled: searchEnabled ? 'true' : 'false', | |
| agent_name: agent, | |
| image_url: imageUrl, | |
| }), | |
| }); | |
| if (!resp.ok) throw new Error('HTTP ' + resp.status); | |
| var reader = resp.body.getReader(); | |
| var decoder = new TextDecoder(); | |
| var buffer = ''; | |
| while (true) { | |
| var result = await reader.read(); | |
| if (result.done) break; | |
| buffer += decoder.decode(result.value, {stream: true}); | |
| var lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (var i = 0; i < lines.length; i++) { | |
| var line = lines[i]; | |
| if (!line.trim()) continue; | |
| try { | |
| var ev = JSON.parse(line); | |
| switch (ev.type) { | |
| case 'streaming': | |
| updateStreaming(ev.content || ''); | |
| break; | |
| case 'tool_call': | |
| setStatus('status-info', 'TOOL: ' + ev.tool); | |
| addToolCall(ev.tool, ev.args); | |
| break; | |
| case 'tool_result': | |
| var tcIds = Object.keys(state.toolCalls).reverse(); | |
| for (var j = 0; j < tcIds.length; j++) { | |
| var tcDiv = state.toolCalls[tcIds[j]]; | |
| if (tcDiv && tcDiv.querySelector('.tool-badge.running')) { | |
| updateToolCall(tcIds[j], ev.result.success, ev.result.output || ev.result.error); | |
| break; | |
| } | |
| } | |
| // Capture code for Code tab | |
| if (ev.result.output && (ev.result.output.includes('<!DOCTYPE') || ev.result.output.includes('<html'))) { | |
| showPreview(ev.result.output); | |
| } | |
| if (ev.result.stdout) { state.consoleStdout += ev.result.stdout + '\n'; updateConsole(); } | |
| if (ev.result.stderr) { state.consoleStderr += ev.result.stderr + '\n'; updateConsole(); } | |
| break; | |
| case 'status': | |
| if (ev.iteration && state.currentStreamDiv) { | |
| setStatus('status-working', 'ITERATION ' + ev.iteration + '/' + ev.max_iterations); | |
| } | |
| if (ev.session_id && !state.sessionId) state.sessionId = ev.session_id; | |
| break; | |
| case 'complete': | |
| finalizeStream(ev.content || 'Done.'); | |
| setTimeout(refreshWorkspace, 500); | |
| setStatus('status-success', 'COMPLETE'); | |
| setTimeout(function() { if (!state.isGenerating) setStatus('status-idle', 'IDLE'); }, 3000); | |
| // Clear attached image after a successful send | |
| if (state.attachedImage) { | |
| state.attachedImage = null; | |
| document.getElementById('opt-image-input').value = ''; | |
| renderImagePreview(); | |
| } | |
| break; | |
| case 'error': | |
| finalizeStream('Error: ' + ev.error); | |
| setStatus('status-error', 'ERROR'); | |
| break; | |
| } | |
| } catch(e) { /* skip */ } | |
| } | |
| } | |
| } catch(e) { | |
| if (e.name !== 'AbortError') { | |
| finalizeStream('Connection error: ' + e.message); | |
| setStatus('status-error', 'CONNECTION ERROR'); | |
| } | |
| } | |
| state.isGenerating = false; | |
| document.getElementById('btn-send').style.display = 'flex'; | |
| document.getElementById('btn-stop').style.display = 'none'; | |
| } | |
| function stopGeneration() { | |
| state.isGenerating = false; | |
| if (state.currentStreamDiv) finalizeStream(state.currentStreamDiv.querySelector('.msg-content').textContent || '(stopped)'); | |
| document.getElementById('btn-send').style.display = 'flex'; | |
| document.getElementById('btn-stop').style.display = 'none'; | |
| setStatus('status-idle', 'IDLE'); | |
| } | |
| function newChat() { | |
| if (state.sessionId) { | |
| fetch('/api/reset_session', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ session_id: state.sessionId }), | |
| }).catch(function(){}); | |
| } | |
| state.sessionId = ''; | |
| state.toolCalls = {}; | |
| state.consoleStdout = ''; | |
| state.consoleStderr = ''; | |
| document.getElementById('chat-messages').innerHTML = ''; | |
| // Show welcome banner again | |
| var welcomeBanner = document.getElementById('welcome-banner'); | |
| if (welcomeBanner) welcomeBanner.style.display = ''; | |
| addMessage('system', 'New session started. What would you like to build?'); | |
| updateConsole(); | |
| setStatus('status-idle', 'IDLE'); | |
| } | |
| // βββ PREVIEW & CONSOLE βββ | |
| function showPreview(html) { | |
| var iframe = document.getElementById('preview-iframe'); | |
| var placeholder = document.getElementById('preview-placeholder'); | |
| var btn = document.getElementById('btn-fullscreen'); | |
| placeholder.style.display = 'none'; | |
| iframe.style.display = 'block'; | |
| btn.style.display = 'inline-block'; | |
| iframe.srcdoc = html; | |
| switchTab('preview'); | |
| } | |
| function updateConsole() { | |
| document.getElementById('console-stdout').textContent = state.consoleStdout || 'No output yet.'; | |
| document.getElementById('console-stderr').textContent = state.consoleStderr || 'No errors.'; | |
| } | |
| // βββ STATUS HELPER βββ | |
| function setStatus(className, text) { | |
| var el = document.getElementById('status-display'); | |
| el.className = 'status-item ' + className; | |
| document.getElementById('status-text').textContent = text; | |
| } | |
| </script> | |
| </body> | |
| </html> | |