| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <meta name="description" content="Rox AI - Next-generation conversational AI interface"> |
| <meta name="theme-color" content="#667eea"> |
| <meta name="apple-mobile-web-app-capable" content="yes"> |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> |
| |
| <title>Rox AI | Intelligent Conversations</title> |
| |
| |
| <link rel="preconnect" href="https://Rox-Turbo-API.hf.space"> |
| <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;500&display=swap" rel="stylesheet"> |
| |
| <style> |
| :root { |
| |
| --primary-50: #eef2ff; |
| --primary-100: #e0e7ff; |
| --primary-200: #c7d2fe; |
| --primary-300: #a5b4fc; |
| --primary-400: #818cf8; |
| --primary-500: #667eea; |
| --primary-600: #5b67d6; |
| --primary-700: #4f46e5; |
| --primary-800: #3730a3; |
| --primary-900: #312e81; |
| |
| --accent-purple: #764ba2; |
| --accent-pink: #f093fb; |
| --accent-blue: #4facfe; |
| |
| |
| --gray-50: #f9fafb; |
| --gray-100: #f3f4f6; |
| --gray-200: #e5e7eb; |
| --gray-300: #d1d5db; |
| --gray-400: #9ca3af; |
| --gray-500: #6b7280; |
| --gray-600: #4b5563; |
| --gray-700: #374151; |
| --gray-800: #1f2937; |
| --gray-900: #111827; |
| |
| |
| --success: #10b981; |
| --warning: #f59e0b; |
| --error: #ef4444; |
| --info: #3b82f6; |
| |
| |
| --space-1: 0.25rem; |
| --space-2: 0.5rem; |
| --space-3: 0.75rem; |
| --space-4: 1rem; |
| --space-5: 1.25rem; |
| --space-6: 1.5rem; |
| --space-8: 2rem; |
| --space-10: 2.5rem; |
| --space-12: 3rem; |
| --space-16: 4rem; |
| |
| |
| --font-sans: 'Inter', system-ui, -apple-system, sans-serif; |
| --font-mono: 'JetBrains Mono', 'Fira Code', monospace; |
| |
| |
| --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); |
| --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); |
| --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); |
| --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); |
| --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); |
| --shadow-glow: 0 0 20px rgba(102, 126, 234, 0.5); |
| |
| |
| --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); |
| --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1); |
| --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); |
| --transition-spring: 500ms cubic-bezier(0.34, 1.56, 0.64, 1); |
| |
| |
| --radius-sm: 0.375rem; |
| --radius-md: 0.5rem; |
| --radius-lg: 0.75rem; |
| --radius-xl: 1rem; |
| --radius-2xl: 1.5rem; |
| --radius-full: 9999px; |
| |
| |
| --z-dropdown: 100; |
| --z-sticky: 200; |
| --z-modal: 300; |
| --z-tooltip: 400; |
| --z-toast: 500; |
| } |
| |
| |
| @media (prefers-color-scheme: dark) { |
| :root { |
| --bg-primary: var(--gray-900); |
| --bg-secondary: var(--gray-800); |
| --bg-tertiary: var(--gray-700); |
| --text-primary: var(--gray-50); |
| --text-secondary: var(--gray-300); |
| --text-tertiary: var(--gray-400); |
| --border-color: var(--gray-700); |
| } |
| } |
| |
| @media (prefers-color-scheme: light) { |
| :root { |
| --bg-primary: #ffffff; |
| --bg-secondary: var(--gray-50); |
| --bg-tertiary: var(--gray-100); |
| --text-primary: var(--gray-900); |
| --text-secondary: var(--gray-600); |
| --text-tertiary: var(--gray-400); |
| --border-color: var(--gray-200); |
| } |
| } |
| |
| |
| *, *::before, *::after { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| } |
| |
| html { |
| font-size: 16px; |
| -webkit-font-smoothing: antialiased; |
| -moz-osx-font-smoothing: grayscale; |
| text-rendering: optimizeLegibility; |
| scroll-behavior: smooth; |
| } |
| |
| body { |
| font-family: var(--font-sans); |
| background: linear-gradient(135deg, var(--primary-500) 0%, var(--accent-purple) 50%, var(--primary-700) 100%); |
| background-attachment: fixed; |
| min-height: 100vh; |
| color: var(--text-primary); |
| line-height: 1.6; |
| overflow-x: hidden; |
| } |
| |
| |
| .ambient-bg { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| pointer-events: none; |
| z-index: -1; |
| overflow: hidden; |
| } |
| |
| .ambient-bg::before, |
| .ambient-bg::after { |
| content: ''; |
| position: absolute; |
| border-radius: 50%; |
| filter: blur(80px); |
| opacity: 0.4; |
| animation: float 20s infinite ease-in-out; |
| } |
| |
| .ambient-bg::before { |
| width: 600px; |
| height: 600px; |
| background: var(--accent-pink); |
| top: -200px; |
| right: -100px; |
| animation-delay: 0s; |
| } |
| |
| .ambient-bg::after { |
| width: 500px; |
| height: 500px; |
| background: var(--accent-blue); |
| bottom: -150px; |
| left: -100px; |
| animation-delay: -10s; |
| } |
| |
| @keyframes float { |
| 0%, 100% { transform: translate(0, 0) scale(1); } |
| 33% { transform: translate(30px, -30px) scale(1.1); } |
| 66% { transform: translate(-20px, 20px) scale(0.9); } |
| } |
| |
| |
| .app-container { |
| display: grid; |
| grid-template-columns: 280px 1fr; |
| grid-template-rows: 1fr; |
| height: 100vh; |
| max-width: 1600px; |
| margin: 0 auto; |
| background: var(--bg-primary); |
| box-shadow: var(--shadow-2xl); |
| overflow: hidden; |
| } |
| |
| @media (max-width: 1024px) { |
| .app-container { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| |
| .sidebar { |
| background: var(--bg-secondary); |
| border-right: 1px solid var(--border-color); |
| display: flex; |
| flex-direction: column; |
| padding: var(--space-6); |
| gap: var(--space-6); |
| overflow-y: auto; |
| } |
| |
| @media (max-width: 1024px) { |
| .sidebar { |
| position: fixed; |
| left: -100%; |
| top: 0; |
| height: 100vh; |
| width: 280px; |
| z-index: var(--z-modal); |
| transition: left var(--transition-slow); |
| } |
| |
| .sidebar.open { |
| left: 0; |
| } |
| } |
| |
| .sidebar-header { |
| display: flex; |
| align-items: center; |
| gap: var(--space-3); |
| padding-bottom: var(--space-6); |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .logo { |
| width: 40px; |
| height: 40px; |
| background: linear-gradient(135deg, var(--primary-500), var(--accent-purple)); |
| border-radius: var(--radius-lg); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 1.25rem; |
| color: white; |
| box-shadow: var(--shadow-md); |
| } |
| |
| .brand-text h1 { |
| font-size: 1.25rem; |
| font-weight: 700; |
| color: var(--text-primary); |
| letter-spacing: -0.025em; |
| } |
| |
| .brand-text p { |
| font-size: 0.75rem; |
| color: var(--text-tertiary); |
| font-weight: 500; |
| } |
| |
| |
| .btn-new-chat { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: var(--space-2); |
| padding: var(--space-3) var(--space-4); |
| background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); |
| color: white; |
| border: none; |
| border-radius: var(--radius-lg); |
| font-size: 0.875rem; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all var(--transition-base); |
| box-shadow: var(--shadow-md); |
| } |
| |
| .btn-new-chat:hover { |
| transform: translateY(-2px); |
| box-shadow: var(--shadow-lg), var(--shadow-glow); |
| } |
| |
| .btn-new-chat:active { |
| transform: translateY(0); |
| } |
| |
| |
| .history-section { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-2); |
| } |
| |
| .section-title { |
| font-size: 0.75rem; |
| font-weight: 600; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| color: var(--text-tertiary); |
| padding: 0 var(--space-2); |
| } |
| |
| .conversation-list { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-1); |
| } |
| |
| .conversation-item { |
| display: flex; |
| align-items: center; |
| gap: var(--space-3); |
| padding: var(--space-3) var(--space-4); |
| border-radius: var(--radius-lg); |
| cursor: pointer; |
| transition: all var(--transition-fast); |
| color: var(--text-secondary); |
| font-size: 0.875rem; |
| font-weight: 500; |
| } |
| |
| .conversation-item:hover { |
| background: var(--bg-tertiary); |
| color: var(--text-primary); |
| } |
| |
| .conversation-item.active { |
| background: var(--primary-50); |
| color: var(--primary-700); |
| } |
| |
| .conversation-item svg { |
| width: 18px; |
| height: 18px; |
| flex-shrink: 0; |
| } |
| |
| |
| .settings-panel { |
| border-top: 1px solid var(--border-color); |
| padding-top: var(--space-6); |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-4); |
| } |
| |
| .setting-group { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-2); |
| } |
| |
| .setting-label { |
| font-size: 0.75rem; |
| font-weight: 600; |
| color: var(--text-tertiary); |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| |
| .model-selector { |
| position: relative; |
| } |
| |
| .model-select { |
| width: 100%; |
| padding: var(--space-3) var(--space-4); |
| padding-right: var(--space-10); |
| background: var(--bg-tertiary); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius-lg); |
| font-size: 0.875rem; |
| color: var(--text-primary); |
| cursor: pointer; |
| appearance: none; |
| transition: all var(--transition-fast); |
| } |
| |
| .model-select:hover { |
| border-color: var(--primary-300); |
| } |
| |
| .model-select:focus { |
| outline: none; |
| border-color: var(--primary-500); |
| box-shadow: 0 0 0 3px var(--primary-100); |
| } |
| |
| .model-selector::after { |
| content: '▼'; |
| position: absolute; |
| right: var(--space-4); |
| top: 50%; |
| transform: translateY(-50%); |
| font-size: 0.625rem; |
| color: var(--text-tertiary); |
| pointer-events: none; |
| } |
| |
| |
| .toggle-group { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| } |
| |
| .toggle-label { |
| font-size: 0.875rem; |
| color: var(--text-secondary); |
| font-weight: 500; |
| } |
| |
| .toggle-switch { |
| position: relative; |
| width: 44px; |
| height: 24px; |
| background: var(--gray-300); |
| border-radius: var(--radius-full); |
| cursor: pointer; |
| transition: background var(--transition-fast); |
| } |
| |
| .toggle-switch.active { |
| background: var(--primary-500); |
| } |
| |
| .toggle-switch::after { |
| content: ''; |
| position: absolute; |
| top: 2px; |
| left: 2px; |
| width: 20px; |
| height: 20px; |
| background: white; |
| border-radius: 50%; |
| transition: transform var(--transition-spring); |
| box-shadow: var(--shadow-sm); |
| } |
| |
| .toggle-switch.active::after { |
| transform: translateX(20px); |
| } |
| |
| |
| .main-content { |
| display: flex; |
| flex-direction: column; |
| height: 100vh; |
| position: relative; |
| } |
| |
| |
| .mobile-header { |
| display: none; |
| align-items: center; |
| justify-content: space-between; |
| padding: var(--space-4); |
| background: var(--bg-primary); |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| @media (max-width: 1024px) { |
| .mobile-header { |
| display: flex; |
| } |
| } |
| |
| .menu-btn { |
| width: 40px; |
| height: 40px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: var(--bg-secondary); |
| border: none; |
| border-radius: var(--radius-lg); |
| cursor: pointer; |
| color: var(--text-secondary); |
| transition: all var(--transition-fast); |
| } |
| |
| .menu-btn:hover { |
| background: var(--bg-tertiary); |
| color: var(--text-primary); |
| } |
| |
| |
| .chat-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: var(--space-4) var(--space-6); |
| background: var(--bg-primary); |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .chat-info { |
| display: flex; |
| align-items: center; |
| gap: var(--space-3); |
| } |
| |
| .model-badge { |
| display: inline-flex; |
| align-items: center; |
| gap: var(--space-2); |
| padding: var(--space-1) var(--space-3); |
| background: var(--primary-50); |
| color: var(--primary-700); |
| font-size: 0.75rem; |
| font-weight: 600; |
| border-radius: var(--radius-full); |
| border: 1px solid var(--primary-200); |
| } |
| |
| .status-indicator { |
| width: 8px; |
| height: 8px; |
| background: var(--success); |
| border-radius: 50%; |
| animation: pulse 2s infinite; |
| } |
| |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| } |
| |
| .header-actions { |
| display: flex; |
| gap: var(--space-2); |
| } |
| |
| .icon-btn { |
| width: 36px; |
| height: 36px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: transparent; |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius-lg); |
| cursor: pointer; |
| color: var(--text-secondary); |
| transition: all var(--transition-fast); |
| } |
| |
| .icon-btn:hover { |
| background: var(--bg-secondary); |
| color: var(--text-primary); |
| border-color: var(--primary-300); |
| } |
| |
| |
| .messages-container { |
| flex: 1; |
| overflow-y: auto; |
| padding: var(--space-6); |
| scroll-behavior: smooth; |
| } |
| |
| .messages-container::-webkit-scrollbar { |
| width: 8px; |
| } |
| |
| .messages-container::-webkit-scrollbar-track { |
| background: transparent; |
| } |
| |
| .messages-container::-webkit-scrollbar-thumb { |
| background: var(--gray-300); |
| border-radius: var(--radius-full); |
| } |
| |
| .messages-container::-webkit-scrollbar-thumb:hover { |
| background: var(--gray-400); |
| } |
| |
| .welcome-screen { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| height: 100%; |
| text-align: center; |
| gap: var(--space-6); |
| padding: var(--space-8); |
| } |
| |
| .welcome-icon { |
| width: 80px; |
| height: 80px; |
| background: linear-gradient(135deg, var(--primary-500), var(--accent-purple)); |
| border-radius: var(--radius-2xl); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 2.5rem; |
| box-shadow: var(--shadow-xl); |
| animation: bounce-in var(--transition-spring); |
| } |
| |
| @keyframes bounce-in { |
| 0% { transform: scale(0); opacity: 0; } |
| 50% { transform: scale(1.1); } |
| 100% { transform: scale(1); opacity: 1; } |
| } |
| |
| .welcome-title { |
| font-size: 2rem; |
| font-weight: 700; |
| color: var(--text-primary); |
| letter-spacing: -0.025em; |
| } |
| |
| .welcome-subtitle { |
| font-size: 1rem; |
| color: var(--text-secondary); |
| max-width: 400px; |
| } |
| |
| .suggestion-chips { |
| display: flex; |
| flex-wrap: wrap; |
| gap: var(--space-3); |
| justify-content: center; |
| margin-top: var(--space-4); |
| } |
| |
| .suggestion-chip { |
| padding: var(--space-3) var(--space-5); |
| background: var(--bg-secondary); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius-full); |
| font-size: 0.875rem; |
| color: var(--text-secondary); |
| cursor: pointer; |
| transition: all var(--transition-fast); |
| } |
| |
| .suggestion-chip:hover { |
| background: var(--primary-50); |
| border-color: var(--primary-300); |
| color: var(--primary-700); |
| transform: translateY(-2px); |
| } |
| |
| |
| .message { |
| display: flex; |
| gap: var(--space-4); |
| margin-bottom: var(--space-6); |
| animation: message-in var(--transition-base); |
| max-width: 100%; |
| } |
| |
| @keyframes message-in { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .message.user { |
| flex-direction: row-reverse; |
| } |
| |
| .message-avatar { |
| width: 36px; |
| height: 36px; |
| border-radius: var(--radius-lg); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| flex-shrink: 0; |
| font-size: 1rem; |
| } |
| |
| .message.user .message-avatar { |
| background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); |
| color: white; |
| } |
| |
| .message.assistant .message-avatar { |
| background: var(--bg-tertiary); |
| color: var(--text-secondary); |
| } |
| |
| .message-content-wrapper { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-2); |
| max-width: calc(100% - 60px); |
| } |
| |
| .message.user .message-content-wrapper { |
| align-items: flex-end; |
| } |
| |
| .message-header { |
| display: flex; |
| align-items: center; |
| gap: var(--space-2); |
| font-size: 0.75rem; |
| color: var(--text-tertiary); |
| } |
| |
| .message-content { |
| padding: var(--space-4) var(--space-5); |
| border-radius: var(--radius-xl); |
| font-size: 0.9375rem; |
| line-height: 1.7; |
| word-wrap: break-word; |
| position: relative; |
| } |
| |
| .message.user .message-content { |
| background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); |
| color: white; |
| border-bottom-right-radius: var(--radius-sm); |
| box-shadow: var(--shadow-md); |
| } |
| |
| .message.assistant .message-content { |
| background: var(--bg-secondary); |
| color: var(--text-primary); |
| border: 1px solid var(--border-color); |
| border-bottom-left-radius: var(--radius-sm); |
| } |
| |
| |
| .code-block { |
| background: var(--gray-900); |
| border-radius: var(--radius-lg); |
| margin: var(--space-4) 0; |
| overflow: hidden; |
| } |
| |
| .code-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: var(--space-3) var(--space-4); |
| background: var(--gray-800); |
| border-bottom: 1px solid var(--gray-700); |
| } |
| |
| .code-language { |
| font-size: 0.75rem; |
| font-weight: 600; |
| color: var(--gray-400); |
| text-transform: uppercase; |
| } |
| |
| .code-actions { |
| display: flex; |
| gap: var(--space-2); |
| } |
| |
| .code-btn { |
| padding: var(--space-1) var(--space-3); |
| background: var(--gray-700); |
| border: none; |
| border-radius: var(--radius-md); |
| font-size: 0.75rem; |
| color: var(--gray-300); |
| cursor: pointer; |
| transition: all var(--transition-fast); |
| font-family: var(--font-sans); |
| } |
| |
| .code-btn:hover { |
| background: var(--gray-600); |
| color: white; |
| } |
| |
| .code-content { |
| padding: var(--space-4); |
| overflow-x: auto; |
| font-family: var(--font-mono); |
| font-size: 0.875rem; |
| line-height: 1.6; |
| color: #e5e7eb; |
| } |
| |
| .code-content pre { |
| margin: 0; |
| } |
| |
| |
| code { |
| font-family: var(--font-mono); |
| font-size: 0.875em; |
| background: var(--bg-tertiary); |
| padding: 0.2em 0.4em; |
| border-radius: var(--radius-md); |
| color: var(--primary-600); |
| } |
| |
| .message.user code { |
| background: rgba(255, 255, 255, 0.2); |
| color: white; |
| } |
| |
| |
| .typing-indicator { |
| display: flex; |
| gap: var(--space-4); |
| margin-bottom: var(--space-6); |
| opacity: 0; |
| transition: opacity var(--transition-fast); |
| } |
| |
| .typing-indicator.visible { |
| opacity: 1; |
| } |
| |
| .typing-bubbles { |
| display: flex; |
| align-items: center; |
| gap: 4px; |
| padding: var(--space-4) var(--space-5); |
| background: var(--bg-secondary); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius-xl); |
| border-bottom-left-radius: var(--radius-sm); |
| } |
| |
| .typing-bubble { |
| width: 8px; |
| height: 8px; |
| background: var(--primary-400); |
| border-radius: 50%; |
| animation: typing-bounce 1.4s infinite ease-in-out both; |
| } |
| |
| .typing-bubble:nth-child(1) { animation-delay: -0.32s; } |
| .typing-bubble:nth-child(2) { animation-delay: -0.16s; } |
| |
| @keyframes typing-bounce { |
| 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; } |
| 40% { transform: scale(1); opacity: 1; } |
| } |
| |
| |
| .input-container { |
| padding: var(--space-4) var(--space-6); |
| background: var(--bg-primary); |
| border-top: 1px solid var(--border-color); |
| } |
| |
| .input-wrapper { |
| display: flex; |
| gap: var(--space-3); |
| align-items: flex-end; |
| background: var(--bg-secondary); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius-xl); |
| padding: var(--space-3); |
| transition: all var(--transition-fast); |
| } |
| |
| .input-wrapper:focus-within { |
| border-color: var(--primary-300); |
| box-shadow: 0 0 0 3px var(--primary-100); |
| background: var(--bg-primary); |
| } |
| |
| .input-actions { |
| display: flex; |
| gap: var(--space-2); |
| padding-bottom: var(--space-1); |
| } |
| |
| .input-btn { |
| width: 32px; |
| height: 32px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: transparent; |
| border: none; |
| border-radius: var(--radius-lg); |
| cursor: pointer; |
| color: var(--text-tertiary); |
| transition: all var(--transition-fast); |
| flex-shrink: 0; |
| } |
| |
| .input-btn:hover { |
| background: var(--bg-tertiary); |
| color: var(--text-primary); |
| } |
| |
| .message-input { |
| flex: 1; |
| background: transparent; |
| border: none; |
| resize: none; |
| font-family: var(--font-sans); |
| font-size: 0.9375rem; |
| line-height: 1.6; |
| color: var(--text-primary); |
| max-height: 200px; |
| min-height: 24px; |
| padding: var(--space-1) 0; |
| } |
| |
| .message-input:focus { |
| outline: none; |
| } |
| |
| .message-input::placeholder { |
| color: var(--text-tertiary); |
| } |
| |
| .send-btn { |
| width: 36px; |
| height: 36px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); |
| border: none; |
| border-radius: var(--radius-lg); |
| cursor: pointer; |
| color: white; |
| transition: all var(--transition-fast); |
| flex-shrink: 0; |
| box-shadow: var(--shadow-md); |
| } |
| |
| .send-btn:hover:not(:disabled) { |
| transform: scale(1.05); |
| box-shadow: var(--shadow-lg); |
| } |
| |
| .send-btn:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .input-footer { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-top: var(--space-2); |
| padding: 0 var(--space-2); |
| font-size: 0.75rem; |
| color: var(--text-tertiary); |
| } |
| |
| .shortcut-hint { |
| display: flex; |
| gap: var(--space-3); |
| } |
| |
| kbd { |
| font-family: var(--font-mono); |
| padding: 0.125rem 0.375rem; |
| background: var(--bg-tertiary); |
| border-radius: var(--radius-md); |
| font-size: 0.75rem; |
| border: 1px solid var(--border-color); |
| } |
| |
| |
| .toast-container { |
| position: fixed; |
| bottom: var(--space-6); |
| right: var(--space-6); |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-3); |
| z-index: var(--z-toast); |
| pointer-events: none; |
| } |
| |
| .toast { |
| display: flex; |
| align-items: center; |
| gap: var(--space-3); |
| padding: var(--space-4) var(--space-5); |
| background: var(--gray-800); |
| color: white; |
| border-radius: var(--radius-lg); |
| box-shadow: var(--shadow-xl); |
| font-size: 0.875rem; |
| font-weight: 500; |
| animation: toast-in var(--transition-spring); |
| pointer-events: auto; |
| } |
| |
| @keyframes toast-in { |
| from { |
| transform: translateX(100%); |
| opacity: 0; |
| } |
| to { |
| transform: translateX(0); |
| opacity: 1; |
| } |
| } |
| |
| .toast.success { border-left: 4px solid var(--success); } |
| .toast.error { border-left: 4px solid var(--error); } |
| .toast.info { border-left: 4px solid var(--info); } |
| |
| |
| .sidebar-overlay { |
| display: none; |
| position: fixed; |
| inset: 0; |
| background: rgba(0, 0, 0, 0.5); |
| z-index: calc(var(--z-modal) - 1); |
| backdrop-filter: blur(4px); |
| } |
| |
| @media (max-width: 1024px) { |
| .sidebar-overlay.visible { |
| display: block; |
| } |
| } |
| |
| |
| .empty-state { |
| text-align: center; |
| padding: var(--space-12); |
| color: var(--text-tertiary); |
| } |
| |
| |
| .skeleton { |
| background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-secondary) 50%, var(--bg-tertiary) 75%); |
| background-size: 200% 100%; |
| animation: shimmer 1.5s infinite; |
| border-radius: var(--radius-md); |
| } |
| |
| @keyframes shimmer { |
| 0% { background-position: 200% 0; } |
| 100% { background-position: -200% 0; } |
| } |
| |
| |
| @media (max-width: 640px) { |
| .messages-container { |
| padding: var(--space-4); |
| } |
| |
| .message-content { |
| padding: var(--space-3) var(--space-4); |
| font-size: 0.875rem; |
| } |
| |
| .welcome-title { |
| font-size: 1.5rem; |
| } |
| |
| .input-container { |
| padding: var(--space-3) var(--space-4); |
| } |
| } |
| |
| |
| @media print { |
| .sidebar, .input-container, .chat-header { |
| display: none; |
| } |
| |
| .messages-container { |
| overflow: visible; |
| } |
| } |
| |
| |
| @media (prefers-reduced-motion: reduce) { |
| *, *::before, *::after { |
| animation-duration: 0.01ms !important; |
| animation-iteration-count: 1 !important; |
| transition-duration: 0.01ms !important; |
| } |
| } |
| |
| |
| :focus-visible { |
| outline: 2px solid var(--primary-500); |
| outline-offset: 2px; |
| } |
| |
| |
| ::selection { |
| background: var(--primary-200); |
| color: var(--primary-900); |
| } |
| </style> |
| </head> |
| <body> |
| <div class="ambient-bg"></div> |
| |
| <div class="app-container"> |
| |
| <aside class="sidebar" id="sidebar"> |
| <div class="sidebar-header"> |
| <div class="logo">🚀</div> |
| <div class="brand-text"> |
| <h1>Rox AI</h1> |
| <p>Next-Gen Intelligence</p> |
| </div> |
| </div> |
|
|
| <button class="btn-new-chat" onclick="chatApp.newConversation()"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <line x1="12" y1="5" x2="12" y2="19"></line> |
| <line x1="5" y1="12" x2="19" y2="12"></line> |
| </svg> |
| New Conversation |
| </button> |
|
|
| <div class="history-section"> |
| <span class="section-title">Recent Conversations</span> |
| <div class="conversation-list" id="conversationList"> |
| |
| </div> |
| </div> |
|
|
| <div class="settings-panel"> |
| <div class="setting-group"> |
| <span class="setting-label">Model</span> |
| <div class="model-selector"> |
| <select class="model-select" id="modelSelect"> |
| <option value="chat">Rox Core (Balanced)</option> |
| <option value="turbo">Rox Turbo (Fast)</option> |
| <option value="coder">Rox Coder (Code)</option> |
| <option value="turbo45">Rox 4.5 Turbo (Advanced)</option> |
| <option value="ultra">Rox Ultra (Powerful)</option> |
| <option value="dyno">Rox Dyno (Dynamic)</option> |
| <option value="coder7">Rox 7 Coder (Enterprise)</option> |
| <option value="vision">Rox Vision (Multimodal)</option> |
| </select> |
| </div> |
| </div> |
|
|
| <div class="setting-group"> |
| <div class="toggle-group"> |
| <span class="toggle-label">Stream Responses</span> |
| <div class="toggle-switch active" id="streamToggle" onclick="chatApp.toggleStreaming()"></div> |
| </div> |
| </div> |
|
|
| <div class="setting-group"> |
| <div class="toggle-group"> |
| <span class="toggle-label">Auto-scroll</span> |
| <div class="toggle-switch active" id="autoScrollToggle" onclick="chatApp.toggleAutoScroll()"></div> |
| </div> |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <main class="main-content"> |
| |
| <div class="mobile-header"> |
| <button class="menu-btn" onclick="chatApp.toggleSidebar()"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <line x1="3" y1="12" x2="21" y2="12"></line> |
| <line x1="3" y1="6" x2="21" y2="6"></line> |
| <line x1="3" y1="18" x2="21" y2="18"></line> |
| </svg> |
| </button> |
| <div class="chat-info"> |
| <span class="model-badge" id="mobileModelBadge"> |
| <span class="status-indicator"></span> |
| Rox Core |
| </span> |
| </div> |
| <button class="icon-btn" onclick="chatApp.newConversation()"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <line x1="12" y1="5" x2="12" y2="19"></line> |
| <line x1="5" y1="12" x2="19" y2="12"></line> |
| </svg> |
| </button> |
| </div> |
|
|
| |
| <div class="chat-header"> |
| <div class="chat-info"> |
| <span class="model-badge" id="modelBadge"> |
| <span class="status-indicator"></span> |
| <span id="currentModelName">Rox Core</span> |
| </span> |
| </div> |
| <div class="header-actions"> |
| <button class="icon-btn" onclick="chatApp.exportConversation()" title="Export"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> |
| <polyline points="7 10 12 15 17 10"></polyline> |
| <line x1="12" y1="15" x2="12" y2="3"></line> |
| </svg> |
| </button> |
| <button class="icon-btn" onclick="chatApp.clearConversation()" title="Clear"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="3 6 5 6 21 6"></polyline> |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> |
| </svg> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="messages-container" id="messagesContainer"> |
| <div class="welcome-screen" id="welcomeScreen"> |
| <div class="welcome-icon">🚀</div> |
| <h2 class="welcome-title">Welcome to Rox AI</h2> |
| <p class="welcome-subtitle">Experience next-generation AI conversations. Select a model and start chatting.</p> |
| <div class="suggestion-chips"> |
| <button class="suggestion-chip" onclick="chatApp.sendSuggestion('Explain quantum computing in simple terms')"> |
| Explain quantum computing |
| </button> |
| <button class="suggestion-chip" onclick="chatApp.sendSuggestion('Write a Python function to calculate fibonacci')"> |
| Write Python code |
| </button> |
| <button class="suggestion-chip" onclick="chatApp.sendSuggestion('Help me brainstorm ideas for a sci-fi novel')"> |
| Brainstorm ideas |
| </button> |
| <button class="suggestion-chip" onclick="chatApp.sendSuggestion('Analyze the implications of AI in healthcare')"> |
| AI analysis |
| </button> |
| </div> |
| </div> |
| <div id="messagesList" style="display: none;"></div> |
| </div> |
|
|
| |
| <div class="input-container"> |
| <div class="input-wrapper"> |
| <div class="input-actions"> |
| <button class="input-btn" title="Attach file"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path> |
| </svg> |
| </button> |
| </div> |
| <textarea |
| class="message-input" |
| id="messageInput" |
| placeholder="Message Rox AI..." |
| rows="1" |
| oninput="chatApp.autoResize(this)" |
| onkeydown="chatApp.handleKeydown(event)" |
| ></textarea> |
| <button class="send-btn" id="sendBtn" onclick="chatApp.sendMessage()"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <line x1="22" y1="2" x2="11" y2="13"></line> |
| <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> |
| </svg> |
| </button> |
| </div> |
| <div class="input-footer"> |
| <span>Rox AI can make mistakes. Consider checking important information.</span> |
| <div class="shortcut-hint"> |
| <span><kbd>Shift</kbd> + <kbd>Enter</kbd> for new line</span> |
| <span><kbd>Enter</kbd> to send</span> |
| </div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| |
| <div class="sidebar-overlay" id="sidebarOverlay" onclick="chatApp.toggleSidebar()"></div> |
|
|
| |
| <div class="toast-container" id="toastContainer"></div> |
|
|
| <script> |
| |
| |
| |
| |
| |
| class ChatApplication { |
| constructor() { |
| |
| this.config = { |
| apiBaseUrl: 'https://Rox-Turbo-API.hf.space', |
| maxRetries: 3, |
| retryDelay: 1000, |
| maxHistory: 50, |
| debounceMs: 150 |
| }; |
| |
| |
| this.state = { |
| currentConversationId: null, |
| conversations: new Map(), |
| isStreaming: true, |
| autoScroll: true, |
| isProcessing: false, |
| currentModel: 'chat', |
| sidebarOpen: false |
| }; |
| |
| |
| this.elements = { |
| messagesContainer: document.getElementById('messagesContainer'), |
| messagesList: document.getElementById('messagesList'), |
| welcomeScreen: document.getElementById('welcomeScreen'), |
| messageInput: document.getElementById('messageInput'), |
| sendBtn: document.getElementById('sendBtn'), |
| modelSelect: document.getElementById('modelSelect'), |
| streamToggle: document.getElementById('streamToggle'), |
| autoScrollToggle: document.getElementById('autoScrollToggle'), |
| conversationList: document.getElementById('conversationList'), |
| sidebar: document.getElementById('sidebar'), |
| sidebarOverlay: document.getElementById('sidebarOverlay'), |
| modelBadge: document.getElementById('modelBadge'), |
| currentModelName: document.getElementById('currentModelName'), |
| mobileModelBadge: document.getElementById('mobileModelBadge'), |
| toastContainer: document.getElementById('toastContainer') |
| }; |
| |
| |
| this.init(); |
| } |
| |
| init() { |
| this.loadConversations(); |
| this.setupEventListeners(); |
| this.renderConversationList(); |
| |
| |
| setTimeout(() => this.elements.messageInput.focus(), 100); |
| } |
| |
| setupEventListeners() { |
| |
| this.elements.modelSelect.addEventListener('change', (e) => { |
| this.state.currentModel = e.target.value; |
| this.updateModelDisplay(); |
| }); |
| |
| |
| window.addEventListener('resize', this.debounce(() => { |
| this.scrollToBottom(); |
| }, this.config.debounceMs)); |
| |
| |
| window.addEventListener('beforeunload', () => { |
| this.saveConversations(); |
| }); |
| } |
| |
| |
| |
| async sendMessage() { |
| if (this.state.isProcessing) return; |
| |
| const input = this.elements.messageInput; |
| const message = input.value.trim(); |
| |
| if (!message) return; |
| |
| |
| if (!this.state.currentConversationId) { |
| this.createNewConversation(); |
| } |
| |
| |
| this.addMessage('user', message); |
| |
| |
| input.value = ''; |
| input.style.height = 'auto'; |
| this.updateSendButton(); |
| |
| |
| const conversation = this.state.conversations.get(this.state.currentConversationId); |
| conversation.messages.push({ role: 'user', content: message, timestamp: Date.now() }); |
| conversation.title = this.generateTitle(conversation.messages); |
| |
| |
| this.showTypingIndicator(); |
| |
| |
| try { |
| if (this.state.isStreaming) { |
| await this.handleStreamingResponse(conversation); |
| } else { |
| await this.handleStandardResponse(conversation); |
| } |
| |
| this.saveConversations(); |
| this.renderConversationList(); |
| } catch (error) { |
| this.handleError(error); |
| } finally { |
| this.hideTypingIndicator(); |
| this.state.isProcessing = false; |
| this.updateSendButton(); |
| } |
| } |
| |
| async handleStreamingResponse(conversation) { |
| const apiUrl = `${this.config.apiBaseUrl}/${this.state.currentModel}`; |
| let retryCount = 0; |
| |
| while (retryCount < this.config.maxRetries) { |
| try { |
| const response = await fetch(apiUrl, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Accept': 'text/event-stream' |
| }, |
| body: JSON.stringify({ |
| messages: conversation.messages.slice(-this.config.maxHistory), |
| temperature: 0.7, |
| top_p: 0.95, |
| max_tokens: 8192, |
| stream: true |
| }) |
| }); |
| |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder(); |
| let fullContent = ''; |
| let messageElement = null; |
| |
| 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: ')) { |
| const data = line.slice(6).trim(); |
| if (data === '[DONE]') continue; |
| |
| try { |
| const parsed = JSON.parse(data); |
| if (parsed.content) { |
| if (!messageElement) { |
| messageElement = this.addMessage('assistant', ''); |
| } |
| fullContent += parsed.content; |
| this.updateMessageContent(messageElement, fullContent); |
| |
| if (this.state.autoScroll) { |
| this.scrollToBottom(); |
| } |
| } |
| } catch (e) { |
| |
| } |
| } |
| } |
| } |
| |
| if (fullContent) { |
| conversation.messages.push({ |
| role: 'assistant', |
| content: fullContent, |
| timestamp: Date.now() |
| }); |
| } |
| |
| return; |
| |
| } catch (error) { |
| retryCount++; |
| if (retryCount >= this.config.maxRetries) throw error; |
| await this.delay(this.config.retryDelay * retryCount); |
| } |
| } |
| } |
| |
| async handleStandardResponse(conversation) { |
| const apiUrl = `${this.config.apiBaseUrl}/${this.state.currentModel}`; |
| |
| const response = await fetch(apiUrl, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| messages: conversation.messages.slice(-this.config.maxHistory), |
| temperature: 0.7, |
| top_p: 0.95, |
| max_tokens: 8192, |
| stream: false |
| }) |
| }); |
| |
| if (!response.ok) { |
| const error = await response.json().catch(() => ({})); |
| throw new Error(error.detail || `HTTP ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| |
| if (data.content) { |
| this.addMessage('assistant', data.content); |
| conversation.messages.push({ |
| role: 'assistant', |
| content: data.content, |
| timestamp: Date.now() |
| }); |
| } |
| } |
| |
| |
| |
| addMessage(role, content) { |
| |
| this.elements.welcomeScreen.style.display = 'none'; |
| this.elements.messagesList.style.display = 'block'; |
| |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `message ${role}`; |
| |
| const avatar = document.createElement('div'); |
| avatar.className = 'message-avatar'; |
| avatar.textContent = role === 'user' ? '👤' : '🤖'; |
| |
| const wrapper = document.createElement('div'); |
| wrapper.className = 'message-content-wrapper'; |
| |
| const header = document.createElement('div'); |
| header.className = 'message-header'; |
| header.textContent = role === 'user' ? 'You' : 'Rox AI'; |
| |
| const time = document.createElement('span'); |
| time.textContent = '• ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); |
| header.appendChild(time); |
| |
| const contentDiv = document.createElement('div'); |
| contentDiv.className = 'message-content'; |
| |
| if (role === 'assistant') { |
| contentDiv.innerHTML = this.formatContent(content); |
| } else { |
| contentDiv.textContent = content; |
| } |
| |
| wrapper.appendChild(header); |
| wrapper.appendChild(contentDiv); |
| messageDiv.appendChild(avatar); |
| messageDiv.appendChild(wrapper); |
| |
| this.elements.messagesList.appendChild(messageDiv); |
| |
| if (this.state.autoScroll) { |
| this.scrollToBottom(); |
| } |
| |
| return contentDiv; |
| } |
| |
| updateMessageContent(element, content) { |
| element.innerHTML = this.formatContent(content); |
| } |
| |
| formatContent(content) { |
| |
| let formatted = content |
| .replace(/&/g, '&') |
| .replace(/</g, '<') |
| .replace(/>/g, '>'); |
| |
| |
| formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { |
| const language = lang || 'text'; |
| return ` |
| <div class="code-block"> |
| <div class="code-header"> |
| <span class="code-language">${language}</span> |
| <div class="code-actions"> |
| <button class="code-btn" onclick="chatApp.copyCode(this)">Copy</button> |
| </div> |
| </div> |
| <div class="code-content"><pre>${code.trim()}</pre></div> |
| </div> |
| `; |
| }); |
| |
| |
| formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>'); |
| |
| |
| formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); |
| |
| |
| formatted = formatted.replace(/\*(.*?)\*/g, '<em>$1</em>'); |
| |
| |
| formatted = formatted.replace(/\n/g, '<br>'); |
| |
| return formatted; |
| } |
| |
| showTypingIndicator() { |
| const indicator = document.createElement('div'); |
| indicator.className = 'typing-indicator visible'; |
| indicator.id = 'typingIndicator'; |
| indicator.innerHTML = ` |
| <div class="message-avatar">🤖</div> |
| <div class="typing-bubbles"> |
| <div class="typing-bubble"></div> |
| <div class="typing-bubble"></div> |
| <div class="typing-bubble"></div> |
| </div> |
| `; |
| this.elements.messagesList.appendChild(indicator); |
| this.scrollToBottom(); |
| } |
| |
| hideTypingIndicator() { |
| const indicator = document.getElementById('typingIndicator'); |
| if (indicator) indicator.remove(); |
| } |
| |
| |
| |
| createNewConversation() { |
| const id = 'conv_' + Date.now(); |
| const conversation = { |
| id, |
| title: 'New Conversation', |
| messages: [], |
| model: this.state.currentModel, |
| createdAt: Date.now(), |
| updatedAt: Date.now() |
| }; |
| |
| this.state.conversations.set(id, conversation); |
| this.state.currentConversationId = id; |
| this.renderConversationList(); |
| |
| return conversation; |
| } |
| |
| newConversation() { |
| this.state.currentConversationId = null; |
| this.elements.messagesList.innerHTML = ''; |
| this.elements.messagesList.style.display = 'none'; |
| this.elements.welcomeScreen.style.display = 'flex'; |
| this.elements.messageInput.value = ''; |
| this.elements.messageInput.style.height = 'auto'; |
| this.elements.messageInput.focus(); |
| |
| if (window.innerWidth <= 1024) { |
| this.toggleSidebar(); |
| } |
| } |
| |
| loadConversation(id) { |
| const conversation = this.state.conversations.get(id); |
| if (!conversation) return; |
| |
| this.state.currentConversationId = id; |
| this.state.currentModel = conversation.model || 'chat'; |
| this.elements.modelSelect.value = this.state.currentModel; |
| this.updateModelDisplay(); |
| |
| |
| this.elements.messagesList.innerHTML = ''; |
| this.elements.welcomeScreen.style.display = 'none'; |
| this.elements.messagesList.style.display = 'block'; |
| |
| conversation.messages.forEach(msg => { |
| if (msg.role === 'user') { |
| this.addMessage('user', msg.content); |
| } else { |
| const element = this.addMessage('assistant', ''); |
| this.updateMessageContent(element, msg.content); |
| } |
| }); |
| |
| this.scrollToBottom(); |
| |
| if (window.innerWidth <= 1024) { |
| this.toggleSidebar(); |
| } |
| } |
| |
| clearConversation() { |
| if (!this.state.currentConversationId) return; |
| |
| if (confirm('Clear all messages in this conversation?')) { |
| const conversation = this.state.conversations.get(this.state.currentConversationId); |
| conversation.messages = []; |
| this.elements.messagesList.innerHTML = ''; |
| this.saveConversations(); |
| } |
| } |
| |
| exportConversation() { |
| if (!this.state.currentConversationId) { |
| this.showToast('No conversation to export', 'error'); |
| return; |
| } |
| |
| const conversation = this.state.conversations.get(this.state.currentConversationId); |
| const exportData = { |
| title: conversation.title, |
| model: conversation.model, |
| exportedAt: new Date().toISOString(), |
| messages: conversation.messages |
| }; |
| |
| const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `rox-ai-conversation-${Date.now()}.json`; |
| a.click(); |
| URL.revokeObjectURL(url); |
| |
| this.showToast('Conversation exported', 'success'); |
| } |
| |
| |
| |
| toggleSidebar() { |
| this.state.sidebarOpen = !this.state.sidebarOpen; |
| this.elements.sidebar.classList.toggle('open', this.state.sidebarOpen); |
| this.elements.sidebarOverlay.classList.toggle('visible', this.state.sidebarOpen); |
| } |
| |
| toggleStreaming() { |
| this.state.isStreaming = !this.state.isStreaming; |
| this.elements.streamToggle.classList.toggle('active', this.state.isStreaming); |
| this.showToast(this.state.isStreaming ? 'Streaming enabled' : 'Streaming disabled', 'info'); |
| } |
| |
| toggleAutoScroll() { |
| this.state.autoScroll = !this.state.autoScroll; |
| this.elements.autoScrollToggle.classList.toggle('active', this.state.autoScroll); |
| } |
| |
| updateModelDisplay() { |
| const modelNames = { |
| 'chat': 'Rox Core', |
| 'turbo': 'Rox Turbo', |
| 'coder': 'Rox Coder', |
| 'turbo45': 'Rox 4.5 Turbo', |
| 'ultra': 'Rox Ultra', |
| 'dyno': 'Rox Dyno', |
| 'coder7': 'Rox 7 Coder', |
| 'vision': 'Rox Vision' |
| }; |
| |
| const name = modelNames[this.state.currentModel] || 'Rox Core'; |
| this.elements.currentModelName.textContent = name; |
| this.elements.mobileModelBadge.innerHTML = `<span class="status-indicator"></span>${name}`; |
| } |
| |
| updateSendButton() { |
| this.elements.sendBtn.disabled = this.state.isProcessing || !this.elements.messageInput.value.trim(); |
| } |
| |
| autoResize(textarea) { |
| textarea.style.height = 'auto'; |
| textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; |
| this.updateSendButton(); |
| } |
| |
| handleKeydown(event) { |
| if (event.key === 'Enter' && !event.shiftKey) { |
| event.preventDefault(); |
| this.sendMessage(); |
| } |
| } |
| |
| sendSuggestion(text) { |
| this.elements.messageInput.value = text; |
| this.autoResize(this.elements.messageInput); |
| this.sendMessage(); |
| } |
| |
| scrollToBottom() { |
| this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight; |
| } |
| |
| |
| |
| saveConversations() { |
| const data = Array.from(this.state.conversations.entries()); |
| localStorage.setItem('rox_ai_conversations', JSON.stringify(data)); |
| localStorage.setItem('rox_ai_current', this.state.currentConversationId || ''); |
| } |
| |
| loadConversations() { |
| try { |
| const saved = localStorage.getItem('rox_ai_conversations'); |
| const current = localStorage.getItem('rox_ai_current'); |
| |
| if (saved) { |
| const parsed = JSON.parse(saved); |
| this.state.conversations = new Map(parsed); |
| } |
| |
| if (current && this.state.conversations.has(current)) { |
| this.loadConversation(current); |
| } |
| } catch (e) { |
| console.error('Failed to load conversations:', e); |
| } |
| } |
| |
| renderConversationList() { |
| const list = this.elements.conversationList; |
| list.innerHTML = ''; |
| |
| const sorted = Array.from(this.state.conversations.values()) |
| .sort((a, b) => b.updatedAt - a.updatedAt) |
| .slice(0, 10); |
| |
| sorted.forEach(conv => { |
| const item = document.createElement('div'); |
| item.className = `conversation-item ${conv.id === this.state.currentConversationId ? 'active' : ''}`; |
| item.innerHTML = ` |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> |
| </svg> |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${conv.title}</span> |
| `; |
| item.onclick = () => this.loadConversation(conv.id); |
| list.appendChild(item); |
| }); |
| } |
| |
| generateTitle(messages) { |
| if (messages.length === 0) return 'New Conversation'; |
| const firstUser = messages.find(m => m.role === 'user'); |
| if (!firstUser) return 'New Conversation'; |
| return firstUser.content.slice(0, 30) + (firstUser.content.length > 30 ? '...' : ''); |
| } |
| |
| |
| |
| copyCode(button) { |
| const code = button.closest('.code-block').querySelector('pre').textContent; |
| navigator.clipboard.writeText(code).then(() => { |
| button.textContent = 'Copied!'; |
| setTimeout(() => button.textContent = 'Copy', 2000); |
| }); |
| } |
| |
| showToast(message, type = 'info') { |
| const toast = document.createElement('div'); |
| toast.className = `toast ${type}`; |
| |
| const icons = { |
| success: '✓', |
| error: '✕', |
| info: 'ℹ' |
| }; |
| |
| toast.innerHTML = `<span>${icons[type]}</span> ${message}`; |
| this.elements.toastContainer.appendChild(toast); |
| |
| setTimeout(() => { |
| toast.style.animation = 'toast-in 0.3s reverse forwards'; |
| setTimeout(() => toast.remove(), 300); |
| }, 3000); |
| } |
| |
| handleError(error) { |
| console.error('Chat error:', error); |
| this.showToast(error.message || 'Failed to send message', 'error'); |
| this.addMessage('system', `Error: ${error.message}. Please try again.`); |
| } |
| |
| debounce(fn, ms) { |
| let timeout; |
| return (...args) => { |
| clearTimeout(timeout); |
| timeout = setTimeout(() => fn.apply(this, args), ms); |
| }; |
| } |
| |
| delay(ms) { |
| return new Promise(resolve => setTimeout(resolve, ms)); |
| } |
| } |
| |
| |
| const chatApp = new ChatApplication(); |
| </script> |
| </body> |
| </html> |