sonicoder / index.html
Z User
fix: Redesign UI to match DeepSeek chat style
2476d62
Raw
History Blame Contribute Delete
84.2 kB
<!DOCTYPE html>
<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 !important; }
.visible { display: block !important; }
/* 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>