Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Transformers Workbench</title> | |
| <!-- Import Google Fonts --> | |
| <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=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <!-- Import FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* --- CSS VARIABLES & RESET --- */ | |
| :root { | |
| --bg-dark: #0f172a; | |
| --bg-panel: #1e293b; | |
| --bg-input: #0f172a; | |
| --border: #334155; | |
| --border-hover: #94a3b8; | |
| --primary: #38bdf8; /* Sky Blue */ | |
| --secondary: #818cf8; /* Indigo */ | |
| --accent-glow: rgba(56, 189, 248, 0.15); | |
| --success: #22c55e; | |
| --danger: #ef4444; | |
| --text-main: #f1f5f9; | |
| --text-muted: #94a3b8; | |
| --font-main: 'Inter', sans-serif; | |
| --font-code: 'JetBrains Mono', monospace; | |
| --header-height: 60px; | |
| --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| font-family: var(--font-main); | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* --- Scrollbar --- */ | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } | |
| /* --- UTILITIES --- */ | |
| .hidden { display: none ; } | |
| .flex { display: flex; } | |
| .flex-col { flex-direction: column; } | |
| .items-center { align-items: center; } | |
| .justify-between { justify-content: space-between; } | |
| .gap-2 { gap: 8px; } | |
| .gap-4 { gap: 16px; } | |
| .text-sm { font-size: 0.875rem; } | |
| .text-xs { font-size: 0.75rem; } | |
| .font-mono { font-family: var(--font-code); } | |
| /* --- HEADER --- */ | |
| header { | |
| height: var(--header-height); | |
| background-color: rgba(30, 41, 59, 0.8); | |
| backdrop-filter: blur(12px); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| flex-shrink: 0; | |
| z-index: 50; | |
| } | |
| .logo { | |
| font-weight: 700; | |
| font-size: 1.2rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| color: var(--primary); | |
| text-shadow: 0 0 10px rgba(56, 189, 248, 0.3); | |
| } | |
| .anycoder-link { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: var(--transition); | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| border: 1px solid transparent; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--primary); | |
| border-color: var(--border); | |
| background: var(--bg-panel); | |
| } | |
| /* --- MAIN LAYOUT --- */ | |
| .main-container { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* --- SIDEBAR (History) --- */ | |
| .sidebar { | |
| width: 280px; | |
| background-color: var(--bg-panel); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| transition: var(--transition); | |
| z-index: 40; | |
| } | |
| .sidebar-header { padding: 15px; border-bottom: 1px solid var(--border); } | |
| .btn-new-chat { | |
| width: 100%; | |
| padding: 10px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: #fff; | |
| border: none; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| transition: var(--transition); | |
| box-shadow: 0 4px 12px rgba(56, 189, 248, 0.2); | |
| } | |
| .btn-new-chat:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3); } | |
| .history-list { flex: 1; overflow-y: auto; padding: 10px; } | |
| .history-item { | |
| padding: 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 5px; | |
| color: var(--text-muted); | |
| transition: var(--transition); | |
| border: 1px solid transparent; | |
| } | |
| .history-item:hover { background-color: rgba(255,255,255,0.05); color: var(--text-main); } | |
| .history-item.active { | |
| background: var(--accent-glow); | |
| border-color: var(--primary); | |
| color: var(--primary); | |
| } | |
| .history-name { | |
| font-size: 0.9rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| flex: 1; | |
| margin-right: 8px; | |
| padding: 2px 4px; | |
| border-radius: 4px; | |
| } | |
| .history-name:focus { background: var(--bg-input); border: 1px solid var(--primary); color: var(--text-main); } | |
| .history-actions i { | |
| font-size: 0.8rem; | |
| padding: 4px; | |
| border-radius: 4px; | |
| transition: 0.2s; | |
| } | |
| .history-actions i:hover { color: var(--danger); background: rgba(239, 68, 68, 0.1); } | |
| .sidebar-footer { padding: 15px; border-top: 1px solid var(--border); } | |
| .btn-import { | |
| width: 100%; padding: 8px; background: transparent; | |
| border: 1px dashed var(--border); color: var(--text-muted); | |
| border-radius: 6px; cursor: pointer; transition: var(--transition); | |
| display: flex; align-items: center; justify-content: center; gap: 6px; | |
| } | |
| .btn-import:hover { border-color: var(--primary); color: var(--primary); background: rgba(56, 189, 248, 0.05); } | |
| /* --- CHAT AREA --- */ | |
| .chat-area { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background-color: var(--bg-dark); | |
| position: relative; | |
| min-width: 0; | |
| } | |
| .chat-header { | |
| padding: 10px 20px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(15, 23, 42, 0.6); | |
| backdrop-filter: blur(5px); | |
| } | |
| .model-indicator { display: flex; align-items: center; gap: 10px; font-weight: 600; } | |
| .badge { | |
| font-size: 0.7rem; padding: 2px 8px; border-radius: 12px; | |
| background: var(--bg-panel); border: 1px solid var(--border); | |
| color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; | |
| transition: var(--transition); | |
| } | |
| .badge.active { background: var(--success); color: #fff; border-color: var(--success); box-shadow: 0 0 8px rgba(34, 197, 94, 0.4); } | |
| .chat-messages { | |
| flex: 1; overflow-y: auto; padding: 20px; | |
| display: flex; flex-direction: column; gap: 20px; | |
| scroll-behavior: smooth; | |
| } | |
| .message { | |
| max-width: 85%; line-height: 1.6; position: relative; | |
| animation: slideUp 0.3s ease; | |
| } | |
| @keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| .message.user { align-self: flex-end; } | |
| .message.ai { align-self: flex-start; } | |
| .message.system { | |
| align-self: center; max-width: 90%; font-size: 0.85rem; | |
| color: var(--text-muted); text-align: center; | |
| background: rgba(255,255,255,0.05); padding: 4px 12px; | |
| border-radius: 20px; border: 1px solid var(--border); | |
| } | |
| .bubble { | |
| padding: 12px 18px; border-radius: 12px; position: relative; | |
| word-wrap: break-word; white-space: pre-wrap; font-size: 0.95rem; | |
| } | |
| .message.user .bubble { | |
| background-color: var(--primary); color: #0f172a; | |
| border-bottom-right-radius: 2px; | |
| } | |
| .message.ai .bubble { | |
| background-color: var(--bg-panel); border: 1px solid var(--border); | |
| border-bottom-left-radius: 2px; color: var(--text-main); | |
| } | |
| /* Copy/Paste Actions */ | |
| .msg-actions { | |
| position: absolute; top: -25px; right: 0; | |
| display: none; gap: 6px; | |
| } | |
| .message:hover .msg-actions { display: flex; } | |
| .action-btn { | |
| background: var(--bg-panel); border: 1px solid var(--border); | |
| color: var(--text-muted); padding: 4px 8px; border-radius: 4px; | |
| font-size: 0.75rem; cursor: pointer; display: flex; align-items: center; gap: 4px; | |
| transition: var(--transition); | |
| } | |
| .action-btn:hover { color: var(--primary); border-color: var(--primary); transform: translateY(-2px); } | |
| .input-area { | |
| padding: 20px; border-top: 1px solid var(--border); | |
| background-color: var(--bg-panel); | |
| } | |
| .input-wrapper { position: relative; max-width: 900px; margin: 0 auto; } | |
| .chat-input { | |
| width: 100%; background-color: var(--bg-input); | |
| border: 1px solid var(--border); border-radius: 8px; | |
| padding: 15px 50px 15px 15px; color: var(--text-main); | |
| font-family: var(--font-main); resize: none; | |
| min-height: 54px; max-height: 200px; line-height: 1.5; | |
| transition: var(--transition); | |
| } | |
| .chat-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.1); } | |
| .send-btn { | |
| position: absolute; right: 12px; bottom: 12px; background: none; | |
| border: none; color: var(--text-muted); cursor: pointer; | |
| font-size: 1.2rem; transition: var(--transition); | |
| } | |
| .send-btn:hover { color: var(--primary); transform: scale(1.1); } | |
| /* --- RIGHT PANEL (Tools) --- */ | |
| .right-panel { | |
| width: 320px; | |
| background-color: var(--bg-panel); | |
| border-left: 1px solid var(--border); | |
| display: flex; flex-direction: column; | |
| overflow-y: auto; transition: var(--transition); | |
| } | |
| .panel-section { padding: 20px; border-bottom: 1px solid var(--border); } | |
| .panel-title { | |
| font-size: 0.75rem; text-transform: uppercase; color: var(--text-muted); | |
| margin-bottom: 12px; font-weight: 700; letter-spacing: 0.5px; | |
| display: flex; align-items: center; gap: 8px; | |
| } | |
| /* Model Cards */ | |
| .model-card { | |
| background: var(--bg-input); border: 1px solid var(--border); | |
| border-radius: 8px; padding: 12px; margin-bottom: 10px; | |
| cursor: pointer; transition: var(--transition); | |
| } | |
| .model-card:hover { border-color: var(--text-muted); transform: translateY(-2px); } | |
| .model-card.active { | |
| border-color: var(--primary); background: var(--accent-glow); | |
| box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.3); | |
| } | |
| .model-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } | |
| .model-name { font-weight: 600; font-size: 0.95rem; } | |
| .model-meta { font-size: 0.8rem; color: var(--text-muted); line-height: 1.4; } | |
| .showcase-stats { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; } | |
| .stat { font-size: 0.7rem; background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; color: var(--text-muted); } | |
| /* Forms & Config */ | |
| .form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } | |
| .form-group label { font-size: 0.8rem; color: var(--text-muted); } | |
| .form-input { | |
| background: var(--bg-input); border: 1px solid var(--border); | |
| color: var(--text-main); padding: 8px 12px; border-radius: 6px; | |
| font-family: var(--font-code); font-size: 0.85rem; width: 100%; | |
| } | |
| .form-input:focus { border-color: var(--primary); } | |
| .btn-download, .btn-secondary { | |
| width: 100%; padding: 10px; border-radius: 6px; cursor: pointer; | |
| font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px; | |
| transition: var(--transition); | |
| } | |
| .btn-download { | |
| background-color: var(--primary); color: #0f172a; border: none; | |
| } | |
| .btn-download:hover { box-shadow: 0 0 15px rgba(56, 189, 248, 0.4); } | |
| .btn-secondary { | |
| background-color: transparent; border: 1px solid var(--border); color: var(--text-main); | |
| } | |
| .btn-secondary:hover { background: var(--bg-input); border-color: var(--text-muted); } | |
| /* Terminal */ | |
| .terminal { | |
| background: #000; color: #22c55e; font-family: var(--font-code); | |
| font-size: 0.75rem; padding: 12px; border-radius: 6px; | |
| height: 180px; overflow-y: auto; border: 1px solid #333; | |
| box-shadow: inset 0 0 10px rgba(0,0,0,0.8); | |
| } | |
| .terminal-line { margin-bottom: 4px; word-break: break-all; } | |
| .terminal-line.error { color: var(--danger); } | |
| .terminal-line.info { color: #94a3b8; } | |
| .terminal-line.command { color: var(--text-main); font-weight: bold; } | |
| /* Toast */ | |
| .toast { | |
| position: fixed; bottom: 24px; right: 24px; | |
| background: var(--bg-panel); border: 1px solid var(--primary); | |
| color: var(--text-main); padding: 12px 20px; border-radius: 8px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| transform: translateY(100px); opacity: 0; | |
| transition: var(--transition); z-index: 1000; | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .toast.show { transform: translateY(0); opacity: 1; } | |
| /* --- FOCUS MODE & RESPONSIVE --- */ | |
| body.focus-mode .sidebar, body.focus-mode .right-panel { display: none; } | |
| body.focus-mode .chat-area { max-width: 900px; margin: 0 auto; border-left: 1px solid var(--border); border-right: 1px solid var(--border); } | |
| @media (max-width: 1024px) { | |
| .right-panel { position: absolute; right: 0; height: 100%; transform: translateX(100%); z-index: 60; box-shadow: -5px 0 15px rgba(0,0,0,0.5); } | |
| .right-panel.open { transform: translateX(0); } | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { position: absolute; left: 0; height: 100%; transform: translateX(-100%); box-shadow: 5px 0 15px rgba(0,0,0,0.5); } | |
| .sidebar.open { transform: translateX(0); } | |
| .message { max-width: 95%; } | |
| } | |
| /* Mobile Overlay */ | |
| .mobile-overlay { | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0,0,0,0.5); z-index: 30; display: none; | |
| } | |
| .mobile-overlay.active { display: block; } | |
| /* Loading Animation */ | |
| .typing-indicator span { | |
| display: inline-block; width: 6px; height: 6px; | |
| background-color: var(--text-muted); border-radius: 50%; | |
| animation: typing 1.4s infinite ease-in-out both; margin: 0 2px; | |
| } | |
| .typing-indicator span:nth-child(1) { animation-delay: -0.32s; } | |
| .typing-indicator span:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes typing { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-cube"></i> | |
| <span>Transformers<span style="color:var(--text-muted); font-weight:400; font-size:0.9em;">WebApp</span></span> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square" style="font-size: 0.7em;"></i> | |
| </a> | |
| <button class="action-btn" id="toggle-terminal-btn" title="Toggle Settings Panel"> | |
| <i class="fa-solid fa-sliders"></i> | |
| </button> | |
| <button class="action-btn hidden" id="toggle-sidebar-btn" title="Toggle History"> | |
| <i class="fa-solid fa-bars"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <div class="main-container"> | |
| <!-- Mobile Overlay --> | |
| <div class="mobile-overlay" id="mobile-overlay"></div> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <button class="btn-new-chat" onclick="createNewConversation()"> | |
| <i class="fa-solid fa-plus"></i> New Conversation | |
| </button> | |
| </div> | |
| <div class="history-list" id="history-list"> | |
| <!-- History Items Injected via JS --> | |
| </div> | |
| <div class="sidebar-footer"> | |
| <button class="btn-import" onclick="importConversation()"> | |
| <i class="fa-solid fa-file-import"></i> Import JSON | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Main Chat Area --> | |
| <main class="chat-area"> | |
| <div class="chat-header"> | |
| <div class="model-indicator"> | |
| <i class="fa-solid fa-microchip" style="color: var(--primary);"></i> | |
| <span id="current-model-display">Llama-3-8B</span> | |
| <span class="badge" id="focus-mode-badge">Focus: OFF</span> | |
| </div> | |
| <div> | |
| <button class="action-btn" onclick="toggleFocusMode()" title="Toggle Focus Mode"> | |
| <i class="fa-solid fa-crosshairs"></i> Focus | |
| </button> | |
| </div> | |
| </div> | |
| <div class="chat-messages" id="chat-messages"> | |
| <!-- Messages Injected via JS --> | |
| </div> | |
| <div class="input-area"> | |
| <div class="input-wrapper"> | |
| <textarea class="chat-input" id="user-input" placeholder="Type a message... (Shift+Enter for new line)"></textarea> | |
| <button class="send-btn" onclick="sendMessage()"><i class="fa-solid fa-paper-plane"></i></button> | |
| </div> | |
| <!-- Editable Suggestions Area --> | |
| <div class="mt-2 text-xs text-muted flex justify-between" style="margin-top:8px;"> | |
| <span>Suggestions:</span> | |
| <div id="suggestions" class="flex gap-2"> | |
| <span class="cursor-pointer hover:text-primary" onclick="fillInput('Explain quantum entanglement')">Explain quantum entanglement</span> | |
| <span class="cursor-pointer hover:text-primary" onclick="fillInput('Generate a Python class for User')">Generate Python Class</span> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Right Panel (Models & Tools) --> | |
| <aside class="right-panel" id="right-panel"> | |
| <!-- Model Showcase --> | |
| <div class="panel-section"> | |
| <div class="panel-title"><i class="fa-solid fa-layer-group"></i> Model Showcase</div> | |
| <div id="model-list"> | |
| <!-- Models injected JS --> | |
| </div> | |
| </div> | |
| <!-- Configuration & Downloads --> | |
| <div class="panel-section"> | |
| <div class="panel-title"><i class="fa-solid fa-gears"></i> Configuration & API</div> | |
| <div class="form-group"> | |
| <label>WebApp Folder Path (:/)</label> | |
| <input type="text" class="form-input" value=":/app/models" id="app-path"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Download Destination</label> | |
| <input type="text" class="form-input" value="~/Downloads/" id="dl-path"> | |
| </div> | |
| <button class="btn-download" onclick="startDownloadProcess()"> | |
| <i class="fa-solid fa-cloud-arrow-down"></i> Fetch & Download Models | |
| </button> | |
| <button class="btn-secondary" style="margin-top:8px;" onclick="simulateAutoConfig()"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> Auto Config App | |
| </button> | |
| </div> | |
| <!-- Simulated Terminal --> | |
| <div class="panel-section"> | |
| <div class="panel-title"> | |
| <i class="fa-solid fa-terminal"></i> System Terminal (Bash) | |
| <span style="margin-left:auto; font-size:0.6em; cursor:pointer;" onclick="clearTerminal()">Clear</span> | |
| </div> | |
| <div class="terminal" id="terminal-output"> | |
| <div class="terminal-line info">> System initialized...</div> | |
| <div class="terminal-line info">> Ready for commands.</div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div class="toast" id="toast"> | |
| <i class="fa-solid fa-circle-check" style="color:var(--primary)"></i> | |
| <span id="toast-msg">Operation Successful</span> | |
| </div> | |
| <script> | |
| // --- STATE MANAGEMENT --- | |
| const state = { | |
| conversations: [], | |
| currentConversationId: null, | |
| models: [ | |
| { id: 'llama-3', name: 'Llama-3-8B', type: 'Instruct', size: '4.7GB', desc: 'General purpose assistant.' }, | |
| { id: 'mistral', name: 'Mistral-7B', type: 'Creative', size: '4.1GB', desc: 'Good for creative writing.' }, | |
| { id: 'falcon', name: 'Falcon-40B', type: 'Logic', size: '80GB', desc: 'Advanced reasoning.' }, | |
| { id: 'bert', name: 'BERT-Large', type: 'NLP', size: '1.2GB', desc: 'Text classification & embeddings.' } | |
| ], | |
| currentModel: 'llama-3', | |
| focusMode: false, | |
| appPath: ':/app/models', | |
| dlPath: '~/Downloads/' | |
| }; | |
| // --- INITIALIZATION --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadFromStorage(); | |
| if (state.conversations.length === 0) { | |
| createNewConversation(true); | |
| } else { | |
| loadConversation(state.conversations[0].id); | |
| } | |
| renderModelList(); | |
| renderHistoryList(); | |
| checkResponsive(); | |
| // Auto-refresh simulation (AJAX polling visual) | |
| setInterval(() => { | |
| if(Math.random() > 0.95) { | |
| logTerminal(`> Checking HuggingFace API for updates...`, 'info'); | |
| } | |
| }, 15000); | |
| // Textarea auto-resize | |
| const ta = document.getElementById('user-input'); | |
| ta.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = (this.scrollHeight) + 'px'; | |
| }); | |
| // Handle Enter key | |
| ta.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', checkResponsive); | |
| // Button Listeners | |
| document.getElementById('toggle-terminal-btn').addEventListener('click', () => { | |
| const panel = document.getElementById('right-panel'); | |
| if (window.innerWidth <= 1024) { | |
| panel.classList.toggle('open'); | |
| } else { | |
| if (panel.style.width === '0px') { | |
| panel.style.width = '320px'; panel.style.opacity = '1'; panel.style.padding = '0'; | |
| } else { | |
| panel.style.width = '0px'; panel.style.opacity = '0'; panel.style.overflow = 'hidden'; panel.style.padding = '0'; | |
| } | |
| } | |
| }); | |
| document.getElementById('toggle-sidebar-btn').addEventListener('click', () => { | |
| document.getElementById('sidebar').classList.toggle('open'); | |
| document.getElementById('mobile-overlay').classList.toggle('active'); | |
| }); | |
| document.getElementById('mobile-overlay').addEventListener('click', () => { | |
| document.getElementById('sidebar').classList.remove('open'); | |
| document.getElementById('mobile-overlay').classList.remove('active'); | |
| }); | |
| }); | |
| function checkResponsive() { | |
| const toggleBtn = document.getElementById('toggle-sidebar-btn'); | |
| if (window.innerWidth <= 768) { | |
| toggleBtn.classList.remove('hidden'); | |
| } else { | |
| toggleBtn.classList.add('hidden'); | |
| document.getElementById('sidebar').classList.remove('open'); | |
| document.getElementById('mobile-overlay').classList.remove('active'); | |
| } | |
| } | |
| // --- CORE LOGIC --- | |
| function createNewConversation(isFirst = false) { | |
| const newId = Date.now().toString(); | |
| const currentModelObj = state.models.find(m => m.id === state.currentModel); | |
| const newConv = { | |
| id: newId, | |
| name: isFirst ? 'New Conversation' : `Conversation ${state.conversations.length + 1}`, | |
| model: state.currentModel, | |
| messages: [] | |
| }; | |
| // Pre-suggestion based on model | |
| if (currentModelObj) { | |
| newConv.messages.push({ | |
| role: 'system', | |
| content: `Switched to ${currentModelObj.name}. ${currentModelObj.desc} Suggestion: Try asking for a summary or creative story.` | |
| }); | |
| } | |
| state.conversations.unshift(newConv); | |
| state.currentConversationId = newId; | |
| saveToStorage(); | |
| renderHistoryList(); | |
| renderChat(); | |
| if(!isFirst) showToast("New Conversation Created"); | |
| // Close sidebar on mobile if open | |
| if(window.innerWidth <= 768) { | |
| document.getElementById('sidebar').classList.remove('open'); | |
| document.getElementById('mobile-overlay').classList.remove('active'); | |
| } | |
| } | |
| function loadConversation(id) { | |
| state.currentConversationId = id; | |
| const conv = state.conversations.find(c => c.id === id); | |
| if (conv) { | |
| // Update current model to match the conversation's model | |
| if (conv.model && state.models.find(m => m.id === conv.model)) { | |
| state.currentModel = conv.model; | |
| document.getElementById('current-model-display').innerText = state.models.find(m => m.id === conv.model).name; | |
| renderModelList(); | |
| } | |
| renderChat(); | |
| renderHistoryList(); | |
| } | |
| } | |
| function deleteConversation(id, e) { | |
| e.stopPropagation(); | |
| if(confirm("Delete this conversation?")) { | |
| state.conversations = state.conversations.filter(c => c.id !== id); | |
| if (state.conversations.length === 0) { | |
| createNewConversation(); | |
| } else { | |
| loadConversation(state.conversations[0].id); | |
| } | |
| saveToStorage(); | |
| renderHistoryList(); | |
| } | |
| } | |
| function updateConversationName(id, newName) { | |
| const conv = state.conversations.find(c => c.id === id); | |
| if (conv) { | |
| conv.name = newName; | |
| saveToStorage(); | |
| renderHistoryList(); | |
| } | |
| } | |
| function sendMessage() { | |
| const input = document.getElementById('user-input'); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| const conv = state.conversations.find(c => c.id === state.currentConversationId); | |
| if (!conv) return; | |
| // Add User Message | |
| conv.messages.push({ role: 'user', content: text }); | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| saveToStorage(); | |
| renderChat(); | |
| // Show typing indicator | |
| const typingId = 'typing-' + Date.now(); | |
| addTypingIndicator(typingId); | |
| // Simulate AI Response | |
| setTimeout(() => { | |
| removeTypingIndicator(typingId); | |
| const aiResponse = generateSimulatedResponse(text); | |
| conv.messages.push({ role: 'ai', content: aiResponse }); | |
| saveToStorage(); | |
| renderChat(); | |
| }, 1000 + Math.random() * 1000); | |
| } | |
| function switchModel(modelId) { | |
| state.currentModel = modelId; | |
| const model = state.models.find(m => m.id === modelId); | |
| // Update UI | |
| document.getElementById('current-model-display').innerText = model.name; | |
| renderModelList(); | |
| // Add intro suggestion to current chat | |
| const conv = state.conversations.find(c => c.id === state.currentConversationId); | |
| if (conv) { | |
| conv.model = modelId; | |
| conv.messages.push({ | |
| role: 'system', | |
| content: `Model switched to ${model.name}. Intro suggestion: This model is optimized for ${model.type} tasks.` | |
| }); | |
| saveToStorage(); | |
| renderChat(); | |
| } | |
| showToast(`Switched to ${model.name}`); | |
| } | |
| function toggleFocusMode() { | |
| state.focusMode = !state.focusMode; | |
| const badge = document.getElementById('focus-mode-badge'); | |
| const body = document.body; | |
| if (state.focusMode) { | |
| badge.classList.add('active'); | |
| badge.innerText = 'Focus: ON'; | |
| body.classList.add('focus-mode'); | |
| showToast("Focus Mode Enabled"); | |
| } else { | |
| badge.classList.remove('active'); | |
| badge.innerText = 'Focus: OFF'; | |
| body.classList.remove('focus-mode'); | |
| showToast("Focus Mode Disabled"); | |
| } | |
| } | |
| function importConversation() { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.accept = '.json'; | |
| input.onchange = e => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const imported = JSON.parse(e.target.result); | |
| // Ensure it has basic structure | |
| if(!imported.messages) imported.messages = []; | |
| if(!imported.name) imported.name = file.name; | |
| imported.id = Date.now().toString(); | |
| state.conversations.unshift(imported); | |
| saveToStorage(); | |
| renderHistoryList(); | |
| loadConversation(imported.id); | |
| showToast("Conversation Imported Successfully"); | |
| logTerminal(`> Imported ${file.name} to repository.`, 'info'); | |
| } catch (err) { | |
| showToast("Invalid JSON file"); | |
| logTerminal(`> Error importing file: ${err.message}`, 'error'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| } | |
| }; | |
| input.click(); | |
| } | |
| // --- RENDER FUNCTIONS --- | |
| function renderHistoryList() { | |
| const list = document.getElementById('history-list'); | |
| list.innerHTML = ''; | |
| state.conversations.forEach(conv => { | |
| const div = document.createElement('div'); | |
| div.className = `history-item ${conv.id === state.currentConversationId ? 'active' : ''}`; | |
| div.onclick = () => loadConversation(conv.id); | |
| div.innerHTML = ` | |
| <div class="history-name" contenteditable="true" onblur="updateConversationName('${conv.id}', this.innerText)">${conv.name}</div> | |
| <div class="history-actions"> | |
| <i class="fa-solid fa-trash" onclick="deleteConversation('${conv.id}', event)"></i> | |
| </div> | |
| `; | |
| list.appendChild(div); | |
| }); | |
| } | |
| function renderChat() { | |
| const container = document.getElementById('chat-messages'); | |
| const conv = state.conversations.find(c => c.id === state.currentConversationId); | |
| container.innerHTML = ''; | |
| if (!conv) return; | |
| conv.messages.forEach(msg => { | |
| const div = document.createElement('div'); | |
| div.className = `message ${msg.role}`; | |
| let contentHtml = escapeHtml(msg.content); | |
| // Simple code block formatting | |
| if(contentHtml.includes('```')) { | |
| contentHtml = contentHtml.replace(/```([\s\S]*?)```/g, '<div style="background:#000; padding:8px; border-radius:4px; margin:5px 0; font-family:monospace; overflow-x:auto;"><code>$1</code></div>'); | |
| } | |
| div.innerHTML = ` | |
| <div class="msg-actions"> | |
| <button class="action-btn" onclick="copyMessage(this)" title="Copy"> | |
| <i class="fa-regular fa-copy"></i> | |
| </button> | |
| <button class="action-btn" onclick="pasteToInput(this)" title="Paste to Input"> | |
| <i class="fa-solid fa-paste"></i> | |
| </button> | |
| </div> | |
| <div class="bubble">${contentHtml}</div> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| function renderModelList() { | |
| const list = document.getElementById('model-list'); | |
| list.innerHTML = ''; | |
| state.models.forEach(model => { | |
| const div = document.createElement('div'); | |
| div.className = `model-card ${model.id === state.currentModel ? 'active' : ''}`; | |
| div.onclick = () => switchModel(model.id); | |
| div.innerHTML = ` | |
| <div class="model-header"> | |
| <span class="model-name">${model.name}</span> | |
| <i class="fa-solid fa-check" style="opacity: ${model.id === state.currentModel ? 1 : 0}; color: var(--primary);"></i> | |
| </div |