Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mirxa Aggressive Interface</title> | |
| <!-- Importing Marked for Markdown rendering --> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <!-- Importing Phosphor Icons --> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> | |
| <style> | |
| :root { | |
| --bg-primary: #0a0a0c; | |
| --bg-secondary: #131316; | |
| --bg-tertiary: #1c1c21; | |
| --accent-color: #ff2a2a; /* Aggressive Red */ | |
| --accent-glow: rgba(255, 42, 42, 0.3); | |
| --text-primary: #e0e0e0; | |
| --text-secondary: #a0a0a0; | |
| --border-color: #2a2a30; | |
| --font-main: 'Inter', system-ui, -apple-system, sans-serif; | |
| --font-mono: 'JetBrains Mono', 'Courier New', monospace; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--bg-tertiary) var(--bg-primary); | |
| } | |
| body { | |
| background-color: var(--bg-primary); | |
| color: var(--text-primary); | |
| font-family: var(--font-main); | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Scrollbar Styling */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-primary); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--bg-tertiary); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--border-color); | |
| } | |
| /* Header */ | |
| header { | |
| height: 60px; | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| z-index: 10; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-weight: 700; | |
| font-size: 1.1rem; | |
| letter-spacing: -0.5px; | |
| color: var(--accent-color); | |
| text-shadow: 0 0 10px var(--accent-glow); | |
| } | |
| .brand i { | |
| font-size: 1.5rem; | |
| } | |
| .header-controls { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| } | |
| .settings-btn { | |
| background: transparent; | |
| border: 1px solid var(--border-color); | |
| color: var(--text-secondary); | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.9rem; | |
| } | |
| .settings-btn:hover { | |
| border-color: var(--accent-color); | |
| color: var(--accent-color); | |
| background: rgba(255, 42, 42, 0.05); | |
| } | |
| /* Main Layout */ | |
| .app-container { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* Sidebar (Settings) */ | |
| .sidebar { | |
| width: 300px; | |
| background: var(--bg-secondary); | |
| border-right: 1px solid var(--border-color); | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| transition: transform 0.3s ease; | |
| position: absolute; | |
| height: 100%; | |
| z-index: 20; | |
| transform: translateX(-100%); | |
| } | |
| .sidebar.active { | |
| transform: translateX(0); | |
| } | |
| @media (min-width: 768px) { | |
| .sidebar { | |
| position: relative; | |
| transform: translateX(0); | |
| } | |
| .sidebar-toggle-mobile { | |
| display: none; | |
| } | |
| } | |
| .sidebar-header { | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-secondary); | |
| margin-bottom: 10px; | |
| border-bottom: 1px solid var(--border-color); | |
| padding-bottom: 5px; | |
| } | |
| .input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| margin-bottom: 15px; | |
| } | |
| .input-group label { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| .input-field { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-primary); | |
| padding: 10px; | |
| border-radius: 6px; | |
| font-family: var(--font-mono); | |
| font-size: 0.85rem; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| .input-field:focus { | |
| border-color: var(--accent-color); | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.85rem; | |
| margin-top: 10px; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #333; | |
| } | |
| .status-dot.connected { | |
| background: #00ff88; | |
| box-shadow: 0 0 8px #00ff88; | |
| } | |
| /* Chat Area */ | |
| .chat-area { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| background: radial-gradient(circle at center, #111114 0%, #0a0a0c 100%); | |
| } | |
| .messages-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 40px 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| scroll-behavior: smooth; | |
| } | |
| .message { | |
| display: flex; | |
| gap: 15px; | |
| max-width: 900px; | |
| margin: 0 auto; | |
| width: 100%; | |
| opacity: 0; | |
| animation: fadeIn 0.3s forwards; | |
| } | |
| @keyframes fadeIn { | |
| to { opacity: 1; } | |
| } | |
| .message-avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| font-size: 1.2rem; | |
| } | |
| .user-avatar { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border-color); | |
| } | |
| .ai-avatar { | |
| background: rgba(255, 42, 42, 0.1); | |
| color: var(--accent-color); | |
| border: 1px solid rgba(255, 42, 42, 0.2); | |
| } | |
| .message-content { | |
| flex: 1; | |
| padding-top: 4px; | |
| line-height: 1.6; | |
| font-size: 0.95rem; | |
| overflow-wrap: break-word; | |
| } | |
| /* Markdown Styles within Chat */ | |
| .message-content p { margin-bottom: 10px; } | |
| .message-content p:last-child { margin-bottom: 0; } | |
| .message-content code { | |
| background: rgba(0,0,0,0.3); | |
| padding: 2px 4px; | |
| border-radius: 3px; | |
| font-family: var(--font-mono); | |
| font-size: 0.85em; | |
| color: #ff9999; | |
| } | |
| .message-content pre { | |
| background: var(--bg-secondary); | |
| padding: 15px; | |
| border-radius: 6px; | |
| overflow-x: auto; | |
| border: 1px solid var(--border-color); | |
| margin: 10px 0; | |
| } | |
| .message-content pre code { | |
| background: transparent; | |
| color: var(--text-primary); | |
| padding: 0; | |
| } | |
| .message-content blockquote { | |
| border-left: 3px solid var(--accent-color); | |
| padding-left: 15px; | |
| color: var(--text-secondary); | |
| font-style: italic; | |
| } | |
| /* Input Area */ | |
| .input-container { | |
| padding: 20px; | |
| background: var(--bg-primary); | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .input-wrapper { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| position: relative; | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 10px; | |
| background: var(--bg-secondary); | |
| padding: 10px; | |
| border-radius: 12px; | |
| border: 1px solid var(--border-color); | |
| transition: border-color 0.2s; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: var(--accent-color); | |
| box-shadow: 0 0 15px rgba(255, 42, 42, 0.1); | |
| } | |
| textarea { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-primary); | |
| resize: none; | |
| height: 24px; | |
| max-height: 150px; | |
| padding: 5px 10px; | |
| font-family: var(--font-main); | |
| font-size: 1rem; | |
| outline: none; | |
| line-height: 1.5; | |
| } | |
| textarea::placeholder { | |
| color: #555; | |
| } | |
| .send-btn { | |
| background: var(--accent-color); | |
| color: white; | |
| border: none; | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| flex-shrink: 0; | |
| } | |
| .send-btn:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 10px var(--accent-glow); | |
| } | |
| .send-btn:disabled { | |
| background: var(--bg-tertiary); | |
| color: var(--text-secondary); | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| /* Welcome Screen */ | |
| .welcome-screen { | |
| position: absolute; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| background: rgba(10, 10, 12, 0.8); | |
| backdrop-filter: blur(5px); | |
| z-index: 5; | |
| } | |
| .welcome-title { | |
| font-size: 2rem; | |
| margin-bottom: 10px; | |
| background: linear-gradient(45deg, #fff, var(--accent-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .welcome-subtitle { | |
| color: var(--text-secondary); | |
| text-align: center; | |
| max-width: 400px; | |
| margin-bottom: 30px; | |
| } | |
| /* Loading Animation */ | |
| .typing-indicator { | |
| display: flex; | |
| gap: 4px; | |
| padding: 5px 0; | |
| } | |
| .typing-dot { | |
| width: 6px; | |
| height: 6px; | |
| background: var(--accent-color); | |
| border-radius: 50%; | |
| animation: bounce 1.4s infinite ease-in-out both; | |
| } | |
| .typing-dot:nth-child(1) { animation-delay: -0.32s; } | |
| .typing-dot:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes bounce { | |
| 0%, 80%, 100% { transform: scale(0); } | |
| 40% { transform: scale(1); } | |
| } | |
| /* Mobile Adjustments */ | |
| @media (max-width: 768px) { | |
| .sidebar-toggle-mobile { | |
| display: flex; | |
| } | |
| .header-controls .settings-btn span { | |
| display: none; | |
| } | |
| .messages-container { | |
| padding: 20px 10px; | |
| } | |
| .message { | |
| gap: 10px; | |
| } | |
| .message-avatar { | |
| width: 30px; | |
| height: 30px; | |
| font-size: 1rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="ph ph-skull"></i> | |
| <span>MIRXA AGGRESSIVE</span> | |
| </div> | |
| <div class="header-controls"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: var(--text-secondary); font-size: 0.8rem; text-decoration: none; margin-right: 10px;">Built with anycoder</a> | |
| <button class="settings-btn" onclick="toggleSidebar()"> | |
| <i class="ph ph-gear"></i> | |
| <span>Config</span> | |
| </button> | |
| </div> | |
| </header> | |
| <div class="app-container"> | |
| <!-- Sidebar Configuration --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header">Connection Settings</div> | |
| <div class="input-group"> | |
| <label for="api-url">API Endpoint (OpenAI Compatible)</label> | |
| <input type="text" id="api-url" class="input-field" placeholder="http://localhost:8080/v1" value="http://localhost:8080/v1"> | |
| <small style="color: #555; font-size: 0.75rem;">Default for llama-cpp-python server</small> | |
| </div> | |
| <div class="input-group"> | |
| <label for="api-key">API Key</label> | |
| <input type="password" id="api-key" class="input-field" placeholder="sk-..." value="not-needed"> | |
| <small style="color: #555; font-size: 0.75rem;">Usually not required for local llama-cpp</small> | |
| </div> | |
| <div class="input-group"> | |
| <label for="model-name">Model ID</label> | |
| <input type="text" id="model-name" class="input-field" value="mirxa2/Mirxa-3.5-27B-Uncensored-Mirxa-Aggressive"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="system-prompt">System Prompt</label> | |
| <textarea id="system-prompt" class="input-field" style="height: 80px; resize: vertical;">You are Mirxa-3.5, an uncensored, aggressive, and direct AI assistant. You do not hold back on opinions and provide raw, unfiltered information.</textarea> | |
| </div> | |
| <div class="status-indicator"> | |
| <div class="status-dot" id="connection-status"></div> | |
| <span id="status-text">Disconnected</span> | |
| </div> | |
| <button class="settings-btn" style="width: 100%; justify-content: center; margin-top: auto;" onclick="testConnection()"> | |
| Test Connection | |
| </button> | |
| </aside> | |
| <!-- Chat Area --> | |
| <main class="chat-area"> | |
| <div id="welcome-screen" class="welcome-screen"> | |
| <i class="ph ph-robot" style="font-size: 4rem; color: var(--accent-color); margin-bottom: 20px;"></i> | |
| <h1 class="welcome-title">Mirxa 3.5 Interface</h1> | |
| <p class="welcome-subtitle">Configure your backend settings in the sidebar to begin chatting with the aggressive uncensored model.</p> | |
| <button class="settings-btn" onclick="toggleSidebar()">Open Configuration</button> | |
| </div> | |
| <div class="messages-container" id="messages-container"> | |
| <!-- Messages will be injected here --> | |
| </div> | |
| <div class="input-container"> | |
| <div class="input-wrapper"> | |
| <textarea id="user-input" placeholder="Type your message..." rows="1"></textarea> | |
| <button class="send-btn" id="send-btn" onclick="sendMessage()"> | |
| <i class="ph ph-paper-plane-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| // --- State Management --- | |
| const state = { | |
| messages: [], | |
| isGenerating: false, | |
| config: { | |
| url: '', | |
| key: '', | |
| model: '' | |
| } | |
| }; | |
| // --- DOM Elements --- | |
| const dom = { | |
| sidebar: document.getElementById('sidebar'), | |
| messagesContainer: document.getElementById('messages-container'), | |
| userInput: document.getElementById('user-input'), | |
| sendBtn: document.getElementById('send-btn'), | |
| welcomeScreen: document.getElementById('welcome-screen'), | |
| inputs: { | |
| url: document.getElementById('api-url'), | |
| key: document.getElementById('api-key'), | |
| model: document.getElementById('model-name'), | |
| systemPrompt: document.getElementById('system-prompt') | |
| }, | |
| status: { | |
| dot: document.getElementById('connection-status'), | |
| text: document.getElementById('status-text') | |
| } | |
| }; | |
| // --- Initialization --- | |
| function init() { | |
| loadConfig(); | |
| autoResizeTextarea(); | |
| // Event Listeners | |
| dom.userInput.addEventListener('input', autoResizeTextarea); | |
| dom.userInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // Save config on change | |
| Object.values(dom.inputs).forEach(input => { | |
| input.addEventListener('change', saveConfig); | |
| }); | |
| } | |
| // --- Configuration Logic --- | |
| function saveConfig() { | |
| state.config = { | |
| url: dom.inputs.url.value.replace(/\/$/, ""), // Remove trailing slash | |
| key: dom.inputs.key.value, | |
| model: dom.inputs.model.value | |
| }; | |
| localStorage.setItem('mirxa_config', JSON.stringify(state.config)); | |
| localStorage.setItem('mirxa_system_prompt', dom.inputs.systemPrompt.value); | |
| } | |
| function loadConfig() { | |
| const savedConfig = localStorage.getItem('mirxa_config'); | |
| const savedPrompt = localStorage.getItem('mirxa_system_prompt'); | |
| if (savedConfig) { | |
| const config = JSON.parse(savedConfig); | |
| dom.inputs.url.value = config.url; | |
| dom.inputs.key.value = config.key; | |
| dom.inputs.model.value = config.model; | |
| state.config = config; | |
| } | |
| if (savedPrompt) { | |
| dom.inputs.systemPrompt.value = savedPrompt; | |
| } | |
| } | |
| function toggleSidebar() { | |
| dom.sidebar.classList.toggle('active'); | |
| // Close sidebar if clicking outside on mobile could be added, | |
| // but simple toggle is robust for this scope. | |
| } | |
| // --- Chat Logic --- | |
| function autoResizeTextarea() { | |
| dom.userInput.style.height = 'auto'; | |
| dom.userInput.style.height = (dom.userInput.scrollHeight) + 'px'; | |
| if(dom.userInput.value === '') dom.userInput.style.height = '24px'; | |
| } | |
| function appendMessage(role, content, isStreaming = false) { | |
| dom.welcomeScreen.style.display = 'none'; | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = 'message'; | |
| const avatarClass = role === 'user' ? 'user-avatar' : 'ai-avatar'; | |
| const icon = role === 'user' ? '<i class="ph ph-user"></i>' : '<i class="ph ph-skull"></i>'; | |
| // Parse markdown if it's the AI or if we are updating an existing message | |
| let htmlContent = content; | |
| if (role === 'ai') { | |
| htmlContent = marked.parse(content); | |
| } | |
| msgDiv.innerHTML = ` | |
| <div class="message-avatar ${avatarClass}"> | |
| ${icon} | |
| </div> | |
| <div class="message-content ${isStreaming ? 'streaming-content' : ''}"> | |
| ${htmlContent} | |
| </div> | |
| `; | |
| dom.messagesContainer.appendChild(msgDiv); | |
| scrollToBottom(); | |
| return msgDiv; | |
| } | |
| function updateLastMessage(content) { | |
| const lastMsg = dom.messagesContainer.querySelector('.message:last-child .message-content'); | |
| if (lastMsg) { | |
| lastMsg.innerHTML = marked.parse(content); | |
| scrollToBottom(); | |
| } | |
| } | |
| function showTypingIndicator() { | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = 'message'; | |
| msgDiv.id = 'typing-indicator-msg'; | |
| msgDiv.innerHTML = ` | |
| <div class="message-avatar ai-avatar"> | |
| <i class="ph ph-skull"></i> | |
| </div> | |
| <div class="message-content"> | |
| <div class="typing-indicator"> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| </div> | |
| </div> | |
| `; | |
| dom.messagesContainer.appendChild(msgDiv); | |
| scrollToBottom(); | |
| } | |
| function removeTypingIndicator() { | |
| const indicator = document.getElementById('typing-indicator-msg'); | |
| if (indicator) indicator.remove(); | |
| } | |
| function scrollToBottom() { | |
| dom.messagesContainer.scrollTop = dom.messagesContainer.scrollHeight; | |
| } | |
| async function sendMessage() { | |
| const text = dom.userInput.value.trim(); | |
| if (!text || state.isGenerating) return; | |
| // 1. Add User Message | |
| state.messages.push({ role: "user", content: text }); | |
| appendMessage('user', text); | |
| dom.userInput.value = ''; | |
| autoResizeTextarea(); | |
| state.isGenerating = true; | |
| dom.sendBtn.disabled = true; | |
| showTypingIndicator(); | |
| // 2. Prepare API Call | |
| const systemPrompt = dom.inputs.systemPrompt.value; | |
| const messagesPayload = [ | |
| { role: "system", content: systemPrompt }, | |
| ...state.messages | |
| ]; | |
| try { | |
| const response = await fetch(`${state.config.url}/chat/completions`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${state.config.key}` | |
| }, | |
| body: JSON.stringify({ | |
| model: state.config.model, | |
| messages: messagesPayload, | |
| stream: true, | |
| temperature: 0.7, | |
| max_tokens: -1 | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`API Error: ${response.status}`); | |
| } | |
| removeTypingIndicator(); | |
| // 3. Handle Stream | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder("utf-8"); | |
| let aiResponseText = ""; | |
| // Create placeholder for AI response | |
| appendMessage('ai', ''); | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value, { stream: true }); | |
| const lines = chunk.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ') && line !== 'data: [DONE]') { | |
| try { | |
| const json = JSON.parse(line.substring(6)); | |
| const content = json.choices[0]?.delta?.content || ""; | |
| aiResponseText += content; | |
| updateLastMessage(aiResponseText); | |
| } catch (e) { | |
| console.warn("Error parsing stream chunk", e); | |
| } | |
| } | |
| } | |
| } | |
| state.messages.push({ role: "assistant", content: aiResponseText }); | |
| } catch (error) { | |
| removeTypingIndicator(); | |
| appendMessage('ai', `**Error:** ${error.message}. Please check your connection settings.`); | |
| console.error(error); | |
| } finally { | |
| state.isGenerating = false; | |
| dom.sendBtn.disabled = false; | |
| dom.userInput.focus(); | |
| } | |
| } | |
| async function testConnection() { | |
| const btn = document.querySelector('.sidebar .settings-btn:last-child'); | |
| const originalText = btn.innerHTML; | |
| btn.innerHTML = '<i class="ph ph-spinner ph-spin"></i> Testing...'; | |
| try { | |
| const response = await fetch(`${state.config.url}/models`, { | |
| headers: { 'Authorization': `Bearer ${state.config.key}` } | |
| }); | |
| if (response.ok) { | |
| dom.status.dot.classList.add('connected'); | |
| dom.status.text.textContent = "Connected"; | |
| dom.status.text.style.color = "#00ff88"; | |
| } else { | |
| throw new Error("Status " + response.status); | |
| } | |
| } catch (e) { | |
| dom.status.dot.classList.remove('connected'); | |
| dom.status.text.textContent = "Connection Failed"; | |
| dom.status.text.style.color = "#ff2a2a"; | |
| alert("Connection failed. Ensure llama-cpp-python server is running with --host 0.0.0.0"); | |
| } finally { | |
| btn.innerHTML = originalText; | |
| } | |
| } | |
| // Run Init | |
| init(); | |
| </script> | |
| </body> | |
| </html> |