diff --git a/app/frontend/.agents/task.md b/app/frontend/.agents/task.md new file mode 100644 index 0000000..9d32aed --- /dev/null +++ b/app/frontend/.agents/task.md @@ -0,0 +1,13 @@ +- [x] Rearrange composer JSX in `TextChat.jsx` to stack the textarea at the top and the action buttons in a separate toolbar at the bottom. +- [x] Refactor composer styles in `App.css` to align with the vertical stack layout, ensuring full-width textarea and proper spacing. +- [x] Remove obsolete `margin-bottom: 2px` properties from action buttons and center them vertically in the toolbar. +- [x] Implement sidebar visibility toggle state in `App.jsx` and persist state to `localStorage`. +- [x] Add sidebar collapse transitions and `.sidebar.collapsed` styles in `App.css`. +- [x] Integrate left sidebar toggle button in `TopStatusBar.jsx` using `PanelLeft`/`PanelLeftClose` Lucide icons. +- [x] Set AI/assistant chat bubble wrap max-width to `80%` in `App.css` to improve text readability and prevent wide stretching. +- [x] Implement global font size state in `App.jsx` and synchronize it to a CSS custom variable `--app-font-size` on the html element. +- [x] Refactor body font-size to use relative `1rem` in `App.css` to enable page-wide scaling. +- [x] Add a Font Size range slider to the "Appearance & Themes" section in `Settings.jsx` to adjust text size from 12px to 24px. +- [x] Fix dropdown overlay menu readability issues in `App.css` by using a solid color-mix base (blending transparent surface with solid background) and forcing strong `backdrop-filter: blur(20px)` to prevent underlying text/icon bleeding. +- [x] Merge "Color Themes" and "Text & Interface Size" subsections in `Settings.jsx` into a single unified card container separated by a horizontal divider line. +- [x] Test production build using `npm run build` to verify there are no compilation or syntax errors. diff --git a/app/frontend/src/App.css b/app/frontend/src/App.css index 72a7649..f690d08 100644 --- a/app/frontend/src/App.css +++ b/app/frontend/src/App.css @@ -1,5 +1,6 @@ /* Modern Minimalist & Glassmorphism Design System */ :root { + --app-font-size: 16px; font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-synthesis: none; text-rendering: optimizeLegibility; @@ -50,6 +51,10 @@ --glass-blur: blur(16px); } +html { + font-size: var(--app-font-size); +} + /* Dark Theme Variables */ html[data-theme="dark"] { --md-sys-color-primary: #6366f1; @@ -282,12 +287,12 @@ html[data-theme="dark-oled"] body { top: 40px; right: 0; width: 190px; - background: var(--md-sys-color-surface); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); + 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.15); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25); padding: 6px; display: flex; flex-direction: column; @@ -385,7 +390,7 @@ body { overflow: hidden; height: 100vh; width: 100vw; - font-size: 16px; + font-size: 1rem; transition: background-color var(--md-transition-duration) ease; } @@ -414,6 +419,16 @@ body { 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; @@ -744,7 +759,7 @@ body { .chat-message-row.ai .chat-bubble-wrap { align-items: flex-start; - max-width: min(1520px, calc(100% - 42px)); + max-width: 80%; } .chat-bubble { @@ -1209,8 +1224,9 @@ body { min-width: 280px; max-height: 280px; overflow-y: auto; - background: color-mix(in srgb, var(--md-sys-color-surface) 95%, black); - backdrop-filter: blur(16px); + 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); @@ -1384,6 +1400,28 @@ body { 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; diff --git a/app/frontend/src/App.jsx b/app/frontend/src/App.jsx index 0b44307..de89a38 100644 --- a/app/frontend/src/App.jsx +++ b/app/frontend/src/App.jsx @@ -214,6 +214,24 @@ function App() { 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"; + }); + + 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]); const [speechTranscriptions, setSpeechTranscriptions] = useState([]); const [selectedSpeechTranscript, setSelectedSpeechTranscript] = useState(null); const [showSpeechHistory, setShowSpeechHistory] = useState(false); @@ -623,6 +641,7 @@ function App() { const sidebarContent = useMemo(() => ( - ), [activeTab, specs, conversations, activeConversationId, showHistory, handleDeleteConversation, speechTranscriptions, selectedSpeechTranscript, showSpeechHistory, handleDeleteSpeechTranscription, ttsOutputs, selectedTtsOutput, showTtsHistory, handleDeleteTtsOutput]); + ), [sidebarVisible, activeTab, specs, conversations, activeConversationId, showHistory, handleDeleteConversation, speechTranscriptions, selectedSpeechTranscript, showSpeechHistory, handleDeleteSpeechTranscription, ttsOutputs, selectedTtsOutput, showTtsHistory, handleDeleteTtsOutput]); const handleStopServer = useCallback(async () => { if (!serverRunning || isStoppingServer) return; @@ -679,6 +698,8 @@ function App() { isStoppingServer={isStoppingServer} theme={theme} setTheme={setTheme} + sidebarVisible={sidebarVisible} + onToggleSidebar={() => setSidebarVisible(!sidebarVisible)} /> {/* Dynamic Workspace Container */} @@ -793,6 +814,8 @@ function App() { diagnosticsCopied={diagnosticsCopied} theme={theme} setTheme={setTheme} + fontSize={fontSize} + setFontSize={setFontSize} /> diff --git a/app/frontend/src/components/Settings.jsx b/app/frontend/src/components/Settings.jsx index bd6413b..f6135e3 100644 --- a/app/frontend/src/components/Settings.jsx +++ b/app/frontend/src/components/Settings.jsx @@ -206,6 +206,14 @@ function SectionHeader({ icon: Icon, title, count, color, isExpanded, onToggle } ); } +const getSliderStyle = (value, min, max) => { + const v = Number(value); + const mn = Number(min); + const mx = Number(max); + const pct = mx === mn ? 0 : ((v - mn) / (mx - mn)) * 100; + return { "--value": `${pct}%` }; +}; + // ─── Main Settings Component ─── function Settings({ constraints, @@ -234,6 +242,8 @@ function Settings({ diagnosticsCopied, theme, setTheme, + fontSize, + setFontSize, }) { const [llmStatus, setLlmStatus] = useState({ ready: false, settings: {} }); const [llmBackends, setLlmBackends] = useState({ available: [], candidates: [] }); @@ -704,6 +714,7 @@ function Settings({ onChange={(e) => updateConstraint("steps", parseInt(e.target.value))} min="1" max={isOpenVinoNpu ? "8" : "60"} + style={getSliderStyle(constraints.steps, 1, isOpenVinoNpu ? 8 : 60)} /> {isOpenVinoNpu @@ -912,6 +923,7 @@ function Settings({ min="0" max="32768" step="512" + style={getSliderStyle(textSettings.contextSize || 0, 0, 32768)} /> Model memory limit. 0 uses default limit. (Recommended: 0) @@ -940,6 +952,7 @@ function Settings({ min="0" max="2" step="0.1" + style={getSliderStyle(textSettings.temperature, 0, 2)} /> Controls creativity. Lower = focused & factual, Higher = creative & diverse. (Recommended: 0.7) @@ -978,6 +991,7 @@ function Settings({ min="64" max="4096" step="64" + style={getSliderStyle(textSettings.maxTokens || 1024, 64, 4096)} /> Manual uses the slider value. @@ -1045,6 +1059,7 @@ function Settings({ onChange={(e) => updateTextSetting("threads", parseInt(e.target.value))} min="1" max={specs?.cpu_cores_logical || 16} + style={getSliderStyle(textSettings.threads || 4, 1, specs?.cpu_cores_logical || 16)} /> @@ -1063,6 +1078,7 @@ function Settings({ }} min="0" max="50" + style={getSliderStyle(textSettings.gpuLayers === -1 ? 50 : textSettings.gpuLayers, 0, 50)} /> 50 = All layers on GPU @@ -1082,6 +1098,7 @@ function Settings({ min="64" max="2048" step="64" + style={getSliderStyle(textSettings.batchSize || 512, 64, 2048)} /> @@ -1201,6 +1218,7 @@ function Settings({ onChange={(e) => updateSpeechSetting("threads", parseInt(e.target.value))} min="1" max={specs?.cpu_cores_logical || 16} + style={getSliderStyle(speechSettings.threads || 4, 1, specs?.cpu_cores_logical || 16)} /> Number of threads to allocate for transcription. (Recommended: 4) @@ -1309,6 +1327,7 @@ function Settings({ min="0.5" max="2" step="0.05" + style={getSliderStyle(ttsSettings?.speed || 1, 0.5, 2)} /> 1.00x is natural speed. Lower values are slower, higher values are faster. @@ -1328,7 +1347,7 @@ function Settings({ toggleSection("appearance")} @@ -1336,74 +1355,107 @@ function Settings({ {expandedSections.appearance && (
-
-
- - Color Themes -
-
- {THEMES.map((t) => { - const isActive = theme === t.id; - return ( -
- - {/* Preview circles for primary and secondary colors */} -
-
-
-
- - {t.type} - -
- - ); - })} + + {/* Preview circles for primary and secondary colors */} +
+
+
+
+ + {t.type} + +
+ + ); + })} +
+ + {/* Divider separating sub-sections inside the single card */} +
+ + {/* Text & Interface Size Sub-section */} +
+ + Text & Interface Size +
+
+
+ Font Size + {fontSize || 16}px +
+ setFontSize(parseInt(e.target.value))} + min="12" + max="24" + step="1" + style={getSliderStyle(fontSize || 16, 12, 24)} + /> + + Adjusts the global font size of the application text, including chat replies and settings labels. Default is 16px. + +
+
-
-
)} ); diff --git a/app/frontend/src/components/Sidebar.jsx b/app/frontend/src/components/Sidebar.jsx index b3b60b3..89f898c 100644 --- a/app/frontend/src/components/Sidebar.jsx +++ b/app/frontend/src/components/Sidebar.jsx @@ -10,6 +10,7 @@ function formatSidebarDate(value) { } function Sidebar({ + collapsed = false, activeTab, setActiveTab, specs, @@ -33,7 +34,7 @@ function Sidebar({ onDeleteTtsOutput }) { return ( -
+
{/* Sidebar Header */}
diff --git a/app/frontend/src/components/TopStatusBar.jsx b/app/frontend/src/components/TopStatusBar.jsx index 994989a..2cf9306 100644 --- a/app/frontend/src/components/TopStatusBar.jsx +++ b/app/frontend/src/components/TopStatusBar.jsx @@ -1,8 +1,19 @@ import React, { memo, useState, useRef, useEffect } from "react"; -import { Cpu, HardDrive, Database, Square, RefreshCw, Sun, Moon, Palette, Check } from "lucide-react"; +import { Cpu, HardDrive, Database, Square, RefreshCw, Sun, Moon, Palette, Check, PanelLeftClose, PanelLeft } from "lucide-react"; import { THEMES } from "../themes"; -function TopStatusBar({ telemetry, serverRunning, activeModel, isLlmLoaded = false, onStopServer, isStoppingServer = false, theme, setTheme }) { +function TopStatusBar({ + telemetry, + serverRunning, + activeModel, + isLlmLoaded = false, + onStopServer, + isStoppingServer = false, + theme, + setTheme, + sidebarVisible, + onToggleSidebar +}) { const [showThemeMenu, setShowThemeMenu] = useState(false); const menuRef = useRef(null); @@ -40,19 +51,29 @@ function TopStatusBar({ telemetry, serverRunning, activeModel, isLlmLoaded = fal return (
-
-
- - {getStatusText()} - - {activeModel && ( - <> - | - - {activeModel} - - - )} +
+ + +
+
+ + {getStatusText()} + + {activeModel && ( + <> + | + + {activeModel} + + + )} +