diff --git a/app/frontend/index.html b/app/frontend/index.html index 4b2d673..921e22c 100644 --- a/app/frontend/index.html +++ b/app/frontend/index.html @@ -7,7 +7,7 @@ - Uncensored AI Studio + Speech Transcriber diff --git a/app/frontend/src/App.css b/app/frontend/src/App.css index 0527bee..092f158 100644 --- a/app/frontend/src/App.css +++ b/app/frontend/src/App.css @@ -6,8 +6,6 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - - /* Animation and Shape Tokens */ --md-shape-corner-small: 8px; --md-shape-corner-medium: 12px; --md-shape-corner-large: 16px; @@ -15,8 +13,6 @@ --md-shape-corner-full: 9999px; --md-transition-duration: 0.25s; --md-transition-easing: cubic-bezier(0.4, 0, 0.2, 1); - - /* Light Theme Variables (Default) */ --md-sys-color-primary: #4f46e5; --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: rgba(79, 70, 229, 0.06); @@ -33,15 +29,10 @@ --md-sys-color-on-surface: #0f172a; --md-sys-color-surface-variant: rgba(241, 245, 249, 0.8); --md-sys-color-on-surface-variant: #475569; - - /* Text description variables - HIGH CONTRAST for readability */ - --md-sys-color-outline: #64748b; /* Used for description texts in JSX */ - --md-sys-color-outline-variant: #94a3b8; /* Used for sub-description texts in JSX */ - - /* Border variables - SOFT TRANSLUCENT borders */ + --md-sys-color-outline: #64748b; + --md-sys-color-outline-variant: #94a3b8; --border-color: rgba(15, 23, 42, 0.08); --border-color-muted: rgba(15, 23, 42, 0.05); - --md-sys-color-error: #ef4444; --md-sys-color-on-error: #ffffff; --md-sys-color-success: #10b981; @@ -51,339 +42,149 @@ --glass-blur: blur(16px); } -html { - font-size: var(--app-font-size); -} +html { font-size: var(--app-font-size); } -/* Dark Theme Variables */ html[data-theme="dark"] { --md-sys-color-primary: #6366f1; - --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: rgba(99, 102, 241, 0.12); --md-sys-color-on-primary-container: #c7d2fe; --md-sys-color-secondary: #38bdf8; - --md-sys-color-on-secondary: #0f172a; --md-sys-color-secondary-container: rgba(56, 189, 248, 0.08); --md-sys-color-on-secondary-container: #bae6fd; --md-sys-color-tertiary: #f472b6; - --md-sys-color-on-tertiary: #0f172a; --md-sys-color-background: #090a0f; --md-sys-color-on-background: #f4f4f5; --md-sys-color-surface: rgba(18, 20, 29, 0.65); --md-sys-color-on-surface: #f4f4f5; --md-sys-color-surface-variant: rgba(26, 29, 41, 0.55); --md-sys-color-on-surface-variant: #a1a1aa; - - /* Text description variables - HIGH CONTRAST for readability */ - --md-sys-color-outline: #a1a1aa; /* Used for description texts in JSX */ - --md-sys-color-outline-variant: #71717a; /* Used for sub-description texts in JSX */ - - /* Border variables - SOFT TRANSLUCENT borders */ - --border-color: rgba(255, 255, 255, 0.09); - --border-color-muted: rgba(255, 255, 255, 0.05); - + --md-sys-color-outline: #a1a1aa; + --md-sys-color-outline-variant: #71717a; + --border-color: rgba(244, 244, 245, 0.06); + --border-color-muted: rgba(244, 244, 245, 0.04); --md-sys-color-error: #f87171; - --md-sys-color-on-error: #ffffff; - --md-sys-color-success: #34d399; - --md-sys-color-on-success: #090a0f; - --md-sys-color-card-border: rgba(255, 255, 255, 0.04); + --md-sys-color-card-border: rgba(244, 244, 245, 0.06); --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); - --glass-blur: blur(16px); + --glass-blur: blur(20px); } -/* Nord Light Theme */ html[data-theme="light-nord"] { --md-sys-color-primary: #5e81ac; - --md-sys-color-on-primary: #ffffff; --md-sys-color-primary-container: rgba(94, 129, 172, 0.08); - --md-sys-color-on-primary-container: #4c566a; - --md-sys-color-secondary: #88c0d0; - --md-sys-color-on-secondary: #2e3440; - --md-sys-color-secondary-container: rgba(136, 192, 208, 0.08); - --md-sys-color-on-secondary-container: #3b4252; - --md-sys-color-tertiary: #b48ead; - --md-sys-color-on-tertiary: #ffffff; + --md-sys-color-on-primary-container: #2e3440; + --md-sys-color-secondary: #81a1c1; --md-sys-color-background: #eceff4; --md-sys-color-on-background: #2e3440; - --md-sys-color-surface: rgba(242, 244, 248, 0.85); + --md-sys-color-surface: rgba(236, 239, 244, 0.85); --md-sys-color-on-surface: #2e3440; - --md-sys-color-surface-variant: rgba(229, 233, 240, 0.85); + --md-sys-color-surface-variant: rgba(216, 222, 233, 0.9); --md-sys-color-on-surface-variant: #4c566a; - --md-sys-color-outline: #4c566a; - --md-sys-color-outline-variant: #d8dee9; - --border-color: rgba(46, 52, 64, 0.09); - --border-color-muted: rgba(46, 52, 64, 0.05); + --md-sys-color-outline: #7b88a1; + --md-sys-color-outline-variant: #a3aac2; + --border-color: rgba(46, 52, 64, 0.08); --md-sys-color-error: #bf616a; - --md-sys-color-on-error: #ffffff; --md-sys-color-success: #a3be8c; - --md-sys-color-on-success: #2e3440; - --md-sys-color-card-border: rgba(46, 52, 64, 0.06); - --glass-shadow: 0 8px 32px 0 rgba(46, 52, 64, 0.03); - --glass-blur: blur(16px); } - -/* Sakura Blossom Light Theme */ html[data-theme="light-sakura"] { - --md-sys-color-primary: #d05a74; - --md-sys-color-on-primary: #ffffff; - --md-sys-color-primary-container: rgba(208, 90, 116, 0.06); - --md-sys-color-on-primary-container: #8c354a; - --md-sys-color-secondary: #e597a7; - --md-sys-color-on-secondary: #ffffff; - --md-sys-color-secondary-container: rgba(229, 151, 167, 0.08); - --md-sys-color-on-secondary-container: #8c354a; - --md-sys-color-tertiary: #a278b5; - --md-sys-color-on-tertiary: #ffffff; - --md-sys-color-background: #fdf6f6; - --md-sys-color-on-background: #5c2a39; - --md-sys-color-surface: rgba(255, 250, 250, 0.85); - --md-sys-color-on-surface: #5c2a39; - --md-sys-color-surface-variant: rgba(250, 235, 235, 0.9); - --md-sys-color-on-surface-variant: #8c5264; - --md-sys-color-outline: #8c5264; - --md-sys-color-outline-variant: #dbb2bc; - --border-color: rgba(92, 42, 57, 0.08); - --border-color-muted: rgba(92, 42, 57, 0.04); - --md-sys-color-error: #e05c5c; - --md-sys-color-on-error: #ffffff; - --md-sys-color-success: #5cbe8a; - --md-sys-color-on-success: #ffffff; - --md-sys-color-card-border: rgba(92, 42, 57, 0.06); - --glass-shadow: 0 8px 32px 0 rgba(92, 42, 57, 0.03); - --glass-blur: blur(16px); + --md-sys-color-primary: #d6336c; + --md-sys-color-primary-container: rgba(214, 51, 108, 0.06); + --md-sys-color-on-primary-container: #880e4f; + --md-sys-color-secondary: #e91e8c; + --md-sys-color-background: #fff5f7; + --md-sys-color-on-background: #331a24; + --md-sys-color-surface: rgba(255, 245, 247, 0.85); + --md-sys-color-on-surface: #331a24; + --md-sys-color-surface-variant: rgba(252, 228, 236, 0.9); + --md-sys-color-on-surface-variant: #6d3a4e; + --md-sys-color-outline: #b87090; + --md-sys-color-outline-variant: #d4a0b8; + --border-color: rgba(51, 26, 36, 0.08); + --md-sys-color-error: #c62828; + --md-sys-color-success: #2e7d32; } - -/* Cyberpunk Neon Dark Theme */ html[data-theme="dark-cyberpunk"] { - --md-sys-color-primary: #ff007f; - --md-sys-color-on-primary: #ffffff; - --md-sys-color-primary-container: rgba(255, 0, 127, 0.15); - --md-sys-color-on-primary-container: #ff9ebb; - --md-sys-color-secondary: #00f0ff; - --md-sys-color-on-secondary: #0d0714; - --md-sys-color-secondary-container: rgba(0, 240, 255, 0.1); - --md-sys-color-on-secondary-container: #a6f5ff; - --md-sys-color-tertiary: #9d4edd; - --md-sys-color-on-tertiary: #ffffff; - --md-sys-color-background: #0d0714; - --md-sys-color-on-background: #e0e0ff; - --md-sys-color-surface: rgba(26, 12, 38, 0.7); - --md-sys-color-on-surface: #e0e0ff; - --md-sys-color-surface-variant: rgba(37, 18, 54, 0.6); - --md-sys-color-on-surface-variant: #b5a8cc; - --md-sys-color-outline: #00f0ff; - --md-sys-color-outline-variant: #ff007f; - --border-color: rgba(0, 240, 255, 0.2); - --border-color-muted: rgba(255, 0, 127, 0.15); - --md-sys-color-error: #ff3333; - --md-sys-color-on-error: #ffffff; - --md-sys-color-success: #00ff66; - --md-sys-color-on-success: #0d0714; - --md-sys-color-card-border: rgba(0, 240, 255, 0.1); - --glass-shadow: 0 8px 32px 0 rgba(255, 0, 127, 0.15); - --glass-blur: blur(16px); + --md-sys-color-primary: #00f0ff; + --md-sys-color-on-primary: #0a0a0f; + --md-sys-color-primary-container: rgba(0, 240, 255, 0.08); + --md-sys-color-on-primary-container: #00f0ff; + --md-sys-color-secondary: #ff00e5; + --md-sys-color-tertiary: #ffee00; + --md-sys-color-background: #0a0a0f; + --md-sys-color-on-background: #00f0ff; + --md-sys-color-surface: rgba(12, 14, 20, 0.9); + --md-sys-color-on-surface: #00f0ff; + --md-sys-color-surface-variant: rgba(20, 24, 35, 0.85); + --md-sys-color-on-surface-variant: #8899aa; + --md-sys-color-outline: #556677; + --md-sys-color-outline-variant: #334455; + --border-color: rgba(0, 240, 255, 0.06); + --border-color-muted: rgba(0, 240, 255, 0.03); + --md-sys-color-error: #ff3366; + --md-sys-color-success: #00ff88; } - -/* Emerald Forest Dark Theme */ html[data-theme="dark-forest"] { - --md-sys-color-primary: #10b981; - --md-sys-color-on-primary: #040a09; - --md-sys-color-primary-container: rgba(16, 185, 129, 0.12); + --md-sys-color-primary: #34d399; + --md-sys-color-on-primary: #022c22; + --md-sys-color-primary-container: rgba(52, 211, 153, 0.08); --md-sys-color-on-primary-container: #a7f3d0; - --md-sys-color-secondary: #34d399; - --md-sys-color-on-secondary: #040a09; - --md-sys-color-secondary-container: rgba(52, 211, 153, 0.08); - --md-sys-color-on-secondary-container: #d1fae5; - --md-sys-color-tertiary: #6ee7b7; - --md-sys-color-on-tertiary: #040a09; - --md-sys-color-background: #060f0e; - --md-sys-color-on-background: #e2ebe9; - --md-sys-color-surface: rgba(13, 28, 25, 0.65); - --md-sys-color-on-surface: #e2ebe9; - --md-sys-color-surface-variant: rgba(20, 41, 37, 0.55); - --md-sys-color-on-surface-variant: #94a8a4; - --md-sys-color-outline: #86efac; - --md-sys-color-outline-variant: #15803d; - --border-color: rgba(52, 211, 153, 0.15); - --border-color-muted: rgba(52, 211, 153, 0.08); + --md-sys-color-secondary: #6ee7b7; + --md-sys-color-background: #0a1f15; + --md-sys-color-on-background: #d1fae5; + --md-sys-color-surface: rgba(10, 31, 21, 0.85); + --md-sys-color-on-surface: #d1fae5; + --md-sys-color-surface-variant: rgba(15, 45, 30, 0.75); + --md-sys-color-on-surface-variant: #86efac; + --md-sys-color-outline: #4ade80; + --md-sys-color-outline-variant: #22c55e; + --border-color: rgba(16, 185, 129, 0.08); --md-sys-color-error: #f87171; - --md-sys-color-on-error: #ffffff; --md-sys-color-success: #34d399; - --md-sys-color-on-success: #040a09; - --md-sys-color-card-border: rgba(52, 211, 153, 0.06); - --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4); - --glass-blur: blur(16px); } - -/* Dracula Classic Dark Theme */ html[data-theme="dark-dracula"] { --md-sys-color-primary: #bd93f9; - --md-sys-color-on-primary: #282a36; - --md-sys-color-primary-container: rgba(189, 147, 249, 0.15); - --md-sys-color-on-primary-container: #d6bbfb; - --md-sys-color-secondary: #ff79c6; - --md-sys-color-on-secondary: #282a36; - --md-sys-color-secondary-container: rgba(255, 121, 198, 0.1); - --md-sys-color-on-secondary-container: #ffb3db; - --md-sys-color-tertiary: #50fa7b; - --md-sys-color-on-tertiary: #282a36; + --md-sys-color-on-primary: #1e1f29; + --md-sys-color-primary-container: rgba(189, 147, 249, 0.1); + --md-sys-color-on-primary-container: #f8f8f2; + --md-sys-color-secondary: #8be9fd; + --md-sys-color-tertiary: #ff79c6; --md-sys-color-background: #282a36; --md-sys-color-on-background: #f8f8f2; - --md-sys-color-surface: rgba(33, 34, 44, 0.7); + --md-sys-color-surface: rgba(40, 42, 54, 0.9); --md-sys-color-on-surface: #f8f8f2; - --md-sys-color-surface-variant: rgba(40, 42, 54, 0.6); - --md-sys-color-on-surface-variant: #6272a4; - --md-sys-color-outline: #ff79c6; - --md-sys-color-outline-variant: #6272a4; - --border-color: rgba(189, 147, 249, 0.18); - --border-color-muted: rgba(255, 121, 198, 0.1); + --md-sys-color-surface-variant: rgba(68, 71, 90, 0.8); + --md-sys-color-on-surface-variant: #bfc4d4; + --md-sys-color-outline: #6272a4; + --md-sys-color-outline-variant: #44475a; + --border-color: rgba(98, 114, 164, 0.12); --md-sys-color-error: #ff5555; - --md-sys-color-on-error: #ffffff; --md-sys-color-success: #50fa7b; - --md-sys-color-on-success: #282a36; - --md-sys-color-card-border: rgba(189, 147, 249, 0.08); - --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.35); - --glass-blur: blur(16px); } - -/* OLED Pitch Black Theme */ html[data-theme="dark-oled"] { - --md-sys-color-primary: #3b82f6; - --md-sys-color-on-primary: #ffffff; - --md-sys-color-primary-container: rgba(59, 130, 246, 0.15); - --md-sys-color-on-primary-container: #93c5fd; - --md-sys-color-secondary: #60a5fa; - --md-sys-color-on-secondary: #000000; - --md-sys-color-secondary-container: rgba(96, 165, 250, 0.1); - --md-sys-color-on-secondary-container: #bfdbfe; - --md-sys-color-tertiary: #38bdf8; - --md-sys-color-on-tertiary: #000000; + --md-sys-color-primary: #7c8cf8; + --md-sys-color-primary-container: rgba(124, 140, 248, 0.1); + --md-sys-color-secondary: #38bdf8; + --md-sys-color-secondary-container: rgba(56, 189, 248, 0.06); --md-sys-color-background: #000000; - --md-sys-color-on-background: #ffffff; - --md-sys-color-surface: rgba(10, 10, 10, 0.95); - --md-sys-color-on-surface: #ffffff; - --md-sys-color-surface-variant: rgba(20, 20, 20, 0.98); - --md-sys-color-on-surface-variant: #cccccc; - --md-sys-color-outline: #888888; - --md-sys-color-outline-variant: #444444; - --border-color: rgba(255, 255, 255, 0.15); - --border-color-muted: rgba(255, 255, 255, 0.08); - --md-sys-color-error: #ef4444; - --md-sys-color-on-error: #ffffff; - --md-sys-color-success: #10b981; - --md-sys-color-on-success: #ffffff; - --md-sys-color-card-border: rgba(255, 255, 255, 0.1); - --glass-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1); - --glass-blur: none; + --md-sys-color-on-background: #e4e4e7; + --md-sys-color-surface: rgba(10, 10, 15, 0.95); + --md-sys-color-surface-variant: rgba(20, 20, 28, 0.9); + --md-sys-color-on-surface-variant: #a1a1aa; + --md-sys-color-outline: #71717a; + --md-sys-color-outline-variant: #3f3f46; + --border-color: rgba(228, 228, 231, 0.04); + --md-sys-color-card-border: rgba(228, 228, 231, 0.04); + --md-sys-color-error: #f87171; } +html[data-theme="dark-oled"] body { background-image: none !important; } -html[data-theme="dark-oled"] body { - background-image: none !important; -} - -/* Theme Dropdown & Selection Styling */ -.theme-dropdown-menu { - position: absolute; - top: 40px; - right: 0; - width: 190px; - background: color-mix(in srgb, var(--md-sys-color-surface) 40%, var(--md-sys-color-background)); - backdrop-filter: blur(20px) !important; - -webkit-backdrop-filter: blur(20px) !important; - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-medium); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25); - padding: 6px; - display: flex; - flex-direction: column; - gap: 2px; - z-index: 9999; - animation: theme-dropdown-fade 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -@keyframes theme-dropdown-fade { - from { - opacity: 0; - transform: translateY(-8px) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.theme-dropdown-header { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--md-sys-color-outline); - padding: 6px 10px 4px; -} - -.theme-dropdown-divider { - height: 1px; - background: var(--border-color-muted); - margin: 4px 0; -} - -.theme-dropdown-item { - background: transparent; - border: none; - border-radius: calc(var(--md-shape-corner-medium) - 4px); - padding: 8px 10px; - font-family: inherit; - font-size: 0.8rem; - font-weight: 500; - color: var(--md-sys-color-on-surface-variant); - cursor: pointer; - display: flex; - align-items: center; - text-align: left; - transition: all 0.15s ease; - width: 100%; -} - -.theme-dropdown-item:hover { - background: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); -} - -.theme-dropdown-item.active { - background: var(--md-sys-color-secondary-container); - color: var(--md-sys-color-on-secondary-container); - font-weight: 600; -} - -.theme-card-btn { - transform: translateY(0); - transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease !important; -} - -.theme-card-btn:hover { - transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08) !important; - border-color: var(--md-sys-color-outline) !important; -} - -.theme-card-btn.active:hover { - border-color: var(--md-sys-color-primary) !important; -} - -.theme-card-btn.active { - box-shadow: 0 4px 16px color-mix(in srgb, var(--md-sys-color-primary) 25%, transparent) !important; -} - -/* Global Reset & Base Styles */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} +@keyframes spin { to { transform: rotate(360deg); } } +.progress-spinner { animation: spin 0.7s linear infinite; border-style: solid; border-radius: 50%; } +@keyframes pulse { 0%, 100% { opacity: 0.6; transform: scale(1); } 50% { opacity: 1; transform: scale(1.3); } } +* { box-sizing: border-box; margin: 0; padding: 0; } body { background-color: var(--md-sys-color-background); - background-image: + background-image: radial-gradient(at 0% 0%, var(--md-sys-color-primary-container) 0px, transparent 50%), radial-gradient(at 100% 0%, var(--md-sys-color-secondary-container) 0px, transparent 40%); color: var(--md-sys-color-on-background); @@ -394,3447 +195,125 @@ body { transition: background-color var(--md-transition-duration) ease; } -/* Layout Framework */ -.app-container { - display: flex; - flex-direction: row; - height: 100vh; - width: 100vw; - overflow: hidden; -} +.app-container { display: flex; flex-direction: column; height: 100vh; width: 100vw; overflow: hidden; } + +.app-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 28px; border-bottom: 1px solid var(--border-color); background: var(--md-sys-color-surface); backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur); position: sticky; top: 0; z-index: 50; flex-shrink: 0; } +.app-header-left { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.app-title { font-size: 1.2rem; font-weight: 700; margin: 0; color: var(--md-sys-color-on-surface); letter-spacing: -0.02em; white-space: nowrap; } +.app-subtitle { font-size: 0.78rem; margin: 0; color: var(--md-sys-color-outline); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.app-header-right { display: flex; align-items: center; gap: 14px; flex-shrink: 0; } +.telemetry-chips { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } +.chip { font-size: 0.72rem; font-weight: 600; padding: 4px 10px; border-radius: var(--md-shape-corner-full); background: var(--md-sys-color-surface-variant); color: var(--md-sys-color-on-surface-variant); border: 1px solid var(--border-color); white-space: nowrap; } +.theme-toggle { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: var(--md-shape-corner-full); border: 1px solid var(--border-color); background: var(--md-sys-color-surface-variant); cursor: pointer; font-size: 1.05rem; transition: all 0.15s ease; flex-shrink: 0; } +.theme-toggle:hover { background: var(--md-sys-color-primary-container); color: var(--md-sys-color-primary); } + +.app-nav { display: flex; gap: 4px; padding: 8px 28px; border-bottom: 1px solid var(--border-color); background: var(--md-sys-color-surface); backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur); flex-shrink: 0; } +.nav-tab { padding: 9px 18px; border: none; border-radius: var(--md-shape-corner-medium); background: transparent; color: var(--md-sys-color-outline); font-weight: 600; font-size: 0.88rem; cursor: pointer; transition: all 0.15s ease; font-family: inherit; } +.nav-tab:hover { background: var(--md-sys-color-surface-variant); color: var(--md-sys-color-on-surface); } +.nav-tab.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); } + +.app-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; } + +.workspace-area { flex: 1; overflow-y: auto; padding: 28px; scroll-behavior: smooth; } +.workspace-area::-webkit-scrollbar { width: 6px; } +.workspace-area::-webkit-scrollbar-track { background: transparent; } +.workspace-area::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--md-sys-color-outline) 25%, transparent); border-radius: 99px; } +.workspace-title-section { margin-bottom: 24px; } +.workspace-title { font-size: 1.6rem; font-weight: 700; color: var(--md-sys-color-on-background); letter-spacing: -0.02em; } +.workspace-subtitle { font-size: 0.9rem; color: var(--md-sys-color-on-surface-variant); margin-top: 4px; opacity: 0.8; } + +.speech-workspace { display: flex; flex-direction: column; gap: 18px; } +.speech-grid { display: grid; grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr); gap: 18px; align-items: stretch; } +@media (max-width: 768px) { .speech-grid { grid-template-columns: 1fr; } } +.speech-panel, .speech-result-panel { display: flex; flex-direction: column; gap: 16px; padding: 18px; } +.speech-panel-header, .speech-button-row, .speech-controls-row { display: flex; align-items: center; gap: 10px; } +.speech-panel-header { justify-content: space-between; } +.speech-panel-header h3 { margin: 0; font-size: 1rem; } +.speech-button-row { flex-wrap: wrap; } +.speech-controls-row { flex-wrap: wrap; } +.speech-label { display: flex; flex-direction: column; gap: 4px; font-size: 0.82rem; font-weight: 600; color: var(--md-sys-color-on-surface-variant); flex: 1; min-width: 100px; } +.speech-capture-zone { display: flex; align-items: center; gap: 14px; padding: 18px; background: var(--md-sys-color-surface-variant); border-radius: var(--md-shape-corner-medium); border: 1px dashed var(--border-color); } +.speech-capture-zone p { font-size: 0.82rem; color: var(--md-sys-color-outline); margin-top: 2px; } +.m3-textarea.speech-result-textarea { min-height: 200px; font-family: 'SF Mono', 'Fira Code', 'Fira Mono', monospace; font-size: 0.88rem; line-height: 1.6; } + +.status-chip { display: inline-flex; align-items: center; gap: 6px; background-color: var(--md-sys-color-surface-variant); border: 1px solid var(--border-color); color: var(--md-sys-color-on-surface-variant); padding: 5px 12px; border-radius: var(--md-shape-corner-full); font-size: 0.78rem; font-weight: 550; } +.status-chip.offline { border-color: var(--md-sys-color-error); color: var(--md-sys-color-error); background: color-mix(in srgb, var(--md-sys-color-error) 8%, var(--md-sys-color-surface-variant)); } + +.text-progress { display: flex; align-items: center; gap: 7px; color: var(--md-sys-color-on-surface-variant); font-size: 0.8rem; } +.text-progress.error { color: var(--md-sys-color-error); } + +.loading-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--md-sys-color-primary); opacity: 0.9; animation: pulse 1.2s ease-in-out infinite; box-shadow: 0 0 8px var(--md-sys-color-primary); } +.model-progress-bar { width: 100%; height: 6px; background-color: var(--md-sys-color-surface-variant); border-radius: var(--md-shape-corner-full); overflow: hidden; } +.model-progress-fill { height: 100%; background: linear-gradient(90deg, var(--md-sys-color-primary), #d946ef); width: 0%; transition: width 0.4s cubic-bezier(0.1, 0.8, 0.25, 1); box-shadow: 0 0 8px rgba(99, 102, 241, 0.4); } +.model-progress-section { display: flex; flex-direction: column; gap: 6px; } +.model-progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--md-sys-color-outline); } + +.m3-card { background-color: var(--md-sys-color-surface); backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur); border: 1px solid var(--md-sys-color-card-border); border-radius: var(--md-shape-corner-large); padding: 24px; margin-bottom: 24px; box-shadow: var(--glass-shadow); transition: all var(--md-transition-duration) var(--md-transition-easing); } +.m3-card:hover { border-color: rgba(99, 102, 241, 0.15); } +.m3-card-title { font-size: 1.05rem; font-weight: 600; margin-bottom: 20px; color: var(--md-sys-color-on-surface); display: flex; align-items: center; gap: 8px; } + +.m3-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 38px; padding: 0 20px; border-radius: var(--md-shape-corner-medium); font-size: 0.88rem; font-weight: 550; font-family: inherit; border: none; cursor: pointer; transition: all var(--md-transition-duration) var(--md-transition-easing); user-select: none; line-height: 1; white-space: nowrap; } +.m3-btn svg { flex: 0 0 auto; } +.m3-btn .progress-spinner { width: 14px; height: 14px; border-width: 2px; border-color: currentColor; border-right-color: transparent; border-bottom-color: transparent; } +.m3-btn:disabled { opacity: 0.45; cursor: not-allowed; } +.m3-btn-filled { background: linear-gradient(135deg, var(--md-sys-color-primary), #8b5cf6); color: var(--md-sys-color-on-primary); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); } +.m3-btn-filled:hover:not(:disabled) { box-shadow: 0 6px 20px rgba(99, 102, 241, 0.35); transform: translateY(-1px); filter: brightness(1.04); } +.m3-btn-tonal { background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); } +.m3-btn-tonal:hover:not(:disabled) { background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); transform: translateY(-1px); } +.m3-btn-outlined { background-color: transparent; border: 1px solid var(--border-color); color: var(--md-sys-color-on-surface); } +.m3-btn-outlined:hover:not(:disabled) { background-color: var(--md-sys-color-surface-variant); border-color: var(--md-sys-color-primary); color: var(--md-sys-color-primary); } +.m3-btn-error { background-color: rgba(244, 63, 94, 0.08); border: 1px solid rgba(244, 63, 94, 0.18); color: #e11d48; box-shadow: 0 2px 8px rgba(244, 63, 94, 0.05); } +.m3-btn-error:hover:not(:disabled) { background-color: rgba(244, 63, 94, 0.15); border-color: rgba(244, 63, 94, 0.3); transform: translateY(-1px); } +html[data-theme^="dark"] .m3-btn-error { background-color: rgba(251, 113, 133, 0.09); border: 1px solid rgba(251, 113, 133, 0.22); color: #fda4af; } +html[data-theme^="dark"] .m3-btn-error:hover:not(:disabled) { background-color: rgba(251, 113, 133, 0.16); border-color: rgba(251, 113, 133, 0.35); } + +.m3-field-group { display: flex; flex-direction: column; gap: 18px; } +.m3-text-field { display: flex; flex-direction: column; gap: 6px; } +.m3-text-field-label { font-size: 0.82rem; font-weight: 600; letter-spacing: 0.01em; color: var(--md-sys-color-on-surface-variant); } +.m3-input, .m3-textarea { width: 100%; background-color: var(--md-sys-color-surface-variant); border: 1.5px solid var(--border-color); border-radius: var(--md-shape-corner-medium); padding: 10px 14px; font-family: inherit; font-size: 0.92rem; color: var(--md-sys-color-on-surface); transition: all var(--md-transition-duration) var(--md-transition-easing); outline: none; } +.m3-input { height: 42px; } +.m3-textarea { min-height: 90px; resize: vertical; } +.m3-input:focus, .m3-textarea:focus { border-color: var(--md-sys-color-primary); background-color: var(--md-sys-color-surface); box-shadow: 0 0 0 3px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent); } + +.m3-slider-group { display: flex; flex-direction: column; gap: 6px; } +.m3-slider-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; } +.m3-slider-label { font-size: 0.82rem; font-weight: 600; color: var(--md-sys-color-on-surface-variant); letter-spacing: 0.01em; } +.m3-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 5px; border-radius: var(--md-shape-corner-full); background: linear-gradient(90deg, var(--md-sys-color-primary) var(--value, 50%), var(--border-color) var(--value, 50%)); outline: none; margin: 10px 0; cursor: pointer; } +.m3-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--md-sys-color-primary); border: 2.5px solid var(--md-sys-color-surface); box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-sys-color-primary) 30%, transparent); cursor: pointer; } +.m3-slider::-webkit-slider-thumb:hover { transform: scale(1.2); } +.m3-slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--md-sys-color-primary); border: 2.5px solid var(--md-sys-color-surface); cursor: pointer; } + +.settings-section-header { display: flex; align-items: center; padding: 14px 0; border-left: 3px solid transparent; margin-top: 8px; } +.settings-section-icon { width: 38px; height: 38px; border-radius: var(--md-shape-corner-small); display: flex; align-items: center; justify-content: center; flex-shrink: 0; } +.settings-section-title { font-size: 1.05rem; font-weight: 700; color: var(--md-sys-color-on-surface); } +.settings-section-count { font-size: 0.78rem; color: var(--md-sys-color-outline); font-weight: 500; } +.settings-expanded-content { padding: 16px 0 8px; } +.settings-two-column { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; } +@media (max-width: 768px) { .settings-two-column { grid-template-columns: 1fr; gap: 18px; } } +.settings-column { display: flex; flex-direction: column; gap: 18px; } +.settings-subsection { display: flex; flex-direction: column; gap: 12px; } +.settings-subsection-title { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; font-weight: 600; color: var(--md-sys-color-on-surface-variant); padding-bottom: 8px; border-bottom: 1px solid var(--border-color-muted); } +.settings-option-desc { font-size: 0.75rem; color: var(--md-sys-color-outline); margin-top: 4px; line-height: 1.4; } +.settings-value-badge { font-size: 0.78rem; font-weight: 700; color: var(--md-sys-color-primary); background-color: var(--md-sys-color-primary-container); padding: 2px 10px; border-radius: var(--md-shape-corner-full); } + +.premium-toggle { display: flex; align-items: center; gap: 12px; padding: 10px 0; } +.premium-toggle-checkbox { width: 20px; height: 20px; border-radius: var(--md-shape-corner-small); border: 2px solid var(--md-sys-color-outline); display: flex; align-items: center; justify-content: center; flex-shrink: 0; cursor: pointer; transition: all 0.15s ease; } +.premium-toggle-checkbox.checked { background: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); color: white; } +.premium-toggle-label { font-size: 0.88rem; font-weight: 600; color: var(--md-sys-color-on-surface); } +.premium-toggle-desc { font-size: 0.75rem; color: var(--md-sys-color-outline); margin-top: 2px; } + +.import-box { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 28px; background: var(--md-sys-color-surface-variant); border: 2px dashed var(--border-color); border-radius: var(--md-shape-corner-large); cursor: pointer; transition: all 0.2s ease; } +.import-box:hover { border-color: var(--md-sys-color-primary); background: var(--md-sys-color-primary-container); } +.import-icon { color: var(--md-sys-color-primary); width: 28px; height: 28px; } + +.app-dialog-backdrop { position: fixed; inset: 0; z-index: 100; background: rgba(0, 0, 0, 0.55); display: flex; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(4px); } +.app-dialog { background: var(--md-sys-color-surface); border-radius: var(--md-shape-corner-large); padding: 28px; max-width: 460px; width: 100%; border: 1px solid var(--border-color); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); } +.app-dialog-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 12px; color: var(--md-sys-color-on-surface); } +.app-dialog-message { font-size: 0.92rem; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; margin-bottom: 20px; white-space: pre-wrap; } +.app-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; } -/* Sidebar Styling */ -.sidebar { - width: 280px; - background-color: var(--md-sys-color-surface); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-right: 1px solid var(--border-color); - display: flex; - flex-direction: column; - padding: 28px 18px; - justify-content: space-between; - box-shadow: var(--glass-shadow); - z-index: 11; - transition: all var(--md-transition-duration) var(--md-transition-easing); -} - -.sidebar.collapsed { - width: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; - border-right-color: transparent !important; - opacity: 0; - pointer-events: none; - overflow: hidden; -} - -.sidebar-logo { - display: flex; - align-items: center; - gap: 12px; - padding: 0 12px 28px 12px; -} - -.sidebar-logo-icon { - color: var(--md-sys-color-primary); - width: 26px; - height: 26px; - filter: drop-shadow(0 2px 8px rgba(99, 102, 241, 0.3)); -} - -.sidebar-logo-text { - font-size: 1.15rem; - font-weight: 700; - letter-spacing: -0.02em; - color: var(--md-sys-color-on-background); -} - -.nav-list { - display: flex; - flex-direction: column; - gap: 6px; - margin-bottom: auto; -} - -.nav-item { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 16px; - height: 48px; - border-radius: var(--md-shape-corner-medium); - color: var(--md-sys-color-on-surface-variant); - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all var(--md-transition-duration) var(--md-transition-easing); - user-select: none; - text-decoration: none; - position: relative; -} - -.nav-item:hover { - background-color: var(--md-sys-color-surface-variant); - color: var(--md-sys-color-primary); - transform: translateX(2px); -} - -.nav-item.active { - background-color: var(--md-sys-color-primary-container); - color: var(--md-sys-color-primary); - font-weight: 600; -} - -.nav-item.active::before { - content: ""; - position: absolute; - left: 0; - top: 14px; - bottom: 14px; - width: 4px; - background-color: var(--md-sys-color-primary); - border-radius: 0 4px 4px 0; -} - -.sidebar-footer { - padding: 18px 12px 0 12px; - border-top: 1px solid var(--border-color); - font-size: 0.78rem; - color: var(--md-sys-color-on-surface-variant); - display: flex; - flex-direction: column; - gap: 6px; - opacity: 0.95; -} - -/* Main Content Panel */ -.main-content { - flex: 1; - display: flex; - flex-direction: column; - background-color: transparent; - overflow: hidden; -} - -.text-chat-layout { - flex: 1; - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 0px; - padding: 10px 12px; - min-height: 0; -} - -.text-chat-main, -.text-chat-settings { - min-height: 0; -} - -.text-chat-main { - display: flex; - flex-direction: column; - overflow: hidden; - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--md-sys-color-surface); - box-shadow: var(--glass-shadow); -} - -.text-chat-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 10px 14px; - border-bottom: 1px solid var(--border-color); -} - -.text-chat-header h2 { - display: flex; - align-items: center; - gap: 9px; - font-size: 1.2rem; -} - -.text-chat-header p, -.chat-empty p { - margin-top: 5px; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.86rem; -} - -.text-engine-badge { - max-width: 240px; - padding: 7px 10px; - border-radius: var(--md-shape-corner-full); - background: var(--md-sys-color-surface-variant); - color: var(--md-sys-color-on-surface-variant); - font-size: 0.75rem; - text-align: center; -} - -.text-engine-badge.ready { - color: var(--md-sys-color-success); - border: 1px solid color-mix(in srgb, var(--md-sys-color-success) 45%, transparent); -} - -/* ─── Chat Messages Container ─────────────────────────────────── */ -.chat-messages { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 14px 12px 8px; - display: flex; - flex-direction: column; - gap: 0; - scroll-behavior: smooth; -} - -.chat-messages::-webkit-scrollbar { - width: 5px; -} -.chat-messages::-webkit-scrollbar-track { - background: transparent; -} -.chat-messages::-webkit-scrollbar-thumb { - background: color-mix(in srgb, var(--md-sys-color-outline) 35%, transparent); - border-radius: 99px; -} - -/* ─── Empty State ─────────────────────────────────────────────── */ -.chat-empty { - margin: auto; - max-width: 460px; - text-align: center; - color: var(--md-sys-color-on-surface-variant); - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - padding: 40px 24px; -} - -.chat-empty-icon { - width: 68px; - height: 68px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, - color-mix(in srgb, var(--md-sys-color-primary) 18%, transparent), - color-mix(in srgb, var(--md-sys-color-tertiary, #7c3aed) 12%, transparent)); - border: 1.5px solid color-mix(in srgb, var(--md-sys-color-primary) 25%, transparent); - color: var(--md-sys-color-primary); - animation: emptyPulse 3s ease-in-out infinite; -} - -@keyframes emptyPulse { - 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--md-sys-color-primary) 25%, transparent); } - 50% { box-shadow: 0 0 0 12px color-mix(in srgb, var(--md-sys-color-primary) 0%, transparent); } -} - -.chat-empty h3 { - margin: 0; - font-size: 1.15rem; - font-weight: 700; - color: var(--md-sys-color-on-surface); - letter-spacing: -0.01em; -} - -.chat-empty p { - margin: 0; - font-size: 0.875rem; - line-height: 1.6; - max-width: 340px; -} - -.chat-suggestions { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - max-width: 380px; - margin-top: 8px; -} - -.chat-suggestion-chip { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - border-radius: var(--md-shape-corner-medium); - border: 1px solid var(--border-color); - background: var(--md-sys-color-surface-variant); - color: var(--md-sys-color-on-surface-variant); - font-size: 0.82rem; - font-weight: 500; - text-align: left; - cursor: pointer; - transition: all 0.18s ease; - line-height: 1.4; -} - -.chat-suggestion-chip:hover { - border-color: var(--md-sys-color-primary); - background: color-mix(in srgb, var(--md-sys-color-primary) 8%, var(--md-sys-color-surface-variant)); - color: var(--md-sys-color-on-surface); - transform: translateX(3px); -} - -/* ─── Message Row (avatar + bubble) ──────────────────────────── */ -.chat-message-row { - display: flex; - align-items: flex-end; - gap: 8px; - margin-bottom: 4px; - animation: msgSlideIn 0.22s cubic-bezier(0.22, 1, 0.36, 1) both; -} - -.chat-message-row.ai { - align-items: flex-start; -} - -@keyframes msgSlideIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.chat-message-row.user { - flex-direction: row-reverse; -} - -.chat-message-row + .chat-message-row { - margin-top: 2px; -} - -/* Group spacing: bigger gap between user→ai switches */ -.chat-message-row.ai + .chat-message-row.user, -.chat-message-row.user + .chat-message-row.ai { - margin-top: 10px; -} - -/* ─── Avatar Chip ─────────────────────────────────────────────── */ -.chat-avatar { - width: 30px; - height: 30px; - border-radius: 50%; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.72rem; - font-weight: 800; - letter-spacing: 0.02em; - user-select: none; -} - -.chat-avatar.ai { - background: linear-gradient(135deg, var(--md-sys-color-primary), color-mix(in srgb, var(--md-sys-color-primary) 70%, #7c3aed)); - color: var(--md-sys-color-on-primary); - box-shadow: - 0 0 0 3px color-mix(in srgb, var(--md-sys-color-primary) 14%, transparent), - 0 8px 20px color-mix(in srgb, var(--md-sys-color-primary) 28%, transparent); -} - -.chat-avatar.user { - background: color-mix(in srgb, var(--md-sys-color-primary-container) 80%, var(--md-sys-color-surface-variant)); - color: var(--md-sys-color-on-primary-container); - border: 1.5px solid color-mix(in srgb, var(--md-sys-color-primary) 30%, transparent); -} - -/* ─── Bubble ──────────────────────────────────────────────────── */ -.chat-bubble-wrap { - display: flex; - flex-direction: column; - max-width: min(1440px, calc(100% - 42px)); - gap: 5px; -} - -.chat-message-row.user .chat-bubble-wrap { - align-items: flex-end; - max-width: min(920px, 72%); -} - -.chat-message-row.ai .chat-bubble-wrap { - align-items: flex-start; - max-width: 80%; -} - -.chat-bubble { - padding: 10px 14px; - border-radius: 14px; - line-height: 1.5; - overflow-wrap: anywhere; - position: relative; - font-size: 0.88rem; -} - -/* AI bubble */ -.chat-message-row.ai .chat-bubble { - background: var(--md-sys-color-surface-variant); - color: var(--md-sys-color-on-surface); - border: 1px solid var(--border-color); - border-bottom-left-radius: 4px; - max-width: 100%; -} - -/* User bubble */ -.chat-message-row.user .chat-bubble { - background: linear-gradient(135deg, - var(--md-sys-color-primary), - color-mix(in srgb, var(--md-sys-color-primary) 80%, #7c3aed 20%)); - color: var(--md-sys-color-on-primary); - border: none; - border-bottom-right-radius: 4px; - box-shadow: 0 2px 12px color-mix(in srgb, var(--md-sys-color-primary) 30%, transparent); -} - -.chat-bubble.error { - background: color-mix(in srgb, var(--md-sys-color-error) 12%, var(--md-sys-color-surface-variant)); - border-color: color-mix(in srgb, var(--md-sys-color-error) 35%, transparent); - color: var(--md-sys-color-error); -} - -/* Sender label */ -.chat-sender-label { - font-size: 0.66rem; - font-weight: 800; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--md-sys-color-outline); - padding: 0 2px; - line-height: 1.1; -} - -/* ─── Generation Stats ────────────────────────────────────────── */ -.chat-generation-warning { - margin-top: 4px; - padding: 7px 10px; - border-radius: var(--md-shape-corner-small); - border: 1px solid color-mix(in srgb, var(--md-sys-color-warning, #f59e0b) 35%, transparent); - background: color-mix(in srgb, var(--md-sys-color-warning, #f59e0b) 10%, transparent); - color: var(--md-sys-color-on-surface-variant); - font-size: 0.76rem; - line-height: 1.35; - max-width: min(720px, 84vw); -} - -.chat-generation-stats { - display: inline-flex; - align-items: center; - gap: 6px; - margin-top: 2px; - padding: 4px 10px; - border-radius: 99px; - background: color-mix(in srgb, var(--md-sys-color-surface-variant) 82%, transparent); - border: 1px solid color-mix(in srgb, var(--border-color) 88%, var(--md-sys-color-primary) 12%); - color: var(--md-sys-color-on-surface-variant); - font-size: 0.68rem; - font-weight: 800; - backdrop-filter: blur(8px); - line-height: 1.4; -} - -.chat-generation-stats.streaming { - color: var(--md-sys-color-primary); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 38%, transparent); - background: color-mix(in srgb, var(--md-sys-color-primary) 10%, var(--md-sys-color-surface-variant)); - box-shadow: 0 6px 18px color-mix(in srgb, var(--md-sys-color-primary) 10%, transparent); -} - -@keyframes statsGlow { - 0%, 100% { box-shadow: none; } - 50% { box-shadow: 0 0 8px color-mix(in srgb, var(--md-sys-color-primary) 30%, transparent); } -} - -.chat-generation-stats.complete { - color: var(--md-sys-color-primary); -} - -.chat-generation-stats.starting { - animation: statsBlink 1.1s ease-in-out infinite; -} - -@keyframes statsBlink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.45; } -} - -/* ─── Thinking / Progress ─────────────────────────────────────── */ -.chat-thinking, -.text-progress { - display: flex; - align-items: center; - gap: 7px; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.8rem; -} - -.text-progress.error { - color: var(--md-sys-color-error); -} - -/* ─── Composer ────────────────────────────────────────────────── */ -.chat-composer { - display: flex; - flex-direction: column; - gap: 0; - padding: 10px 14px 12px; - border-top: 1px solid var(--border-color); - background: color-mix(in srgb, var(--md-sys-color-surface) 85%, transparent); - backdrop-filter: blur(16px); -} - -.chat-composer-inner { - display: flex; - flex-direction: column; - align-items: stretch; - background: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - border-radius: 18px; - padding: 10px 12px 8px; - transition: border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1), - background-color 0.25s cubic-bezier(0.4, 0, 0.2, 1); -} - -.chat-composer-inner:hover { - border-color: color-mix(in srgb, var(--md-sys-color-primary) 30%, var(--border-color)); -} - -.chat-composer-inner:focus-within { - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent), - 0 4px 16px rgba(0, 0, 0, 0.08); - background: color-mix(in srgb, var(--md-sys-color-surface-variant) 97%, var(--md-sys-color-primary)); -} - -.chat-composer textarea, -.system-prompt, -.text-chat-settings input, -.text-chat-settings select { - width: 100%; - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-medium); - background: var(--md-sys-color-surface-variant); - color: var(--md-sys-color-on-surface); - padding: 10px 12px; - font: inherit; - outline: none; -} - -.chat-composer-textarea-container { - width: 100%; - display: flex; - min-width: 0; -} - -.chat-composer-textarea { - flex: 1; - border: none !important; - background: transparent !important; - color: var(--md-sys-color-on-surface); - padding: 2px 4px 6px 4px; - font: inherit; - font-size: 0.92rem; - outline: none; - resize: none !important; - min-height: 28px; - max-height: 140px; - line-height: 1.45; - overflow-y: auto; - margin-bottom: 0; -} - -.chat-composer-textarea::placeholder { - color: var(--md-sys-color-outline); - font-weight: 400; - opacity: 0.8; -} - -.chat-composer textarea { - min-height: 28px; - max-height: 140px; - resize: none; -} - -.chat-composer-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - border-top: 1px solid color-mix(in srgb, var(--border-color) 35%, transparent); - padding-top: 8px; - margin-top: 4px; -} - -.chat-composer-toolbar-left { - display: flex; - align-items: center; - gap: 8px; -} - -.chat-composer-toolbar-right { - display: flex; - align-items: center; -} - -.chat-composer-send-btn { - width: 32px; - height: 32px; - border-radius: 50%; - border: none; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - background: linear-gradient(135deg, var(--md-sys-color-primary), color-mix(in srgb, var(--md-sys-color-primary) 75%, #7c3aed)); - color: var(--md-sys-color-on-primary); - box-shadow: 0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 30%, transparent); - margin-bottom: 0; -} - -.chat-composer-send-btn:hover:not(:disabled) { - transform: scale(1.08) translateY(-1px); - box-shadow: 0 4px 14px color-mix(in srgb, var(--md-sys-color-primary) 45%, transparent); -} - -.chat-composer-send-btn:active:not(:disabled) { - transform: scale(0.96) translateY(0); -} - -.chat-composer-send-btn:disabled { - background: color-mix(in srgb, var(--md-sys-color-surface-variant) 80%, transparent); - color: var(--md-sys-color-outline); - opacity: 0.5; - box-shadow: none; - cursor: not-allowed; -} - -.chat-composer-stop-btn { - width: 32px; - height: 32px; - border-radius: 50%; - border: none; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - background: color-mix(in srgb, var(--md-sys-color-error) 15%, var(--md-sys-color-surface-variant)); - color: var(--md-sys-color-error); - border: 1.5px solid color-mix(in srgb, var(--md-sys-color-error) 40%, transparent); - animation: stopPulse 1.5s ease-in-out infinite; - margin-bottom: 0; -} - -@keyframes stopPulse { - 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--md-sys-color-error) 30%, transparent); } - 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--md-sys-color-error) 0%, transparent); } -} - -.chat-composer-stop-btn:hover { - background: color-mix(in srgb, var(--md-sys-color-error) 22%, var(--md-sys-color-surface-variant)); - transform: scale(1.08) translateY(-1px); -} - -.chat-composer-attach-btn { - width: 32px; - height: 32px; - border-radius: 50%; - border: none; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - background: transparent; - color: var(--md-sys-color-outline); - margin-bottom: 0; -} - -.chat-composer-attach-btn:hover:not(:disabled) { - background: color-mix(in srgb, var(--md-sys-color-primary) 10%, transparent); - color: var(--md-sys-color-primary); - transform: translateY(-1px); -} - -.chat-composer-attach-btn:disabled { - opacity: 0.35; - cursor: not-allowed; -} - -.chat-composer-hint { - display: none; -} - -.chat-composer-deepthink-btn { - height: 32px; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 12px; - border-radius: 999px; - border: 1px solid var(--border-color); - background: color-mix(in srgb, var(--md-sys-color-surface-variant) 40%, transparent); - color: var(--md-sys-color-outline); - font-family: Outfit, sans-serif; - font-size: 0.78rem; - font-weight: 600; - cursor: pointer; - flex-shrink: 0; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - margin-bottom: 0; -} - -.chat-composer-deepthink-btn:hover:not(:disabled) { - border-color: color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent); - background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); - color: var(--md-sys-color-on-surface); - transform: translateY(-1px); -} - -.chat-composer-deepthink-btn.active { - background: color-mix(in srgb, var(--md-sys-color-primary) 12%, transparent); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent); - color: var(--md-sys-color-primary); - box-shadow: 0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 10%, transparent); -} - -.chat-composer-deepthink-btn.deepthink-btn.active { - background: color-mix(in srgb, var(--md-sys-color-primary) 14%, transparent); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 45%, transparent); - color: var(--md-sys-color-primary); - box-shadow: 0 0 12px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent); -} - -.chat-composer-deepthink-btn.active:hover:not(:disabled) { - background: color-mix(in srgb, var(--md-sys-color-primary) 18%, transparent); - transform: translateY(-1px); -} - -/* Custom Dropdown select */ -.chat-composer-select { - height: 32px; - width: 105px; - border-radius: 999px; - border: 1px solid var(--border-color); - background-color: color-mix(in srgb, var(--md-sys-color-surface-variant) 40%, transparent); - color: var(--md-sys-color-outline); - font-family: Outfit, sans-serif; - font-size: 0.78rem; - font-weight: 600; - padding: 0 26px 0 12px; - cursor: pointer; - outline: none; - margin-bottom: 2px; - flex-shrink: 0; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 10px center; -} - -.chat-composer-select:hover:not(:disabled) { - border-color: color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent); - background-color: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); - color: var(--md-sys-color-on-surface); - transform: translateY(-1px); -} - -.chat-composer-select:focus { - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent); -} - -.chat-composer-select option { - background-color: var(--md-sys-color-surface); - color: var(--md-sys-color-on-surface); - padding: 8px; -} - -/* Custom Chat Model Dropdown Selection */ -.chat-model-select-trigger { - height: 34px; - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - border-radius: var(--md-shape-corner-medium); - background: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - color: var(--md-sys-color-on-surface); - padding: 0 12px; - font-family: Outfit, sans-serif; - font-size: 0.9rem; - font-weight: 600; - cursor: pointer; - outline: none; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -.chat-model-select-trigger:hover:not(:disabled) { - border-color: color-mix(in srgb, var(--md-sys-color-primary) 40%, var(--border-color)); - background: color-mix(in srgb, var(--md-sys-color-surface-variant) 95%, var(--md-sys-color-primary)); -} - -.chat-model-select-trigger.active { - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent); - background: color-mix(in srgb, var(--md-sys-color-surface-variant) 95%, var(--md-sys-color-primary)); -} - -.chat-model-select-trigger:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.chat-model-select-label { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - text-align: left; -} - -.chat-model-select-arrow { - transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); - color: var(--md-sys-color-outline); - flex-shrink: 0; -} - -.chat-model-select-arrow.open { - transform: rotate(180deg); - color: var(--md-sys-color-primary); -} - -.chat-model-dropdown-menu { - position: absolute; - top: 40px; - left: 0; - width: 100%; - min-width: 280px; - max-height: 280px; - overflow-y: auto; - background: color-mix(in srgb, var(--md-sys-color-surface) 40%, var(--md-sys-color-background)); - backdrop-filter: blur(20px) !important; - -webkit-backdrop-filter: blur(20px) !important; - border: 1px solid var(--border-color); - border-radius: 12px; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25); - padding: 6px; - display: flex; - flex-direction: column; - gap: 2px; - z-index: 9999; - animation: theme-dropdown-fade 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -.chat-model-dropdown-item { - width: 100%; - padding: 8px 12px; - border: none; - background: transparent; - color: var(--md-sys-color-on-surface); - font-family: Outfit, sans-serif; - font-size: 0.85rem; - font-weight: 500; - text-align: left; - border-radius: 8px; - cursor: pointer; - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.chat-model-dropdown-item:hover { - background: color-mix(in srgb, var(--md-sys-color-primary) 10%, transparent); - color: var(--md-sys-color-primary); -} - -.chat-model-dropdown-item.selected { - background: color-mix(in srgb, var(--md-sys-color-primary) 8%, var(--md-sys-color-surface-variant)); - font-weight: 600; -} - -.chat-model-dropdown-item.active { - border-left: 3px solid var(--md-sys-color-primary); - padding-left: 9px; -} - -.chat-model-name-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -} - -.chat-model-active-badge { - font-size: 0.65rem; - font-weight: 700; - text-transform: uppercase; - background: var(--md-sys-color-primary-container); - color: var(--md-sys-color-primary); - padding: 2px 6px; - border-radius: 4px; - letter-spacing: 0.05em; -} - -@keyframes rotateThinking { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.rotate-anim { - animation: rotateThinking 7s linear infinite; -} - -.text-chat-settings { - display: flex; - flex-direction: column; - gap: 14px; - overflow: auto; - padding-right: 3px; -} - -.text-chat-settings .m3-card { - padding: 16px; -} - -.text-model-actions, -.text-url-row { - display: flex; - gap: 8px; - margin-top: 10px; -} - -.text-local-model, -.text-download-option { - width: 100%; - display: flex; - align-items: center; - gap: 8px; - margin-top: 8px; - padding: 9px 10px; - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-small); - background: transparent; - color: var(--md-sys-color-on-surface); - text-align: left; -} - -.text-local-model span, -.text-download-option span { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.text-local-model button { - border: 0; - background: transparent; - color: var(--md-sys-color-error); - cursor: pointer; -} - -.text-download-option { - cursor: pointer; -} - -.text-download-option:hover { - background: var(--md-sys-color-surface-variant); -} - -.text-download-option small { - color: var(--md-sys-color-on-surface-variant); -} - -.system-prompt { - min-height: 85px; - resize: vertical; - margin-top: 9px; -} - -@media (max-width: 1050px) { - .text-chat-layout { - grid-template-columns: 1fr; - overflow: auto; - } - - .text-chat-main { - min-height: 620px; - } - - .text-chat-settings { - overflow: visible; - } -} - -/* Persistent Top Status Bar */ -.top-status-bar { - height: 64px; - background-color: var(--md-sys-color-surface); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 24px; - z-index: 10; - box-shadow: var(--glass-shadow); -} - -.sidebar-toggle-btn { - background: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - color: var(--md-sys-color-on-surface-variant); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: var(--md-shape-corner-medium); - transition: all var(--md-transition-duration) var(--md-transition-easing); - flex-shrink: 0; -} - -.sidebar-toggle-btn:hover { - color: var(--md-sys-color-primary); - border-color: var(--md-sys-color-primary); - background-color: var(--md-sys-color-primary-container); - transform: scale(1.05); -} - -.current-model-info { - display: flex; - align-items: center; - gap: 10px; - font-size: 0.88rem; -} - -.status-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: var(--md-sys-color-success); - box-shadow: 0 0 8px var(--md-sys-color-success); -} - -.status-indicator.busy { - background-color: var(--md-sys-color-primary); - box-shadow: 0 0 8px var(--md-sys-color-primary); - animation: pulse 1.8s infinite; -} - -.status-indicator.offline { - background-color: var(--md-sys-color-on-surface-variant); - box-shadow: none; - opacity: 0.6; -} - -.telemetry-group { - display: flex; - align-items: center; - gap: 8px; -} - -.telemetry-chip { - display: flex; - align-items: center; - gap: 6px; - background-color: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-medium); - padding: 6px 12px; - font-size: 0.8rem; - font-weight: 500; - color: var(--md-sys-color-on-surface-variant); - transition: all var(--md-transition-duration); -} - -.telemetry-chip-icon { - width: 14px; - height: 14px; -} - -.readiness-card { - margin: 0 0 18px 0; - border-left: 4px solid var(--md-sys-color-primary); -} - -.readiness-card-warning { - border-left-color: var(--md-sys-color-error); -} - -.readiness-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 14px; -} - -.readiness-actions { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 8px; - flex-wrap: wrap; -} - -.readiness-actions .m3-btn { - height: 34px; - padding: 0 14px; -} - -.readiness-issues, -.readiness-cleanup-list { - display: flex; - flex-direction: column; - gap: 6px; - margin-top: 12px; - font-size: 0.82rem; - color: var(--md-sys-color-on-surface-variant); -} - -.readiness-issues span { - color: var(--md-sys-color-error); -} - -/* Theme Toggle Button Style */ -.theme-toggle-btn { - background: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - color: var(--md-sys-color-on-surface-variant); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: var(--md-shape-corner-medium); - transition: all var(--md-transition-duration) var(--md-transition-easing); - margin-right: 6px; -} - -.theme-toggle-btn:hover { - color: var(--md-sys-color-primary); - border-color: var(--md-sys-color-primary); - background-color: var(--md-sys-color-primary-container); - transform: scale(1.05); -} - -/* Workspace Area */ -.workspace-area { - flex: 1; - overflow-y: auto; - padding: 28px; - scroll-behavior: smooth; -} - -.workspace-area::-webkit-scrollbar { - width: 6px; -} - -.workspace-area::-webkit-scrollbar-track { - background: transparent; -} - -.workspace-area::-webkit-scrollbar-thumb { - background: color-mix(in srgb, var(--md-sys-color-outline) 25%, transparent); - border-radius: 99px; -} - -.workspace-area::-webkit-scrollbar-thumb:hover { - background: color-mix(in srgb, var(--md-sys-color-outline) 45%, transparent); -} - -.workspace-title-section { - margin-bottom: 24px; -} - -.workspace-title { - font-size: 1.6rem; - font-weight: 700; - color: var(--md-sys-color-on-background); - letter-spacing: -0.02em; -} - -.workspace-subtitle { - font-size: 0.9rem; - color: var(--md-sys-color-on-surface-variant); - margin-top: 4px; - opacity: 0.8; -} - -.speech-workspace { - display: flex; - flex-direction: column; - gap: 18px; -} - -.speech-grid { - display: grid; - grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr); - gap: 18px; - align-items: stretch; -} - -.speech-panel, -.speech-result-panel { - display: flex; - flex-direction: column; - gap: 16px; - padding: 18px; -} - -.speech-panel-header, -.speech-button-row, -.speech-controls-row { - display: flex; - align-items: center; - gap: 10px; -} - -.speech-panel-header { - justify-content: space-between; -} - -.speech-panel-header h3 { - margin: 0; - font-size: 1rem; -} - -.speech-button-row { - flex-wrap: wrap; -} - -.speech-controls-row { - align-items: flex-end; -} - -.speech-label { - display: flex; - flex: 1; - min-width: 0; - flex-direction: column; - gap: 6px; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.82rem; - font-weight: 600; -} - -.speech-starter-list { - display: grid; - grid-template-columns: 1fr; - gap: 8px; -} - -.speech-capture-zone { - display: flex; - align-items: center; - gap: 14px; - min-height: 120px; - border: 1px dashed var(--md-sys-color-outline-variant); - border-radius: var(--md-shape-corner-medium); - padding: 18px; - background: var(--md-sys-color-surface-container-low); -} - -.speech-capture-zone p { - margin: 4px 0 0 0; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.86rem; - line-height: 1.4; -} - -.m3-textarea.speech-result-textarea { - min-height: 420px; - height: clamp(420px, 46vh, 720px); - resize: vertical; - line-height: 1.55; - font-size: 0.98rem; -} - -.speech-history-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.speech-history-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 10px 12px; - border: 1px solid var(--md-sys-color-outline-variant); - border-radius: var(--md-shape-corner-small); -} - -.speech-history-item div { - display: flex; - min-width: 0; - flex-direction: column; - gap: 2px; -} - -.speech-history-item strong, -.speech-history-item span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.speech-history-item span { - color: var(--md-sys-color-on-surface-variant); - font-size: 0.78rem; -} - -@media (max-width: 980px) { - .speech-grid { - grid-template-columns: 1fr; - } -} - -/* Glassmorphic UI Cards */ -.m3-card { - background-color: var(--md-sys-color-surface); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border: 1px solid var(--md-sys-color-card-border); - border-radius: var(--md-shape-corner-large); - padding: 24px; - margin-bottom: 24px; - box-shadow: var(--glass-shadow); - transition: all var(--md-transition-duration) var(--md-transition-easing); -} - -.m3-card:hover { - border-color: rgba(99, 102, 241, 0.15); -} - -.m3-card-title { - font-size: 1.05rem; - font-weight: 600; - margin-bottom: 20px; - color: var(--md-sys-color-on-surface); - display: flex; - align-items: center; - gap: 8px; -} - -/* Buttons System */ -.m3-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - height: 38px; - padding: 0 20px; - border-radius: var(--md-shape-corner-medium); - font-size: 0.88rem; - font-weight: 550; - font-family: inherit; - border: none; - cursor: pointer; - transition: all var(--md-transition-duration) var(--md-transition-easing); - user-select: none; - line-height: 1; - white-space: nowrap; -} - -.m3-btn svg { - flex: 0 0 auto; -} - -.m3-btn .progress-spinner { - width: 14px; - height: 14px; - border-width: 2px; - border-color: currentColor; - border-right-color: transparent; - border-bottom-color: transparent; -} - -.m3-btn:disabled { - opacity: 0.45; - cursor: not-allowed; -} - -.m3-btn-filled { - background: linear-gradient(135deg, var(--md-sys-color-primary), #8b5cf6); - color: var(--md-sys-color-on-primary); - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); -} - -.m3-btn-filled:hover:not(:disabled) { - box-shadow: 0 6px 20px rgba(99, 102, 241, 0.35); - transform: translateY(-1px); - filter: brightness(1.04); -} - -.m3-btn-tonal { - background-color: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); -} - -.m3-btn-tonal:hover:not(:disabled) { - background-color: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); - transform: translateY(-1px); -} - -.m3-btn-outlined { - background-color: transparent; - border: 1px solid var(--border-color); - color: var(--md-sys-color-on-surface); -} - -.m3-btn-outlined:hover:not(:disabled) { - background-color: var(--md-sys-color-surface-variant); - border-color: var(--md-sys-color-primary); - color: var(--md-sys-color-primary); -} - -.m3-btn-error { - background-color: rgba(244, 63, 94, 0.08); - border: 1px solid rgba(244, 63, 94, 0.18); - color: #e11d48; - box-shadow: 0 2px 8px rgba(244, 63, 94, 0.05); -} - -.m3-btn-error:hover:not(:disabled) { - background-color: rgba(244, 63, 94, 0.15); - border-color: rgba(244, 63, 94, 0.3); - transform: translateY(-1px); -} - -html[data-theme^="dark"] .m3-btn-error { - background-color: rgba(251, 113, 133, 0.09); - border: 1px solid rgba(251, 113, 133, 0.22); - color: #fda4af; - box-shadow: 0 2px 8px rgba(251, 113, 133, 0.04); -} - -html[data-theme^="dark"] .m3-btn-error:hover:not(:disabled) { - background-color: rgba(251, 113, 133, 0.16); - border-color: rgba(251, 113, 133, 0.35); -} - -/* Fields & Groups */ -.m3-field-group { - display: flex; - flex-direction: column; - gap: 18px; -} - -.m3-text-field { - display: flex; - flex-direction: column; - gap: 6px; -} - -.m3-text-field-label { - font-size: 0.82rem; - font-weight: 600; - letter-spacing: 0.01em; - color: var(--md-sys-color-on-surface-variant); -} - -.m3-input, .m3-textarea { - width: 100%; - background-color: var(--md-sys-color-surface-variant); - border: 1.5px solid var(--border-color); - border-radius: var(--md-shape-corner-medium); - padding: 10px 14px; - font-family: inherit; - font-size: 0.92rem; - color: var(--md-sys-color-on-surface); - transition: all var(--md-transition-duration) var(--md-transition-easing); - outline: none; -} - -.m3-input { - height: 42px; -} - -.m3-textarea { - min-height: 90px; - resize: vertical; -} - -.m3-input:focus, .m3-textarea:focus { - border-color: var(--md-sys-color-primary); - background-color: var(--md-sys-color-surface); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent), 0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 10%, transparent); -} - -/* Custom Range Sliders */ -.m3-slider-group { - display: flex; - flex-direction: column; - gap: 6px; -} - -.m3-slider-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2px; -} - -.m3-slider-label { - font-size: 0.82rem; - font-weight: 600; - color: var(--md-sys-color-on-surface-variant); - letter-spacing: 0.01em; -} - -.m3-slider-value { - font-size: 0.78rem; - font-weight: 700; - color: var(--md-sys-color-primary); - background-color: var(--md-sys-color-primary-container); - padding: 2px 8px; - border-radius: var(--md-shape-corner-full); -} - -.m3-slider { - -webkit-appearance: none; - appearance: none; - width: 100%; - height: 5px; - border-radius: var(--md-shape-corner-full); - background: linear-gradient(90deg, - var(--md-sys-color-primary) var(--value, 50%), - var(--border-color) var(--value, 50%) - ); - outline: none; - margin: 10px 0; - transition: all 0.25s; - cursor: pointer; -} - -.m3-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--md-sys-color-primary); - border: 2.5px solid var(--md-sys-color-surface); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-sys-color-primary) 30%, transparent), 0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent); - cursor: pointer; - transition: all var(--md-transition-duration) var(--md-transition-easing); -} - -.m3-slider::-webkit-slider-thumb:hover { - transform: scale(1.2); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--md-sys-color-primary) 25%, transparent), 0 4px 12px color-mix(in srgb, var(--md-sys-color-primary) 50%, transparent); -} - -.m3-slider::-moz-range-thumb { - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--md-sys-color-primary); - border: 2.5px solid var(--md-sys-color-surface); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-sys-color-primary) 30%, transparent), 0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent); - cursor: pointer; - transition: all var(--md-transition-duration) var(--md-transition-easing); -} - -/* Modern Switch Styles */ -.m3-switch-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; -} - -.m3-switch-info { - display: flex; - flex-direction: column; - gap: 2px; -} - -.m3-switch-title { - font-size: 0.9rem; - font-weight: 600; - color: var(--md-sys-color-on-surface); -} - -.m3-switch-description { - font-size: 0.78rem; - color: var(--md-sys-color-on-surface-variant); - opacity: 0.8; -} - -.m3-switch { - position: relative; - display: inline-block; - width: 46px; - height: 26px; -} - -.m3-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.m3-switch-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - border-radius: 26px; - transition: .25s; -} - -.m3-switch-slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background-color: var(--md-sys-color-on-surface-variant); - border-radius: 50%; - transition: .25s; -} - -.m3-switch input:checked + .m3-switch-slider { - background-color: var(--md-sys-color-primary); - border-color: var(--md-sys-color-primary); -} - -.m3-switch input:checked + .m3-switch-slider:before { - transform: translateX(20px); - background-color: var(--md-sys-color-on-primary); -} - -/* Segmented Buttons */ -.m3-segmented-button { - display: flex; - background-color: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-medium); - padding: 3px; - overflow: hidden; - width: 100%; -} - -.m3-segment-item { - flex: 1; - text-align: center; - padding: 8px 0; - font-size: 0.82rem; - font-weight: 600; - cursor: pointer; - background-color: transparent; - color: var(--md-sys-color-on-surface-variant); - transition: all var(--md-transition-duration) var(--md-transition-easing); - border-radius: calc(var(--md-shape-corner-medium) - 2px); - user-select: none; - border: none; -} - -.m3-segment-item:hover:not(.active):not(.disabled) { - color: var(--md-sys-color-on-surface); - background-color: rgba(0, 0, 0, 0.03); -} - -html[data-theme^="dark"] .m3-segment-item:hover:not(.active):not(.disabled) { - background-color: rgba(255, 255, 255, 0.04); -} - -.m3-segment-item.active { - background: linear-gradient(135deg, var(--md-sys-color-primary), color-mix(in srgb, var(--md-sys-color-primary) 85%, var(--md-sys-color-secondary))); - color: var(--md-sys-color-on-primary); - box-shadow: 0 2px 10px color-mix(in srgb, var(--md-sys-color-primary) 35%, transparent); - font-weight: 700; -} - -.m3-segment-item.disabled { - opacity: 0.45; - cursor: not-allowed; - text-decoration: line-through; -} - -/* Grid Layouts */ -.generator-layout { - display: grid; - grid-template-columns: 1fr; - gap: 24px; -} - -@media (min-width: 1000px) { - .generator-layout { - grid-template-columns: 1.15fr 0.85fr; - } -} - -.chips-container { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 8px; -} - -.status-chip { - display: inline-flex; - align-items: center; - gap: 6px; - background-color: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - color: var(--md-sys-color-on-surface-variant); - padding: 5px 12px; - border-radius: var(--md-shape-corner-full); - font-size: 0.78rem; - font-weight: 550; -} - -.prompt-actions { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 16px; -} - -/* Aspect Ratio Buttons with Previews */ -.aspect-ratio-btn { - position: relative; - transition: all var(--md-transition-duration) var(--md-transition-easing); -} - -.aspect-ratio-btn.active { - background: linear-gradient(135deg, - color-mix(in srgb, var(--md-sys-color-primary-container) 80%, transparent), - color-mix(in srgb, var(--md-sys-color-secondary-container) 50%, transparent) - ) !important; - border-color: var(--md-sys-color-primary) !important; - color: var(--md-sys-color-primary) !important; - box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent), 0 4px 12px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent); - font-weight: 700; -} - -.aspect-ratio-preview { - border: 1.5px solid currentColor; - border-radius: 2px; - opacity: 0.6; - background-color: transparent; - transition: all 0.2s ease; - margin-bottom: 6px; -} - -.aspect-ratio-btn:hover .aspect-ratio-preview, -.aspect-ratio-btn.active .aspect-ratio-preview { - opacity: 1; - background-color: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); -} - -.ratio-1-1 { width: 16px; height: 16px; } -.ratio-4-3 { width: 20px; height: 15px; } -.ratio-16-9 { width: 24px; height: 13.5px; } -.ratio-9-16 { width: 13.5px; height: 24px; } - -/* Image Generation Output Area */ -.output-panel { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 380px; -} - -.output-image-container { - width: 100%; - aspect-ratio: 1; - background-color: var(--md-sys-color-surface-variant); - border-radius: var(--md-shape-corner-large); - border: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - position: relative; - box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05); -} - -.output-image { - width: 100%; - height: 100%; - object-fit: contain; - transition: transform 0.3s; -} - -.output-image:hover { - transform: scale(1.015); -} - -/* Generation Progress Overlay */ -.progress-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(9, 10, 15, 0.85); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - color: #ffffff; - gap: 14px; - padding: 24px; - z-index: 5; -} - -.progress-spinner { - width: 44px; - height: 44px; - border: 3px solid rgba(255, 255, 255, 0.15); - border-top-color: var(--md-sys-color-primary); - border-radius: 50%; - animation: spin 0.9s cubic-bezier(0.5, 0.1, 0.4, 0.9) infinite; - box-shadow: 0 0 12px rgba(99, 102, 241, 0.2); -} - -.loading-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--md-sys-color-primary); - opacity: 0.9; - animation: pulse 1.2s ease-in-out infinite; - box-shadow: 0 0 8px var(--md-sys-color-primary); -} - -.progress-text { - font-size: 1.05rem; - font-weight: 700; - letter-spacing: -0.01em; -} - -.progress-bar-container { - width: 80%; - height: 5px; - background-color: rgba(255, 255, 255, 0.12); - border-radius: var(--md-shape-corner-full); - overflow: hidden; - margin: 4px 0; -} - -.progress-bar-fill { - height: 100%; - background: linear-gradient(90deg, var(--md-sys-color-primary), #d946ef); - width: 0%; - transition: width 0.4s cubic-bezier(0.1, 0.8, 0.25, 1); - box-shadow: 0 0 8px rgba(99, 102, 241, 0.4); -} - -.output-placeholder-icon { - width: 48px; - height: 48px; - color: var(--md-sys-color-on-surface-variant); - opacity: 0.5; - filter: drop-shadow(0 2px 10px rgba(0,0,0,0.05)); -} - -.output-placeholder-text { - font-size: 0.88rem; - color: var(--md-sys-color-on-surface-variant); - margin-top: 12px; - text-align: center; - line-height: 1.5; - opacity: 0.8; -} - -.image-actions { - display: flex; - gap: 10px; - margin-top: 16px; - width: 100%; -} - -.image-actions button { - flex: 1; -} - -/* Models Grid and Lists */ -.models-grid { - display: grid; - grid-template-columns: 1fr; - gap: 20px; -} - -@media (min-width: 800px) { - .models-grid { - grid-template-columns: 1fr 1fr; - } -} - -.model-card { - display: flex; - flex-direction: column; - justify-content: space-between; - border: 1px solid var(--border-color-muted); -} - -.model-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; -} - -.model-name { - font-size: 1.05rem; - font-weight: 600; -} - -.model-badge { - font-size: 0.72rem; - font-weight: 700; - padding: 3px 8px; - border-radius: var(--md-shape-corner-full); -} - -.model-badge-gguf { - background-color: rgba(245, 158, 11, 0.15); - color: #d97706; -} - -.model-badge-safetensors { - background-color: rgba(14, 165, 233, 0.15); - color: #0284c7; -} - -.model-description { - font-size: 0.82rem; - color: var(--md-sys-color-on-surface-variant); - margin-bottom: 16px; - line-height: 1.45; - opacity: 0.9; -} - -.model-status-info { - margin-top: auto; - padding-top: 12px; - border-top: 1px solid var(--border-color-muted); - display: flex; - align-items: center; - justify-content: space-between; -} - -.model-size { - font-size: 0.8rem; - font-weight: 550; - color: var(--md-sys-color-on-surface-variant); -} - -.model-progress-section { - width: 100%; - margin-top: 12px; -} - -.model-progress-label { - display: flex; - justify-content: space-between; - font-size: 0.78rem; - font-weight: 600; - margin-bottom: 4px; -} - -.model-progress-bar { - width: 100%; - height: 6px; - background-color: var(--border-color-muted); - border-radius: var(--md-shape-corner-full); - overflow: hidden; -} - -.model-progress-fill { - height: 100%; - background: linear-gradient(90deg, var(--md-sys-color-primary), var(--md-sys-color-secondary)); - width: 0%; - transition: width 0.25s ease; -} - -.model-discovery-controls { - display: flex; - flex-direction: column; - gap: 12px; - margin-bottom: 16px; - padding: 16px; - border: 1px solid var(--border-color-muted); - border-radius: var(--md-shape-corner-large); - background: var(--md-sys-color-surface); -} - -.model-search-box { - display: flex; - align-items: center; - gap: 10px; - min-height: 44px; - padding: 0 12px; - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-medium); - background: var(--md-sys-color-surface-variant); - color: var(--md-sys-color-outline); -} - -.model-search-box:focus-within { - border-color: var(--md-sys-color-primary); -} - -.model-search-box input { - flex: 1; - min-width: 0; - border: 0; - outline: 0; - background: transparent; - color: var(--md-sys-color-on-surface); - font: inherit; -} - -.model-search-box button { - display: inline-flex; - padding: 4px; - border: 0; - background: transparent; - color: inherit; - cursor: pointer; -} - -.model-filter-row { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; -} - -.model-filter-chip { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 34px; - padding: 0 12px; - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-full); - background: transparent; - color: var(--md-sys-color-on-surface-variant); - font-weight: 600; - cursor: pointer; -} - -.model-filter-chip.selected { - border-color: var(--md-sys-color-primary); - background: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); -} - -.model-filter-clear { - border: 0; - background: transparent; - color: var(--md-sys-color-primary); - font-weight: 600; - cursor: pointer; -} - -.model-discovery-loading { - display: flex; - align-items: center; - gap: 12px; - color: var(--md-sys-color-outline); - font-size: 0.82rem; - font-weight: 600; -} - -.model-search-warning { - padding: 10px 12px; - border-radius: var(--md-shape-corner-small); - background: var(--md-sys-color-error-container); - color: var(--md-sys-color-on-error-container); - font-size: 0.8rem; -} - -.model-search-empty { - color: var(--md-sys-color-on-surface-variant); - text-align: center; -} - -.model-load-more { - align-self: center; - min-width: 180px; - margin: 4px 0 8px; -} - -/* Importer & Library */ -.import-box { - border: 2px dashed var(--border-color); - border-radius: var(--md-shape-corner-large); - padding: 32px; - text-align: center; - cursor: pointer; - transition: all var(--md-transition-duration); - background-color: var(--md-sys-color-surface); - color: var(--md-sys-color-on-surface-variant); - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; -} - -.import-box:hover { - background-color: var(--md-sys-color-primary-container); - border-color: var(--md-sys-color-primary); - color: var(--md-sys-color-primary); -} - -.import-icon { - width: 40px; - height: 40px; - color: var(--md-sys-color-primary); - filter: drop-shadow(0 2px 6px rgba(99, 102, 241, 0.15)); -} - -/* Gallery Section */ -.gallery-section { - margin-top: 36px; -} - -.gallery-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; -} - -.gallery-selection-actions { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.gallery-selection-actions .m3-btn { - height: 34px; - padding: 0 14px; -} - -.gallery-selection-count { - font-size: 0.82rem; - font-weight: 700; - color: var(--md-sys-color-primary); - padding: 7px 10px; - border-radius: var(--md-shape-corner-full); - background: var(--md-sys-color-primary-container); -} - -.gallery-hint { - margin-top: 8px; - font-size: 0.78rem; - color: var(--md-sys-color-outline); -} - -.gallery-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); - gap: 12px; - margin-top: 16px; -} - -.gallery-item { - aspect-ratio: 1; - border-radius: var(--md-shape-corner-medium); - overflow: hidden; - border: 1px solid var(--border-color-muted); - cursor: pointer; - position: relative; - contain: layout paint style; - transition: border-color var(--md-transition-duration), box-shadow var(--md-transition-duration), transform var(--md-transition-duration); -} - -.gallery-item.selected { - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.25); -} - -.gallery-thumb { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform var(--md-transition-duration) var(--md-transition-easing); -} - -.gallery-item:hover .gallery-thumb { - transform: scale(1.05); -} - -.gallery-select-badge { - position: absolute; - top: 8px; - right: 8px; - width: 26px; - height: 26px; - border-radius: 50%; - display: grid; - place-items: center; - color: var(--md-sys-color-on-primary); - background: var(--md-sys-color-primary); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); -} - -.app-dialog-backdrop { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - background: rgba(8, 12, 24, 0.64); - backdrop-filter: blur(4px) saturate(0.86); -} - -.app-dialog { - width: min(460px, 100%); - background: color-mix(in srgb, var(--md-sys-color-surface) 96%, white 4%); - color: var(--md-sys-color-on-surface); - border: 1px solid color-mix(in srgb, var(--md-sys-color-outline) 55%, transparent); - border-radius: var(--md-shape-corner-large); - box-shadow: - 0 22px 60px rgba(0, 0, 0, 0.42), - 0 0 0 1px rgba(255, 255, 255, 0.08) inset; - padding: 24px; -} - -.app-dialog-title { - font-size: 1.15rem; - font-weight: 800; - margin: 0 0 10px 0; -} - -.app-dialog-message { - font-size: 0.92rem; - line-height: 1.45; - color: var(--md-sys-color-on-surface-variant); - margin: 0; - white-space: pre-wrap; -} - -.app-dialog-actions { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 22px; - flex-wrap: wrap; -} - -.gallery-empty { - text-align: center; - padding: 36px; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.85rem; - opacity: 0.8; -} - -/* Animations */ -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -@keyframes pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.55; transform: scale(1.06); } -} - -/* Sidebar specs classes */ -.sidebar-specs-header { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 6px; - font-weight: 600; - color: var(--md-sys-color-on-background); -} - -.sidebar-specs-item { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - opacity: 0.85; - margin-bottom: 2px; - color: var(--md-sys-color-on-background); -} - -.sidebar-specs-os { - color: var(--md-sys-color-primary); - font-weight: 600; - margin-top: 4px; -} - -/* Chat History Sidebar styling */ -.chat-history-sidebar { - width: 240px; - flex-shrink: 0; - display: flex; - flex-direction: column; - background: var(--md-sys-color-surface-variant); - opacity: 0.95; - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-large); - padding: 16px; - height: 100%; - overflow: hidden; - box-shadow: var(--glass-shadow); - transition: all 0.3s ease; -} - -.history-items-list { - display: flex; - flex-direction: column; - gap: 8px; - margin-top: 14px; - overflow-y: auto; - flex: 1; -} - -.history-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 12px; - border-radius: var(--md-shape-corner-medium); - cursor: pointer; - background: transparent; - transition: background-color 0.2s ease, border-color 0.2s ease; - border: 1px solid transparent; -} - -.history-item:hover { - background: rgba(120, 120, 120, 0.1); -} - -.history-item.active { - background: var(--md-sys-color-secondary-container); - color: var(--md-sys-color-on-secondary-container); - border-color: var(--md-sys-color-outline-variant); -} - -.history-item-info { - display: flex; - flex-direction: column; - min-width: 0; - flex: 1; -} - -.history-item-title { - font-size: 0.85rem; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--md-sys-color-on-surface); -} - -.history-item-meta { - font-size: 0.68rem; - opacity: 0.7; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-top: 2px; - color: var(--md-sys-color-on-surface-variant); -} - -.history-item-delete { - background: transparent; - border: none; - color: var(--md-sys-color-outline); - cursor: pointer; - padding: 4px; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - transition: transform var(--md-transition-duration) var(--md-transition-easing); -} - -.gallery-item:hover .gallery-thumb { - transform: scale(1.05); -} - -.gallery-select-badge { - position: absolute; - top: 8px; - right: 8px; - width: 26px; - height: 26px; - border-radius: 50%; - display: grid; - place-items: center; - color: var(--md-sys-color-on-primary); - background: var(--md-sys-color-primary); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); -} - -.app-dialog-backdrop { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - background: rgba(8, 12, 24, 0.64); - backdrop-filter: blur(4px) saturate(0.86); -} - -.app-dialog { - width: min(460px, 100%); - background: color-mix(in srgb, var(--md-sys-color-surface) 96%, white 4%); - color: var(--md-sys-color-on-surface); - border: 1px solid color-mix(in srgb, var(--md-sys-color-outline) 55%, transparent); - border-radius: var(--md-shape-corner-large); - box-shadow: - 0 22px 60px rgba(0, 0, 0, 0.42), - 0 0 0 1px rgba(255, 255, 255, 0.08) inset; - padding: 24px; -} - -.app-dialog-title { - font-size: 1.15rem; - font-weight: 800; - margin: 0 0 10px 0; -} - -.app-dialog-message { - font-size: 0.92rem; - line-height: 1.45; - color: var(--md-sys-color-on-surface-variant); - margin: 0; - white-space: pre-wrap; -} - -.app-dialog-actions { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 22px; - flex-wrap: wrap; -} - -.gallery-empty { - text-align: center; - padding: 36px; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.85rem; - opacity: 0.8; -} - -/* Animations */ -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -@keyframes pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.55; transform: scale(1.06); } -} - -/* Sidebar specs classes */ -.sidebar-specs-header { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 6px; - font-weight: 600; - color: var(--md-sys-color-on-background); -} - -.sidebar-specs-item { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - opacity: 0.85; - margin-bottom: 2px; - color: var(--md-sys-color-on-background); -} - -.sidebar-specs-os { - color: var(--md-sys-color-primary); - font-weight: 600; - margin-top: 4px; -} - -/* Chat History Sidebar styling */ -.chat-history-sidebar { - width: 240px; - flex-shrink: 0; - display: flex; - flex-direction: column; - background: var(--md-sys-color-surface-variant); - opacity: 0.95; - border: 1px solid var(--border-color); - border-radius: var(--md-shape-corner-large); - padding: 16px; - height: 100%; - overflow: hidden; - box-shadow: var(--glass-shadow); - transition: all 0.3s ease; -} - -.history-items-list { - display: flex; - flex-direction: column; - gap: 8px; - margin-top: 14px; - overflow-y: auto; - flex: 1; -} - -.history-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 12px; - border-radius: var(--md-shape-corner-medium); - cursor: pointer; - background: transparent; - transition: background-color 0.2s ease, border-color 0.2s ease; - border: 1px solid transparent; -} - -.history-item:hover { - background: rgba(120, 120, 120, 0.1); -} - -.history-item.active { - background: var(--md-sys-color-secondary-container); - color: var(--md-sys-color-on-secondary-container); - border-color: var(--md-sys-color-outline-variant); -} - -.history-item-info { - display: flex; - flex-direction: column; - min-width: 0; - flex: 1; -} - -.history-item-title { - font-size: 0.85rem; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--md-sys-color-on-surface); -} - -.history-item-meta { - font-size: 0.68rem; - opacity: 0.7; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-top: 2px; - color: var(--md-sys-color-on-surface-variant); -} - -.history-item-delete { - background: transparent; - border: none; - color: var(--md-sys-color-outline); - cursor: pointer; - padding: 4px; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.2s, background-color 0.2s; - flex-shrink: 0; - margin-left: 6px; -} - -.history-item-delete:hover { - color: var(--md-sys-color-error); - background: rgba(239, 68, 68, 0.1); -} - -/* Chat Thinking Block (Reasoning Process) */ -.chat-thinking-container { - margin-bottom: 4px; - width: fit-content; - max-width: min(420px, 100%); - border-radius: 999px; - border: 1px solid color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--border-color)); - background: - linear-gradient(135deg, - color-mix(in srgb, var(--md-sys-color-primary) 10%, var(--md-sys-color-surface-variant)), - color-mix(in srgb, var(--md-sys-color-surface-variant) 88%, transparent)); - box-shadow: 0 8px 24px color-mix(in srgb, var(--md-sys-color-primary) 9%, transparent); - overflow: hidden; - backdrop-filter: blur(10px); -} - -.chat-thinking-container:has(.chat-thinking-content) { - border-radius: 13px; - max-width: min(720px, 92vw); -} - -.chat-thinking-header { - width: auto; - min-width: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 7px 11px; - background: transparent; - border: none; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.75rem; - font-weight: 800; - cursor: pointer; - text-align: left; - transition: background 0.18s ease, color 0.18s ease; - outline: none; -} - -.chat-thinking-header:hover { - background: color-mix(in srgb, var(--md-sys-color-primary) 7%, transparent); - color: var(--md-sys-color-on-surface); -} - -.chat-thinking-title { - display: flex; - align-items: center; - gap: 8px; - opacity: 0.92; - white-space: nowrap; -} - -.chat-thinking-title::before { - content: ""; - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--md-sys-color-primary); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--md-sys-color-primary) 14%, transparent); - animation: thinkingPulse 1.35s ease-in-out infinite; -} - -@keyframes thinkingPulse { - 0%, 100% { - transform: scale(0.9); - opacity: 0.72; - } - 50% { - transform: scale(1.12); - opacity: 1; - } -} - -.chat-thinking-content { - padding: 9px 12px 10px; - font-size: 0.76rem; - line-height: 1.45; - color: var(--md-sys-color-on-surface-variant); - opacity: 0.78; - border-top: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent); - white-space: pre-wrap; - font-family: var(--font-family-monospace, monospace); - background: color-mix(in srgb, var(--md-sys-color-surface) 28%, transparent); - max-height: 150px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: - color-mix(in srgb, var(--md-sys-color-primary) 38%, var(--md-sys-color-outline)) - color-mix(in srgb, var(--md-sys-color-surface) 65%, transparent); -} - -.chat-thinking-content::-webkit-scrollbar { - width: 10px; -} - -.chat-thinking-content::-webkit-scrollbar-track { - background: color-mix(in srgb, var(--md-sys-color-surface) 60%, transparent); - border-left: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent); -} - -.chat-thinking-content::-webkit-scrollbar-thumb { - min-height: 34px; - border-radius: 999px; - border: 2px solid transparent; - background: - linear-gradient( - 180deg, - color-mix(in srgb, var(--md-sys-color-primary) 62%, #ffffff 8%), - color-mix(in srgb, var(--md-sys-color-primary) 38%, var(--md-sys-color-outline)) - ) - border-box; - background-clip: padding-box; -} - -.chat-thinking-content::-webkit-scrollbar-thumb:hover { - background: - linear-gradient( - 180deg, - color-mix(in srgb, var(--md-sys-color-primary) 78%, #ffffff 10%), - color-mix(in srgb, var(--md-sys-color-primary) 52%, var(--md-sys-color-outline)) - ) - border-box; - background-clip: padding-box; -} - -/* ============================================================ - PROFESSIONAL SETTINGS UI - Collapsible Cards, Tabs, Hardware Badge - ============================================================ */ - -/* --- Section Tabs (Image | Text) --- */ -.settings-tabs-container { - display: flex; - justify-content: center; - margin-bottom: 24px; -} - -.settings-tabs { - display: flex; - gap: 4px; - padding: 4px; - background: var(--md-sys-color-surface-variant); - border-radius: 14px; - border: 1px solid var(--border-color); - width: fit-content; -} - -.settings-tab { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 24px; - border-radius: 12px; - border: none; - font-weight: 600; - font-size: 0.9rem; - cursor: pointer; - transition: all 0.25s var(--md-transition-easing); - color: var(--md-sys-color-on-surface-variant); - background: transparent; - position: relative; - overflow: hidden; -} - -.settings-tab:hover { - background: var(--md-sys-color-surface); - color: var(--md-sys-color-on-surface); -} - -.settings-tab.active { - background: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); - box-shadow: 0 4px 16px rgba(79, 70, 229, 0.25); -} - -.settings-tab .tab-icon { - transition: transform 0.3s ease; -} - -.settings-tab.active .tab-icon { - transform: scale(1.1); -} - -/* --- Hardware Tier Badge --- */ -.hardware-tier-badge { - border-radius: 20px; - padding: 24px; - margin-bottom: 28px; - position: relative; - overflow: hidden; - border: 1px solid var(--md-sys-color-card-border); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - background: var(--md-sys-color-surface); - box-shadow: var(--glass-shadow); - transition: all 0.3s ease; -} - -.hardware-tier-badge:hover { - box-shadow: 0 12px 40px rgba(15, 23, 42, 0.1); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 15%, var(--md-sys-color-card-border)); -} - -html[data-theme^="dark"] .hardware-tier-badge:hover { - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); -} - -.hardware-tier-badge::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - border-radius: 20px 20px 0 0; -} - -.hardware-tier-badge.tier-high::before { - background: linear-gradient(90deg, #22c55e, #10b981); - box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3); -} - -.hardware-tier-badge.tier-mid::before { - background: linear-gradient(90deg, #3b82f6, #0ea5e9); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); -} - -.hardware-tier-badge.tier-low::before { - background: linear-gradient(90deg, #f59e0b, #fbbf24); - box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3); -} - -.hardware-tier-header { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 12px; -} - -.hardware-tier-icon { - width: 48px; - height: 48px; - border-radius: 14px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - flex-shrink: 0; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); -} - -.tier-high .hardware-tier-icon { - background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(16, 185, 129, 0.1)); - border: 1px solid rgba(34, 197, 94, 0.2); -} - -.tier-mid .hardware-tier-icon { - background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(14, 165, 233, 0.1)); - border: 1px solid rgba(59, 130, 246, 0.2); -} - -.tier-low .hardware-tier-icon { - background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(251, 191, 36, 0.1)); - border: 1px solid rgba(245, 158, 11, 0.2); -} - -.hardware-tier-info { - flex: 1; -} - -.hardware-tier-name { - font-size: 1.15rem; - font-weight: 700; - color: var(--md-sys-color-on-surface); - margin-bottom: 4px; - letter-spacing: -0.01em; -} - -.hardware-tier-specs { - font-size: 0.8rem; - color: var(--md-sys-color-on-surface-variant); - line-height: 1.4; - font-weight: 500; -} - -.hardware-tier-divider { - height: 1px; - background: var(--border-color); - margin: 14px 0; -} - -.hardware-tier-chips { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.hardware-tier-chip { - font-size: 0.72rem; - padding: 5px 14px; - border-radius: 20px; - background: var(--md-sys-color-surface-variant); - font-weight: 600; - color: var(--md-sys-color-on-surface-variant); - border: 1px solid var(--border-color); - transition: all 0.2s ease; - letter-spacing: 0.01em; -} - -.hardware-tier-chip:hover { - background: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 25%, var(--border-color)); - transform: translateY(-1px); - box-shadow: 0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent); -} - -/* --- Collapsible Card --- */ -.collapsible-card { - border: 1px solid var(--md-sys-color-card-border); - border-radius: 16px; - overflow: hidden; - margin-bottom: 16px; - background: var(--md-sys-color-surface); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - box-shadow: var(--glass-shadow); - transition: all 0.3s ease; -} - -.collapsible-card:hover { - box-shadow: 0 12px 40px rgba(15, 23, 42, 0.08); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 15%, var(--md-sys-color-card-border)); - transform: translateY(-1px); -} - -html[data-theme^="dark"] .collapsible-card:hover { - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 20%, var(--md-sys-color-card-border)); -} - -.collapsible-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18px 22px; - cursor: pointer; - user-select: none; - transition: background 0.25s ease; - border: none; - width: 100%; - background: transparent; - color: inherit; - font-family: inherit; - text-align: left; -} - -.collapsible-header:hover { - background: var(--md-sys-color-surface-variant); -} - -.collapsible-header-left { - display: flex; - align-items: center; - gap: 14px; -} - -.collapsible-header-icon { - width: 36px; - height: 36px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, - color-mix(in srgb, var(--md-sys-color-primary) 12%, transparent), - color-mix(in srgb, var(--md-sys-color-secondary) 8%, transparent) - ); - color: var(--md-sys-color-primary); - flex-shrink: 0; - border: 1px solid color-mix(in srgb, var(--md-sys-color-primary) 10%, transparent); -} - -.collapsible-header-title { - font-size: 0.95rem; - font-weight: 700; - color: var(--md-sys-color-on-surface); - letter-spacing: -0.01em; -} - -.collapsible-header-subtitle { - font-size: 0.75rem; - color: var(--md-sys-color-outline); - margin-top: 2px; - font-weight: 500; -} - -.collapsible-chevron { - transition: transform 0.35s var(--md-transition-easing); - color: var(--md-sys-color-outline); - flex-shrink: 0; -} - -.collapsible-chevron.expanded { - transform: rotate(180deg); - color: var(--md-sys-color-primary); -} - -.collapsible-content { - padding: 0 22px 22px; - animation: fadeInSlide 0.3s var(--md-transition-easing); -} - -@keyframes fadeInSlide { - from { - opacity: 0; - transform: translateY(-8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* --- Settings Section Header --- */ -.settings-section-header { - display: flex; - align-items: center; - gap: 14px; - margin: 36px 0 20px 0; - padding: 14px 20px; - border-radius: 14px; - background: var(--md-sys-color-surface); - border: 1px solid var(--md-sys-color-card-border); - border-left: 3px solid; - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - box-shadow: var(--glass-shadow); - transition: all 0.3s ease; -} - -.settings-section-header:hover { - border-color: color-mix(in srgb, var(--md-sys-color-primary) 20%, var(--md-sys-color-card-border)); - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); -} - -html[data-theme^="dark"] .settings-section-header:hover { - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); -} - -.settings-section-icon { - width: 40px; - height: 40px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - background: linear-gradient(135deg, - color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent), - color-mix(in srgb, var(--md-sys-color-secondary) 10%, transparent) - ); - border: 1px solid color-mix(in srgb, var(--md-sys-color-primary) 12%, transparent); -} - -.settings-section-title { - font-size: 1.1rem; - font-weight: 700; - color: var(--md-sys-color-on-surface); - flex: 1; - letter-spacing: -0.01em; -} - -.settings-section-count { - font-size: 0.72rem; - padding: 4px 12px; - border-radius: 999px; - background: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); - font-weight: 700; - border: 1px solid color-mix(in srgb, var(--md-sys-color-primary) 15%, transparent); - letter-spacing: 0.02em; -} - -/* --- Collapsible Header Badge --- */ -.collapsible-header-badge { - font-size: 0.7rem; - padding: 2px 10px; - border-radius: 999px; - margin-left: 10px; - font-weight: 700; - vertical-align: middle; -} - -.collapsible-header-right { - display: flex; - align-items: center; - gap: 12px; -} - -/* --- Settings Grid (Single Column) --- */ -.settings-grid { - display: flex; - flex-direction: column; - gap: 0; - max-width: 900px; - margin: 0 auto; -} - -/* --- Premium Toggle Switch --- */ -.premium-toggle { - display: flex; - align-items: center; - gap: 14px; - padding: 14px 18px; - border-radius: 14px; - background: var(--md-sys-color-surface-variant); - border: 1px solid var(--border-color); - transition: all 0.25s ease; - cursor: pointer; -} - -.premium-toggle:hover { - border-color: color-mix(in srgb, var(--md-sys-color-primary) 25%, var(--border-color)); - background: color-mix(in srgb, var(--md-sys-color-primary-container) 30%, var(--md-sys-color-surface-variant)); - transform: translateX(2px); -} - -.premium-toggle-checkbox { - width: 22px; - height: 22px; - border-radius: 6px; - border: 2px solid var(--md-sys-color-outline); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - flex-shrink: 0; - background: transparent; - cursor: pointer; -} - -.premium-toggle-checkbox.checked { - background: var(--md-sys-color-primary); - border-color: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); - box-shadow: 0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 35%, transparent); -} - -.premium-toggle-label { - font-size: 0.85rem; - font-weight: 600; - color: var(--md-sys-color-on-surface); -} - -.premium-toggle-desc { - font-size: 0.75rem; - color: var(--md-sys-color-outline); - margin-top: 3px; - line-height: 1.4; -} - -/* --- Settings Value Badge --- */ -.settings-value-badge { - font-size: 0.72rem; - padding: 3px 10px; - border-radius: 8px; - background: linear-gradient(135deg, - var(--md-sys-color-primary-container), - color-mix(in srgb, var(--md-sys-color-secondary-container) 50%, var(--md-sys-color-primary-container)) - ); - color: var(--md-sys-color-on-primary-container); - font-weight: 700; - font-variant-numeric: tabular-nums; - border: 1px solid color-mix(in srgb, var(--md-sys-color-primary) 12%, transparent); - box-shadow: 0 1px 4px color-mix(in srgb, var(--md-sys-color-primary) 10%, transparent); - letter-spacing: 0.02em; -} - -/* --- Empty State for Settings --- */ -.settings-empty-state { - text-align: center; - padding: 60px 20px; - color: var(--md-sys-color-outline); -} - -.settings-empty-state-icon { - font-size: 3rem; - margin-bottom: 16px; - opacity: 0.5; -} - -.settings-empty-state-title { - font-size: 1.1rem; - font-weight: 600; - color: var(--md-sys-color-on-surface); - margin-bottom: 8px; -} - -.settings-empty-state-desc { - font-size: 0.85rem; - max-width: 400px; - margin: 0 auto; - line-height: 1.5; -} - -/* --- Settings Two Column Layout --- */ -.settings-two-column { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin-bottom: 20px; -} - -.settings-column { - display: flex; - flex-direction: column; - gap: 16px; -} - -.settings-subsection { - background: var(--md-sys-color-surface); - border: 1px solid var(--md-sys-color-card-border); - border-radius: 16px; - padding: 20px; - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - box-shadow: var(--glass-shadow); - transition: all 0.3s ease; -} - -.settings-option-desc { - font-size: 0.75rem; - color: var(--md-sys-color-outline); - font-weight: 300; - opacity: 0.85; - line-height: 1.4; -} - -.settings-subsection:hover { - box-shadow: 0 12px 40px rgba(15, 23, 42, 0.08); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 15%, var(--md-sys-color-card-border)); - transform: translateY(-1px); -} - -html[data-theme^="dark"] .settings-subsection:hover { - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); - border-color: color-mix(in srgb, var(--md-sys-color-primary) 20%, var(--md-sys-color-card-border)); -} - -.settings-subsection-title { - display: flex; - align-items: center; - gap: 10px; - font-size: 0.88rem; - font-weight: 700; - color: var(--md-sys-color-on-surface); - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid var(--border-color); - letter-spacing: -0.01em; -} - -.settings-subsection-title svg { - color: var(--md-sys-color-primary); - opacity: 0.85; -} - -/* --- Settings Entrance Animation --- */ -@keyframes settingsFadeIn { - from { - opacity: 0; - transform: translateY(12px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.settings-two-column { - animation: settingsFadeIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; -} - -.settings-subsection { - animation: settingsFadeIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; -} - -.settings-subsection:nth-child(1) { animation-delay: 0.05s; } -.settings-subsection:nth-child(2) { animation-delay: 0.1s; } -.settings-subsection:nth-child(3) { animation-delay: 0.15s; } -.settings-subsection:nth-child(4) { animation-delay: 0.2s; } - -/* --- Responsive: Stack on mobile --- */ -@media (max-width: 900px) { - .settings-two-column { - grid-template-columns: 1fr; - } -} - -/* --- Responsive Adjustments --- */ @media (max-width: 600px) { - .hardware-tier-badge { - padding: 16px; - border-radius: 16px; - } - - .hardware-tier-header { - gap: 12px; - } - - .hardware-tier-icon { - width: 40px; - height: 40px; - font-size: 1.2rem; - } - - .hardware-tier-name { - font-size: 1rem; - } - - .collapsible-header { - padding: 14px 16px; - } - - .collapsible-content { - padding: 0 16px 16px; - } - - .settings-section-header { - padding: 12px 16px; - margin: 24px 0 16px 0; - } - - .settings-section-icon { - width: 36px; - height: 36px; - } - - .settings-section-title { - font-size: 1rem; - } - - .settings-page-header { - padding: 18px 20px; - margin-bottom: 20px; - } - - .settings-page-title { - font-size: 1.2rem; - } + .app-header { padding: 10px 16px; } + .app-nav { padding: 6px 16px; } + .workspace-area { padding: 16px; } } - -/* --- Settings Page Header --- */ -.settings-page-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 28px; - padding: 24px 28px; - border-radius: var(--md-shape-corner-large); - background: linear-gradient(135deg, - color-mix(in srgb, var(--md-sys-color-primary-container) 60%, transparent), - color-mix(in srgb, var(--md-sys-color-secondary-container) 40%, transparent) - ); - border: 1px solid var(--md-sys-color-card-border); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - box-shadow: var(--glass-shadow); - position: relative; - overflow: hidden; -} - -.settings-page-header::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient(90deg, var(--md-sys-color-primary), var(--md-sys-color-secondary)); - opacity: 0.6; -} - -.settings-page-title { - font-size: 1.5rem; - font-weight: 800; - color: var(--md-sys-color-on-surface); - display: flex; - align-items: center; - gap: 14px; - letter-spacing: -0.02em; -} - -.settings-page-title svg { - filter: drop-shadow(0 2px 8px color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent)); -} - -.settings-page-subtitle { - font-size: 0.85rem; - color: var(--md-sys-color-on-surface-variant); - margin-top: 6px; - font-weight: 500; - opacity: 0.85; -} - -.sidebar-history-delete { - transition: color 0.2s, background-color 0.2s, opacity 0.2s; - border-radius: 4px; - opacity: 0.6; -} - -.sidebar-history-delete:hover { - color: var(--md-sys-color-error) !important; - background-color: rgba(239, 68, 68, 0.1) !important; - opacity: 1 !important; -} - -.sidebar-history-item:hover .sidebar-history-delete { - opacity: 1; -} - -/* Global modern scrollbars */ -::-webkit-scrollbar { - width: 6px; - height: 6px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: color-mix(in srgb, var(--md-sys-color-outline) 25%, transparent); - border-radius: 99px; -} -::-webkit-scrollbar-thumb:hover { - background: color-mix(in srgb, var(--md-sys-color-outline) 45%, transparent); -} - -/* Firefox */ -* { - scrollbar-width: thin; - scrollbar-color: color-mix(in srgb, var(--md-sys-color-outline) 25%, transparent) transparent; -} - -.settings-expanded-content { - animation: fadeInSlide 0.3s var(--md-transition-easing); -} - diff --git a/app/frontend/src/App.jsx b/app/frontend/src/App.jsx index 0872619..4e35aa1 100644 --- a/app/frontend/src/App.jsx +++ b/app/frontend/src/App.jsx @@ -1,13 +1,8 @@ -import { useState, useEffect, useRef, useCallback, useMemo } from "react"; -import Sidebar from "./components/Sidebar"; -import TopStatusBar from "./components/TopStatusBar"; -import Generator from "./components/Generator"; +import { useState, useEffect, useRef, useCallback } from "react"; +import SpeechTranscriber from "./components/SpeechTranscriber"; import ModelManager from "./components/ModelManager"; import Settings from "./components/Settings"; -import TextChat from "./components/TextChat"; -import SpeechTranscriber from "./components/SpeechTranscriber"; -import TextToSpeech from "./components/TextToSpeech"; -import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, listLlmConversations, saveLlmConversation, deleteLlmConversation, listSpeechTranscriptions, deleteSpeechTranscription, listTtsOutputs, deleteTtsOutput, stopServer } from "./services/api"; +import { getHardwareSpecs, getTelemetry, listSpeechTranscriptions, deleteSpeechTranscription } from "./services/api"; import "./App.css"; function App() { @@ -47,122 +42,10 @@ function App() { localStorage.setItem("theme", theme); }, [theme]); - // Navigation - const [activeTab, setActiveTab] = useState("generator"); - - // Prompts - const [prompt, setPrompt] = useState(""); - const [negativePrompt, setNegativePrompt] = useState(""); - - // Model & Server Status - const [activeModel, setActiveModel] = useState(null); - const [serverRunning, setServerRunning] = useState(false); - const [isStoppingServer, setIsStoppingServer] = useState(false); - const [isLlmLoaded, setIsLlmLoaded] = useState(false); - const [backendOptions, setBackendOptions] = useState({ - options: [{ id: "cpu", label: "CPU", available: true }], - cudaAvailable: false, - vulkanAvailable: false, - defaultBackendType: "cpu", - }); - const [health, setHealth] = useState(null); - const [cleanupItems, setCleanupItems] = useState([]); - const [isReadinessBusy, setIsReadinessBusy] = useState(false); - const [diagnosticsCopied, setDiagnosticsCopied] = useState(false); - - // Gallery History - const [generatedImages, setGeneratedImages] = useState([]); - - // Generation status - const [isGenerating, setIsGenerating] = useState(false); - const [generationProgress, setGenerationProgress] = useState(0); - - // System Specifications & Telemetry - const [specs, setSpecs] = useState({ - os_name: "Loading Specs...", - cpu_name: "Loading Specs...", - cpu_cores_physical: 4, - cpu_cores_logical: 8, - ram_total_gb: 0, - gpu_name: "Loading Specs...", - }); - - const [telemetry, setTelemetry] = useState({ - cpu_usage: 0, - ram_used_gb: 0, - ram_total_gb: 0, - gpu_name: "Detecting...", - vram_used_gb: 0, - vram_total_gb: 0, - }); - - // Default Image Constraints - const [constraints, setConstraints] = useState({ - width: 1024, - height: 1024, - steps: 4, // Recommended default steps for Flux.1 Schnell/Lightning - npuSteps: 4, - standardSteps: 20, - cfgScale: 1.0, // Recommended default CFG for Flux.1 Schnell - sampler: "euler_a", - seed: -1, - denoisingStrength: 0.7, - useGpu: true, - useTaesd: true, - useFlashAttn: true, - useTiling: false, - vaeTiling: true, - vaeOnCpu: false, - threads: 4, - backendType: "auto", - }); - - const [textSettings, setTextSettings] = useState(() => { - const saved = localStorage.getItem("textSettings"); - if (saved) { - try { - const parsed = JSON.parse(saved); - return { - ...parsed, - enableThinking: parsed.enableThinking === true, - maxTokens: !parsed.maxTokens || parsed.maxTokens === 384 ? 1024 : parsed.maxTokens, - responseTokenMode: parsed.responseTokenMode || "auto", - }; - } catch (_) {} - } - return { - contextSize: 0, - temperature: 0.7, - systemPrompt: "You are a helpful local AI assistant.", - threads: Math.max(4, Math.min(16, (navigator.hardwareConcurrency || 4) - 2)), - enableThinking: false, - // New performance settings - gpuLayers: -1, - maxTokens: 1024, - responseTokenMode: "auto", - topP: 0.95, - topK: 40, - minP: 0.05, - repeatPenalty: 1.1, - seed: null, - performanceProfile: "balanced", - flashAttn: true, - cacheTypeK: "q8_0", - cacheTypeV: "q8_0", - mlock: false, - mmap: true, - cachePrompt: true, - defragThold: 0.1, - batchSize: 512, - ubatchSize: 512, - preferredBackend: "auto", - }; - }); - - useEffect(() => { - localStorage.setItem("textSettings", JSON.stringify(textSettings)); - }, [textSettings]); + // Navigation - only speech-related tabs + const [activeTab, setActiveTab] = useState("speech"); + // Speech settings const [speechSettings, setSpeechSettings] = useState(() => { const saved = localStorage.getItem("speechSettings"); if (saved) { @@ -188,173 +71,28 @@ function App() { localStorage.setItem("speechSettings", JSON.stringify(speechSettings)); }, [speechSettings]); - const [ttsSettings, setTtsSettings] = useState(() => { - const saved = localStorage.getItem("ttsSettings"); - if (saved) { - try { - const parsed = JSON.parse(saved); - return { - model: parsed.model || "", - voice: parsed.voice || "af_heart", - speed: Math.max(0.5, Math.min(2, Number(parsed.speed) || 1)), - }; - } catch (_) {} - } - return { - model: "", - voice: "af_heart", - speed: 1, - }; + // System Specifications & Telemetry + const [specs, setSpecs] = useState({ + os_name: "Loading Specs...", + cpu_name: "Loading Specs...", + cpu_cores_physical: 4, + cpu_cores_logical: 8, + ram_total_gb: 0, + gpu_name: "Loading Specs...", }); - useEffect(() => { - localStorage.setItem("ttsSettings", JSON.stringify(ttsSettings)); - }, [ttsSettings]); - - // Lifted Chat History States - const [conversations, setConversations] = useState([]); - const [activeConversationId, setActiveConversationId] = useState(null); - const [showHistory, setShowHistory] = useState(false); // Default hide - const [sidebarVisible, setSidebarVisible] = useState(() => { - const saved = localStorage.getItem("sidebarVisible"); - return saved !== "false"; + const [telemetry, setTelemetry] = useState({ + cpu_usage: 0, + ram_used_gb: 0, + ram_total_gb: 0, + gpu_name: "Detecting...", + vram_used_gb: 0, + vram_total_gb: 0, }); - useEffect(() => { - localStorage.setItem("sidebarVisible", String(sidebarVisible)); - }, [sidebarVisible]); - - const [fontSize, setFontSize] = useState(() => { - const saved = localStorage.getItem("fontSize"); - return saved ? Number(saved) : 16; - }); - - useEffect(() => { - document.documentElement.style.setProperty("--app-font-size", `${fontSize}px`); - localStorage.setItem("fontSize", String(fontSize)); - }, [fontSize]); + // Speech transcriptions history const [speechTranscriptions, setSpeechTranscriptions] = useState([]); const [selectedSpeechTranscript, setSelectedSpeechTranscript] = useState(null); - const [showSpeechHistory, setShowSpeechHistory] = useState(false); - const [ttsOutputs, setTtsOutputs] = useState([]); - const [selectedTtsOutput, setSelectedTtsOutput] = useState(null); - const [showTtsHistory, setShowTtsHistory] = useState(false); - - const sanitizeMessageForStorage = (message) => { - if (!Array.isArray(message?.content)) return message; - return { - ...message, - content: message.content.map((item) => { - if (item?.type !== "image_url") return item; - return { - type: "text", - text: "[Attached image omitted from saved chat history]", - }; - }), - }; - }; - - const sanitizeConversationForStorage = (conversation) => ({ - ...conversation, - messages: Array.isArray(conversation.messages) - ? conversation.messages.map(sanitizeMessageForStorage) - : [], - }); - - const persistConversation = (conversation) => { - const compactConversation = sanitizeConversationForStorage(conversation); - try { - saveLlmConversation(compactConversation).catch((err) => { - console.warn("Could not save chat history to app folder:", err); - }); - } catch (err) { - console.warn("Could not save chat history:", err); - } - }; - - useEffect(() => { - let cancelled = false; - async function loadConversations() { - let legacyConversations = []; - const legacySaved = localStorage.getItem("chat_conversations"); - if (legacySaved) { - try { - legacyConversations = JSON.parse(legacySaved); - } catch (_) { - localStorage.removeItem("chat_conversations"); - } - } - - try { - const diskConversations = await listLlmConversations(); - const diskIds = new Set(diskConversations.map((conversation) => conversation.id)); - const missingLegacy = legacyConversations - .map(sanitizeConversationForStorage) - .filter((conversation) => conversation.id && !diskIds.has(conversation.id)); - - if (missingLegacy.length > 0) { - await Promise.all(missingLegacy.map((conversation) => saveLlmConversation(conversation))); - } - - const merged = [...diskConversations, ...missingLegacy] - .sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0)); - if (!cancelled) setConversations(merged); - if (legacySaved) localStorage.removeItem("chat_conversations"); - } catch (err) { - console.warn("Could not load chat history from app folder:", err); - if (!cancelled && legacyConversations.length > 0) { - setConversations(legacyConversations.map(sanitizeConversationForStorage)); - } - } - } - loadConversations(); - return () => { - cancelled = true; - }; - }, []); - - const saveConversationState = useCallback((id, msgs, modelName, newTitle = null) => { - setConversations((prev) => { - const list = [...prev]; - const idx = list.findIndex(c => c.id === id); - let conversation; - if (idx !== -1) { - conversation = { - ...list[idx], - messages: msgs, - timestamp: Date.now(), - model: modelName, - ...(newTitle ? { title: newTitle } : {}) - }; - list[idx] = conversation; - } else { - conversation = { - id, - title: newTitle || "Chat Session", - model: modelName, - messages: msgs, - timestamp: Date.now() - }; - list.unshift(conversation); - } - persistConversation(conversation); - return list; - }); - }, []); - - const handleDeleteConversation = useCallback((id, e) => { - if (e) e.stopPropagation(); - setConversations((prev) => { - const filtered = prev.filter((c) => c.id !== id); - return filtered; - }); - deleteLlmConversation(id).catch((err) => { - console.warn("Could not delete chat history from app folder:", err); - }); - if (activeConversationId === id) { - setActiveConversationId(null); - } - }, [activeConversationId]); const refreshSpeechTranscriptions = useCallback(async () => { try { @@ -401,65 +139,13 @@ function App() { refreshSpeechTranscriptions(); }, [refreshSpeechTranscriptions]); - const refreshTtsOutputs = useCallback(async () => { - try { - const list = await listTtsOutputs(); - setTtsOutputs(list); - } catch (err) { - console.warn("Could not load TTS outputs:", err); - setTtsOutputs([]); - } - }, []); - - const handleDeleteTtsOutput = useCallback(async (item, e) => { - if (e) e.stopPropagation(); - const itemId = item.filename || item.metadata; - if (!itemId) return; - - const ok = await showConfirm({ - title: "Delete TTS Output", - message: `Are you sure you want to delete "${item.displayName || item.audioFile || "this audio"}"? This action cannot be undone.`, - confirmLabel: "Delete", - danger: true - }); - if (!ok) return; - - try { - await deleteTtsOutput(itemId); - setSelectedTtsOutput((prev) => { - if (prev) { - const prevId = prev.filename || prev.metadata; - if (prevId === itemId) return null; - } - return prev; - }); - refreshTtsOutputs(); - } catch (err) { - console.error("Failed to delete TTS output:", err); - showAlert({ title: "Delete Failed", message: err.message || String(err), danger: true }); - } - }, [showConfirm, showAlert, refreshTtsOutputs]); - - useEffect(() => { - refreshTtsOutputs(); - }, [refreshTtsOutputs]); - // Load hardware specifications on mount useEffect(() => { async function loadSpecs() { try { const hardware = await getHardwareSpecs(); - const backendInfo = await getBackendOptions(); - setBackendOptions(backendInfo); setSpecs(hardware); - // Default CPU threads count to match physical cores - setConstraints((prev) => ({ - ...prev, - threads: hardware.cpu_cores_physical || 4, - backendType: prev.backendType === "auto" ? backendInfo.defaultBackendType || "cpu" : prev.backendType, - useGpu: (prev.backendType === "auto" ? backendInfo.defaultBackendType : prev.backendType) !== "cpu", - })); - setTextSettings((prev) => ({ + setSpeechSettings((prev) => ({ ...prev, threads: prev.threads || hardware.cpu_cores_physical || 4 })); @@ -470,120 +156,6 @@ function App() { loadSpecs(); }, []); - const refreshReadiness = useCallback(async () => { - setIsReadinessBusy(true); - try { - const [healthInfo, candidates] = await Promise.all([ - getHealth(), - getCleanupCandidates().catch(() => []), - ]); - setHealth(healthInfo); - setCleanupItems(candidates); - } finally { - setIsReadinessBusy(false); - } - }, []); - - useEffect(() => { - refreshReadiness(); - }, [refreshReadiness]); - - const copyDiagnostics = useCallback(async () => { - try { - const diagnostics = await getDiagnostics(); - await navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2)); - setDiagnosticsCopied(true); - setTimeout(() => setDiagnosticsCopied(false), 1800); - } catch (err) { - showAlert({ title: "Diagnostics Failed", message: err.message || String(err), danger: true }); - } - }, [showAlert]); - - const cleanupSafeItems = useCallback(async () => { - if (cleanupItems.length === 0) return; - const totalBytes = cleanupItems.reduce((sum, item) => sum + Number(item.sizeBytes || 0), 0); - const ok = await showConfirm({ - title: "Clean Safe Files?", - message: `Delete ${cleanupItems.length} temporary/cache item${cleanupItems.length === 1 ? "" : "s"} and free about ${formatBytes(totalBytes)}?\n\nModels and generated outputs are not included.`, - confirmLabel: "Clean", - danger: true, - }); - if (!ok) return; - - setIsReadinessBusy(true); - try { - await cleanupCandidates(cleanupItems.map((item) => item.id)); - await refreshReadiness(); - } catch (err) { - showAlert({ title: "Cleanup Failed", message: err.message || String(err), danger: true }); - } finally { - setIsReadinessBusy(false); - } - }, [cleanupItems, refreshReadiness, showAlert, showConfirm]); - - useEffect(() => { - async function loadSavedOutputs() { - const outputs = await listGeneratedOutputs(); - setGeneratedImages(outputs); - } - loadSavedOutputs(); - }, []); - - // Save active model configuration to localStorage for persistence across reloads - useEffect(() => { - if (activeModel) { - localStorage.setItem("active-model", activeModel); - } else { - localStorage.removeItem("active-model"); - } - }, [activeModel]); - - // Check if server is already running on mount (handles page refresh and startup lag) - useEffect(() => { - let checkInterval = null; - - async function checkServerRunning() { - const status = await getBackendStatus(); - const isRunning = Boolean(status.ready); - if (isRunning) { - if (checkInterval) clearInterval(checkInterval); - - const savedModel = localStorage.getItem("active-model"); - if (savedModel) { - setActiveModel(savedModel); - setServerRunning(true); - } else { - // Query the active model from status if possible, otherwise use fallback - try { - if (status && status.settings && status.settings.model) { - const modelName = status.settings.model.split(/[\\/]/).pop(); - setActiveModel(modelName); - } - } catch (_) {} - setServerRunning(true); - } - } else { - // If server is not running, check if they had a simulated model active in browser mode - const isTauriDesktop = typeof window !== "undefined" && window.__TAURI_INTERNALS__ !== undefined; - if (!isTauriDesktop) { - const savedModel = localStorage.getItem("active-model"); - if (savedModel) { - setActiveModel(savedModel); - setServerRunning(false); - } - } - } - } - - checkServerRunning(); - // Poll every 2 seconds to auto-detect when the backend becomes ready - checkInterval = setInterval(checkServerRunning, 2000); - - return () => { - if (checkInterval) clearInterval(checkInterval); - }; - }, []); - // Poll system telemetry usage statistics on interval useEffect(() => { async function updateTelemetry() { @@ -605,204 +177,62 @@ function App() { } updateTelemetry(); - const interval = setInterval(updateTelemetry, 1500); // Poll every 1.5 seconds + const interval = setInterval(updateTelemetry, 1500); return () => clearInterval(interval); }, []); - // Sync active model settings default parameters - useEffect(() => { - if (activeModel) { - const name = activeModel.toLowerCase(); - if (name.includes("lcm-dreamshaper-v7-fp16")) { - setConstraints((prev) => ({ - ...prev, - steps: 4, - npuSteps: 4, - cfgScale: 1.0, - width: 512, - height: 512, - backendType: "openvino-npu", - useGpu: true, - })); - } else if (name.includes("flux") || name.includes("schnell")) { - setConstraints((prev) => ({ - ...prev, - steps: 4, - standardSteps: 4, - cfgScale: 1.0, - width: 1024, - height: 1024, - })); - } else if (name.includes("lightning") || name.includes("turbo")) { - setConstraints((prev) => ({ - ...prev, - steps: 4, - standardSteps: 4, - cfgScale: 1.5, - width: 1024, - height: 1024, - })); - } else if (name.includes("sd15")) { - setConstraints((prev) => ({ - ...prev, - steps: 25, - standardSteps: 25, - cfgScale: 7.0, - width: 512, - height: 512, - })); - } else if (name.includes("sd35")) { - setConstraints((prev) => ({ - ...prev, - steps: 20, - standardSteps: 20, - cfgScale: 4.5, - width: 1024, - height: 1024, - })); - } else { - // Fallback for custom or SD 1.x models (like CyberRealistic or arbitrary safetensors) - setConstraints((prev) => ({ - ...prev, - steps: 20, - standardSteps: 20, - cfgScale: 7.0, - width: 512, - height: 512, - })); - } - } - }, [activeModel]); - - // Tab contents are kept mounted to preserve loading/generation state when switching tabs - - const sidebarContent = useMemo(() => ( - - ), [sidebarVisible, activeTab, specs, conversations, activeConversationId, showHistory, handleDeleteConversation, speechTranscriptions, selectedSpeechTranscript, showSpeechHistory, handleDeleteSpeechTranscription, ttsOutputs, selectedTtsOutput, showTtsHistory, handleDeleteTtsOutput]); - - const handleStopServer = useCallback(async () => { - if (!serverRunning || isStoppingServer) return; - setIsStoppingServer(true); - try { - await stopServer(); - setServerRunning(false); - setActiveModel(null); - } catch (err) { - console.error("Failed to stop server:", err); - showAlert({ title: "Stop Server Failed", message: err.message || String(err), danger: true }); - } finally { - setIsStoppingServer(false); - } - }, [serverRunning, isStoppingServer, showAlert]); - return (
- {/* Sidebar with Navigation Rail & Specs */} - {sidebarContent} - - {/* Main Panel */} -
- {/* Top telemetry specs chips bar */} - setSidebarVisible(!sidebarVisible)} - /> - - {/* Dynamic Workspace Container */} -
- + {/* Simple Header */} +
+
+

🎙️ Speech Transcriber

+

Local AI audio transcription with Whisper

- -
- +
+
+ CPU: {telemetry.cpu_usage}% + RAM: {telemetry.ram_used_gb.toFixed(1)}/{telemetry.ram_total_gb.toFixed(1)} GB + {telemetry.gpu_name !== "Detecting..." && telemetry.gpu_name !== "Unavailable" && ( + GPU: {telemetry.gpu_name} + )} +
+
+
-
- -
+ {/* Navigation Tabs */} + -
+ {/* Main Content */} +
+ {activeTab === "speech" && ( -
+ )} -
- + )} + + {activeTab === "settings" && ( + -
- -
- -
-
+ )} + + {/* Dialog */} {dialog && (
diff --git a/app/frontend/src/components/Generator.jsx b/app/frontend/src/components/Generator.jsx deleted file mode 100644 index f9143bc..0000000 --- a/app/frontend/src/components/Generator.jsx +++ /dev/null @@ -1,1001 +0,0 @@ -import React, { memo, useState, useRef, useCallback } from "react"; -import { Sparkles, Download, Copy, RefreshCw, Check, Sliders, Trash2, ImagePlus } from "lucide-react"; -import { - generateImage, - startServer, - stopServer, - waitForServerReady, - getBackendStatus, - getGenerationProgress, - saveGeneratedOutput, - deleteGeneratedOutputs, - getLlmStatus, - listLlmModels, - startLlm, - chatWithLlm -} from "../services/api"; - -const GalleryItem = memo(({ img, idx, isSelected, onClick }) => { - const handleClick = (e) => { - onClick(idx, e); - }; - return ( -
- Thumbnail - {isSelected && ( -
- -
- )} -
- ); -}); - -function Generator({ - prompt, - setPrompt, - negativePrompt, - setNegativePrompt, - constraints, - setConstraints, - activeModel, - generatedImages, - setGeneratedImages, - isGenerating, - setIsGenerating, - generationProgress, - setGenerationProgress, - setActiveTab, - showAlert = async ({ message }) => window.alert(message), - showConfirm = async ({ message }) => window.confirm(message), - specs, - textSettings, -}) { - const [outputImage, setOutputImage] = useState(null); - const [outputSeed, setOutputSeed] = useState(null); - const [genDuration, setGenDuration] = useState(null); - const [copySuccess, setCopySuccess] = useState(false); - const [elapsedTime, setElapsedTime] = useState(0); - const [estimatedLeftTime, setEstimatedLeftTime] = useState(0); - const [currentStep, setCurrentStep] = useState(0); - const [generationSpeed, setGenerationSpeed] = useState(""); - const [isCpuFallback, setIsCpuFallback] = useState(false); - const [isCancelling, setIsCancelling] = useState(false); - const [isRestartingBackend, setIsRestartingBackend] = useState(false); - const [restartLoadProgress, setRestartLoadProgress] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - const [isDecoding, setIsDecoding] = useState(false); - const [selectedGalleryIndexes, setSelectedGalleryIndexes] = useState([]); - const [baseImage, setBaseImage] = useState(null); - const [isEnhancing, setIsEnhancing] = useState(false); - const timerRef = useRef(null); - const abortControllerRef = useRef(null); - const hasRealGenerationStepRef = useRef(false); - - const handleEnhancePrompt = async () => { - if (!prompt.trim() || isEnhancing || isGenerating) return; - setIsEnhancing(true); - try { - const status = await getLlmStatus(); - let targetModel = status.settings?.model; - - if (!status.ready) { - const models = await listLlmModels(); - if (models.length === 0) { - showAlert({ - title: "No Text Models", - message: "You haven't downloaded any text models yet. Please go to the 'Model Manager' or 'Text Chat' tab to download a GGUF model first.", - danger: true - }); - setIsEnhancing(false); - return; - } - - const savedLlm = localStorage.getItem("selectedLlmModel") || models[0]?.filename; - targetModel = models.some(m => m.filename === savedLlm) ? savedLlm : models[0]?.filename; - - const confirm = await showConfirm({ - title: "Load Text Model?", - message: `Enhancing your prompt requires loading the local text model "${targetModel}". This will temporarily unload the image model from memory. Do you want to proceed?`, - confirmLabel: "Load & Enhance", - cancelLabel: "Cancel" - }); - - if (!confirm) { - setIsEnhancing(false); - return; - } - - const cores = textSettings?.threads || specs?.cpu_cores_physical || 4; - const context = textSettings?.contextSize || 2048; - await startLlm(targetModel, { - threads: cores, - contextSize: context, - gpuLayers: -1, - preferredBackend: textSettings?.preferredBackend - }); - } - - const systemPrompt = "You are a helpful assistant. Rewrite the user's image prompt to be more descriptive and detailed for image generation. Keep it under 80 words. Output ONLY the rewritten prompt, no explanation or introductory text."; - const messages = [ - { role: "system", content: systemPrompt }, - { role: "user", content: `Please expand this prompt: "${prompt}"` } - ]; - - const temp = textSettings?.temperature || 0.7; - const response = await chatWithLlm(messages, { temperature: temp, maxTokens: 128 }); - let cleanResponse = response.content.trim(); - if (cleanResponse.startsWith('"') && cleanResponse.endsWith('"')) { - cleanResponse = cleanResponse.slice(1, -1); - } - cleanResponse = cleanResponse.replace(/^Here is the expanded prompt:?\s*/i, ""); - cleanResponse = cleanResponse.replace(/^Enhanced prompt:?\s*/i, ""); - - setPrompt(cleanResponse); - } catch (err) { - showAlert({ - title: "Prompt Enhancement Failed", - message: err.message || String(err), - danger: true - }); - } finally { - setIsEnhancing(false); - } - }; - - React.useEffect(() => { - if (constraints.backendType === "apple-npu" || constraints.backendType === "openvino-npu") { - setBaseImage(null); - } - }, [constraints.backendType]); - - const handleBaseImageSelect = (e) => { - const file = e.target.files?.[0]; - e.target.value = ""; - if (!file) return; - if (!file.type.startsWith("image/")) { - setErrorMsg("Please choose an image file (PNG, JPG, or WEBP)."); - return; - } - const reader = new FileReader(); - reader.onload = () => setBaseImage(reader.result); - reader.onerror = () => setErrorMsg("Could not read the selected image file."); - reader.readAsDataURL(file); - }; - - const handleClearBaseImage = () => setBaseImage(null); - - // Trigger main image generation process - const handleGenerate = async () => { - if (!prompt.trim()) return; - - setErrorMsg(null); - setIsRestartingBackend(true); - setRestartLoadProgress(null); - - let needsRestart = false; - try { - const status = await getBackendStatus(); - if (status && status.settings) { - const settings = status.settings; - const currentModelName = settings.model ? settings.model.split(/[\\/]/).pop() : null; - const targetModelName = activeModel ? activeModel.split(/[\\/]/).pop() : null; - const requestedBackend = constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"); - const currentBackend = settings.backendType || (settings.useGpu === false ? "cpu" : "auto"); - const shouldCompareModel = requestedBackend !== "openvino-npu"; - const isCoreMLBackend = requestedBackend === "apple-npu"; - - if ((shouldCompareModel && currentModelName !== targetModelName) || - (!isCoreMLBackend && parseInt(settings.steps) !== parseInt(constraints.steps)) || - (!isCoreMLBackend && Math.abs(parseFloat(settings.cfgScale) - parseFloat(constraints.cfgScale)) > 0.05) || - (!isCoreMLBackend && settings.sampler !== constraints.sampler) || - (!isCoreMLBackend && parseInt(settings.threads) !== parseInt(constraints.threads)) || - Boolean(settings.useGpu) !== (constraints.useGpu !== false) || - parseInt(settings.width || 512) !== parseInt(constraints.width || 512) || - parseInt(settings.height || 512) !== parseInt(constraints.height || 512) || - currentBackend !== requestedBackend) { - needsRestart = true; - } - } else { - needsRestart = true; - } - } catch (e) { - needsRestart = true; - } - - if (needsRestart) { - try { - console.log("Settings out of sync, restarting backend..."); - await startServer(activeModel, constraints); - let isReady = false; - let crashError = null; - const maxStartupPolls = (constraints.backendType === "openvino-npu" || constraints.backendType === "apple-npu") ? 1200 : 240; - for (let i = 0; i < maxStartupPolls; i++) { - const status = await getBackendStatus(); - if (status.loading) { - setRestartLoadProgress({ - progress: status.loading.progress || 0, - phase: status.loading.phase || "Loading model...", - speed: status.loading.speed || "", - current: status.loading.current || 0, - total: status.loading.total || 0, - backendMode: status.loading.backendMode || status.settings?.backendMode || "", - device: status.loading.device || status.settings?.backendDevice || "", - }); - } - if (status.ready) { - isReady = true; - break; - } - if (status.error) { - crashError = status.error; - break; - } - if (!status.running && !status.loading?.active && i > 3) { - crashError = "The backend process terminated immediately on startup."; - break; - } - await new Promise((r) => setTimeout(r, 500)); - } - - if (!isReady) { - throw new Error(crashError || "Backend server failed to start after applying configuration changes."); - } - } catch (err) { - console.error("Failed to restart backend:", err); - setErrorMsg(err.message || "Failed to sync settings with backend."); - setIsRestartingBackend(false); - return; - } - } - setIsRestartingBackend(false); - setRestartLoadProgress(null); - - if (timerRef.current) clearInterval(timerRef.current); - setIsGenerating(true); - setGenerationProgress(0); - setElapsedTime(0); - setCurrentStep(0); - setGenerationSpeed(""); - hasRealGenerationStepRef.current = false; - setIsCpuFallback(false); - setIsDecoding(false); - setGenDuration(null); - setOutputImage(null); - - // Calculate dynamic step estimation times - // CPU takes ~35s per step at 512px, ~140s at 1024px. GPU is ~0.04x of CPU time. - const baseGpuStepTime = (constraints.width >= 1024 ? 140 : 35) * 0.04; - const baseCpuStepTime = (constraints.width >= 1024 ? 140 : 35); - - const gpuSelected = (constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto")) !== "cpu"; - let activeStepTime = gpuSelected ? baseGpuStepTime : baseCpuStepTime; - let cpuFallbackDetected = false; - let fallbackTime = 0; - let fallbackProgress = 0; - let elapsedSeconds = 0; - - setEstimatedLeftTime(Math.round(constraints.steps * activeStepTime)); - - timerRef.current = setInterval(async () => { - elapsedSeconds++; - setElapsedTime(elapsedSeconds); - - try { - const progress = await getGenerationProgress(); - if (progress && progress.active) { - const decoding = progress.decoding || false; - setIsDecoding(decoding); - - const step = decoding ? (progress.steps || constraints.steps || 20) : (progress.step || 0); - const steps = progress.steps || constraints.steps || 20; - const speed = decoding ? "" : (progress.speed || ""); - const numericSpeed = parseFloat(speed); - const backendMode = String(progress.backendMode || "").toLowerCase(); - const backendDevice = String(progress.backendDevice || "").toLowerCase(); - const isConfirmedGpuBackend = ( - backendMode.includes("gpu") || - backendMode.includes("vulkan") || - backendMode.includes("cuda") || - backendMode.includes("rocm") || - backendMode.includes("metal") || - backendDevice.includes("vulkan") || - backendDevice.includes("cuda") || - backendDevice.includes("rocm") - ); - - if (decoding || progress.steps > 0) { - hasRealGenerationStepRef.current = true; - setCurrentStep(step); - } - if (!decoding) { - setGenerationSpeed(speed); - } else { - setGenerationSpeed("decoding"); - } - - // Calculate progress percentage - let progressPercent = 0; - if (decoding) { - progressPercent = 99; - } else if (steps > 0) { - progressPercent = Math.round((step / steps) * 100); - } - progressPercent = Math.min(99, progressPercent); - if (decoding || progress.steps > 0) { - setGenerationProgress(progressPercent); - } else { - setGenerationProgress((prev) => Math.max(prev, progressPercent)); - } - - // Calculate remaining time - let remaining = 0; - if (decoding) { - remaining = 3; - } else { - if (!isNaN(numericSpeed) && numericSpeed > 0) { - const remainingSteps = Math.max(0, steps - step); - if (speed.includes("it/s")) { - remaining = remainingSteps / numericSpeed; - } else if (speed.includes("s/it")) { - remaining = remainingSteps * numericSpeed; - } - } else { - const activeStepTime = gpuSelected ? baseGpuStepTime : baseCpuStepTime; - remaining = Math.max(0, (steps - step) * activeStepTime); - } - } - - // Detect CPU fallback / slow generation if GPU was requested but it runs slow - if (gpuSelected && !cpuFallbackDetected && !isConfirmedGpuBackend) { - const isSlowItS = speed.includes("it/s") && numericSpeed < 0.2; - const isSlowSIt = speed.includes("s/it") && numericSpeed > 5.0; - if (isSlowItS || isSlowSIt) { - cpuFallbackDetected = true; - setIsCpuFallback(true); - } - } - - setEstimatedLeftTime(Math.round(remaining)); - } else { - // Creep up slowly while loading the model/preparing - setGenerationProgress((prev) => Math.min(15, prev + 1)); - setEstimatedLeftTime((prev) => Math.max(1, prev - 1)); - } - } catch (e) { - console.warn("Failed to get generation progress, running simulation fallback:", e); - - // Fallback simulation logic - if (gpuSelected && !cpuFallbackDetected && elapsedSeconds > constraints.steps * baseGpuStepTime * 1.3) { - cpuFallbackDetected = true; - setIsCpuFallback(true); - activeStepTime = baseCpuStepTime; - fallbackTime = elapsedSeconds; - fallbackProgress = Math.min(95, Math.round((elapsedSeconds / (constraints.steps * baseGpuStepTime)) * 100)); - } - - let step = Math.min(constraints.steps - 1, Math.floor(elapsedSeconds / activeStepTime)); - let expectedTotal = constraints.steps * activeStepTime; - if (elapsedSeconds >= expectedTotal) { - const computedStepTime = Math.ceil(elapsedSeconds / Math.max(0.5, step)); - activeStepTime = Math.max(activeStepTime + 15, computedStepTime + 15); - expectedTotal = constraints.steps * activeStepTime; - step = Math.min(constraints.steps - 1, Math.floor(elapsedSeconds / activeStepTime)); - } - const remaining = Math.max(1, expectedTotal - elapsedSeconds); - setEstimatedLeftTime(Math.round(remaining)); - - let progressPercent = 0; - if (cpuFallbackDetected) { - const remainingProgress = 99 - fallbackProgress; - const remainingTime = expectedTotal - fallbackTime; - if (remainingTime > 0) { - const ratio = Math.min(1.0, (elapsedSeconds - fallbackTime) / remainingTime); - if (ratio < 0.85) { - progressPercent = Math.round(fallbackProgress + ratio * remainingProgress); - } else { - const x = (elapsedSeconds - (fallbackTime + remainingTime * 0.85)) / (activeStepTime * 2); - const decay = 1 - Math.exp(-x); - progressPercent = Math.min(99, Math.round(fallbackProgress + 0.85 * remainingProgress + (0.14 * remainingProgress) * decay)); - } - } else { - progressPercent = 99; - } - } else { - if (elapsedSeconds < expectedTotal * 0.85) { - progressPercent = Math.round((elapsedSeconds / expectedTotal) * 100); - } else { - const x = (elapsedSeconds - expectedTotal * 0.85) / (activeStepTime * 2); - const decay = 1 - Math.exp(-x); - progressPercent = Math.min(99, Math.round(85 + 14 * decay)); - } - } - if (!hasRealGenerationStepRef.current) { - setGenerationProgress((p) => Math.max(p, progressPercent)); - } - } - }, 1000); - - try { - abortControllerRef.current = new AbortController(); - const result = await generateImage( - prompt, - negativePrompt, - constraints, - activeModel, - baseImage, - (prog) => setGenerationProgress((prev) => Math.max(prev, prog)), - abortControllerRef.current.signal - ); - - if (timerRef.current) clearInterval(timerRef.current); - setGenerationProgress(100); - setCurrentStep(constraints.steps); - setEstimatedLeftTime(0); - setGenerationSpeed(""); - - const metadata = { - prompt: prompt, - negativePrompt: negativePrompt, - seed: result.seed, - steps: constraints.steps, - cfgScale: constraints.cfgScale, - width: constraints.width, - height: constraints.height, - sampler: constraints.sampler, - model: activeModel, - mode: baseImage ? "img2img" : "txt2img", - denoisingStrength: baseImage ? constraints.denoisingStrength : null, - duration_sec: result.duration_sec, - timestamp: new Date().toLocaleTimeString(), - }; - let savedOutput = null; - let savedUrl = null; - try { - savedOutput = await saveGeneratedOutput(result.image, metadata); - savedUrl = savedOutput.url; - if (!savedUrl) { - throw new Error("The local server saved the image but did not return a file URL."); - } - } catch (saveErr) { - console.error("Generated image could not be saved:", saveErr); - throw new Error(`Image generated but could not be saved to USB: ${saveErr.message || saveErr}`); - } - - setOutputImage(savedUrl); - setOutputSeed(result.seed); - setGenDuration(result.duration_sec); - - const historyItem = { - ...metadata, - ...(savedOutput || {}), - url: savedUrl, - }; - - setGeneratedImages((prev) => [historyItem, ...prev]); - } catch (e) { - if (e.name === "AbortError") { - console.log("Generation request was aborted by the user"); - } else { - console.error("Generation failed:", e); - setErrorMsg(e.message || "Generation failed. Please try again."); - } - if (timerRef.current) clearInterval(timerRef.current); - } finally { - setIsGenerating(false); - abortControllerRef.current = null; - } - }; - - // Stop/Cancel Generation Handler - const handleCancelGeneration = async () => { - if (await showConfirm({ - title: "Stop Generation?", - message: "The current image generation will be cancelled and the local model server may restart.", - confirmLabel: "Stop", - danger: true, - })) { - if (timerRef.current) clearInterval(timerRef.current); - - // Set cancelling state first to keep the progress overlay active in cancelling mode - setIsCancelling(true); - - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - - try { - console.log("Cancelling: Stopping server to kill generation threads..."); - await stopServer(); - setCurrentStep(0); - setGenerationSpeed(""); - setIsDecoding(false); - hasRealGenerationStepRef.current = false; - - if (activeModel) { - console.log("Cancelling: Restarting server for future generations..."); - await startServer(activeModel, constraints); - const ready = await waitForServerReady(); - if (!ready) { - console.warn("Server failed to restart properly."); - } - } - } catch (err) { - console.error("Failed to restart server on cancellation:", err); - } finally { - setIsCancelling(false); - setIsGenerating(false); - setGenerationProgress(0); - setEstimatedLeftTime(0); - } - } - }; - - // Copy generated image to clipboard - const handleCopyImage = async () => { - if (!outputImage) return; - try { - if (outputImage.startsWith("data:image")) { - // base64 conversion for clipboard - const response = await fetch(outputImage); - const blob = await response.blob(); - await navigator.clipboard.write([ - new ClipboardItem({ [blob.type]: blob }) - ]); - } else { - // Remote Unsplash images fallback (draw on canvas then copy) - const img = new Image(); - img.crossOrigin = "anonymous"; - img.src = outputImage; - img.onload = async () => { - const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); - canvas.toBlob(async (blob) => { - await navigator.clipboard.write([ - new ClipboardItem({ [blob.type]: blob }) - ]); - }, "image/png"); - }; - } - setCopySuccess(true); - setTimeout(() => setCopySuccess(false), 2000); - } catch (err) { - console.error("Failed to copy image:", err); - } - }; - - // Download generated image - const handleDownload = () => { - if (!outputImage) return; - const link = document.createElement("a"); - link.href = outputImage; - link.download = `diffusion-${outputSeed || Date.now()}.png`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - const downloadImageUrl = (url, filename = `diffusion-${Date.now()}.png`) => { - if (!url) return; - const link = document.createElement("a"); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - // Reuse prompt settings from history - const loadHistoryItem = useCallback((item) => { - setPrompt(item.prompt); - setNegativePrompt(item.negativePrompt); - setConstraints((prev) => ({ - ...prev, - steps: item.steps, - cfgScale: item.cfgScale, - width: item.width, - height: item.height, - sampler: item.sampler, - seed: item.seed, - })); - setOutputImage(item.url); - setOutputSeed(item.seed); - setGenDuration(item.duration_sec ?? item.durationSec ?? null); - }, [setPrompt, setNegativePrompt, setConstraints]); - - const toggleGallerySelection = useCallback((idx, event) => { - const multi = event.ctrlKey || event.metaKey || event.shiftKey; - setSelectedGalleryIndexes((prev) => { - if (!multi) return [idx]; - return prev.includes(idx) ? prev.filter((item) => item !== idx) : [...prev, idx]; - }); - if (generatedImages[idx]) { - loadHistoryItem(generatedImages[idx]); - } - }, [generatedImages, loadHistoryItem]); - - const selectedGalleryItems = selectedGalleryIndexes - .map((idx) => generatedImages[idx]) - .filter(Boolean); - - const clearGallerySelection = () => { - setSelectedGalleryIndexes([]); - }; - - const useSelectedGalleryItem = () => { - if (selectedGalleryItems[0]) { - loadHistoryItem(selectedGalleryItems[0]); - } - }; - - const downloadSelectedGalleryItems = () => { - selectedGalleryItems.forEach((item, index) => { - const seed = item.seed ?? Date.now(); - downloadImageUrl(item.url, `diffusion-${seed}-${index + 1}.png`); - }); - }; - - const deleteSelectedGalleryItems = async () => { - if (selectedGalleryItems.length === 0) return; - const count = selectedGalleryItems.length; - const confirmed = await showConfirm({ - title: "Delete Selected Outputs?", - message: `Delete ${count} selected output${count === 1 ? "" : "s"} from USB? This removes the image and metadata files.`, - confirmLabel: "Delete", - danger: true, - }); - if (!confirmed) return; - - try { - await deleteGeneratedOutputs(selectedGalleryItems); - const selectedSet = new Set(selectedGalleryIndexes); - setGeneratedImages((prev) => prev.filter((_, idx) => !selectedSet.has(idx))); - setSelectedGalleryIndexes([]); - - if (selectedGalleryItems.some((item) => item.url === outputImage)) { - setOutputImage(null); - setOutputSeed(null); - setGenDuration(null); - } - } catch (err) { - console.error("Failed to delete selected outputs:", err); - showAlert({ - title: "Delete Failed", - message: err.message || String(err), - danger: true, - }); - } - }; - - return ( -
-
-

Generator Dashboard

-

- Create high-fidelity images completely locally on your external drive. -

-
- -
- {/* Left Side: Parameters & Input */} -
-
-

- - Generation Workspace -

- -
- {/* Prompt Textarea */} -
- -