feat: implement sidebar navigation and history management components for text, speech, and TTS modules

This commit is contained in:
techjarves committed 2026-06-23 06:51:41 +05:30
1 parent 89a7b1fbca
commit 0cf11a72b9
6 files changed
+239 -91

No files matched your search

+13
View File
@@ -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.
+46 -8
View File
@@ -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;
+24 -1
View File
@@ -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(() => (
<Sidebar
collapsed={!sidebarVisible}
activeTab={activeTab}
setActiveTab={setActiveTab}
specs={specs}
@@ -645,7 +664,7 @@ function App() {
setShowTtsHistory={setShowTtsHistory}
onDeleteTtsOutput={handleDeleteTtsOutput}
/>
), [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}
/>
</div>
</div>
+118 -66
View File
@@ -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)}
/>
<span className="settings-option-desc">
{isOpenVinoNpu
@@ -912,6 +923,7 @@ function Settings({
min="0"
max="32768"
step="512"
style={getSliderStyle(textSettings.contextSize || 0, 0, 32768)}
/>
<span className="settings-option-desc">
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)}
/>
<span className="settings-option-desc">
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)}
/>
<span className="settings-option-desc">
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)}
/>
</div>
@@ -1063,6 +1078,7 @@ function Settings({
}}
min="0"
max="50"
style={getSliderStyle(textSettings.gpuLayers === -1 ? 50 : textSettings.gpuLayers, 0, 50)}
/>
<span className="settings-option-desc">
50 = All layers on GPU
@@ -1082,6 +1098,7 @@ function Settings({
min="64"
max="2048"
step="64"
style={getSliderStyle(textSettings.batchSize || 512, 64, 2048)}
/>
</div>
@@ -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)}
/>
<span className="settings-option-desc">
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)}
/>
<span className="settings-option-desc">
1.00x is natural speed. Lower values are slower, higher values are faster.
@@ -1328,7 +1347,7 @@ function Settings({
<SectionHeader
icon={Palette}
title="Appearance & Themes"
count={THEMES.length}
count={2}
color="var(--md-sys-color-primary)"
isExpanded={expandedSections.appearance}
onToggle={() => toggleSection("appearance")}
@@ -1336,74 +1355,107 @@ function Settings({
{expandedSections.appearance && (
<div className="settings-expanded-content">
<div className="settings-subsection" style={{ marginBottom: "28px" }}>
<div className="settings-subsection-title">
<Palette size={16} />
Color Themes
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
gap: "14px",
marginTop: "14px"
}}>
{THEMES.map((t) => {
const isActive = theme === t.id;
return (
<button
key={t.id}
onClick={() => setTheme(t.id)}
className={`theme-card-btn ${isActive ? "active" : ""}`}
style={{
background: t.bg,
color: t.type === "dark" ? "#f4f4f5" : "#0f172a",
border: isActive ? "2px solid var(--md-sys-color-primary)" : "1px solid var(--border-color)",
borderRadius: "14px",
padding: "18px",
textAlign: "left",
cursor: "pointer",
position: "relative",
display: "flex",
flexDirection: "column",
gap: "14px",
boxShadow: isActive ? "0 4px 16px color-mix(in srgb, var(--md-sys-color-primary) 25%, transparent)" : "none",
transition: "all 0.25s ease",
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<span style={{ fontWeight: 700, fontSize: "0.88rem", letterSpacing: "-0.01em" }}>{t.name}</span>
{isActive && (
<div style={{
background: "linear-gradient(135deg, var(--md-sys-color-primary), var(--md-sys-color-secondary))",
color: "var(--md-sys-color-on-primary)",
borderRadius: "50%",
width: "22px",
height: "22px",
<div className="settings-subsection">
{/* Color Themes Sub-section */}
<div className="settings-subsection-title">
<Palette size={16} />
Color Themes
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
gap: "14px",
marginTop: "14px"
}}>
{THEMES.map((t) => {
const isActive = theme === t.id;
return (
<button
key={t.id}
onClick={() => setTheme(t.id)}
className={`theme-card-btn ${isActive ? "active" : ""}`}
style={{
background: t.bg,
color: t.type === "dark" ? "#f4f4f5" : "#0f172a",
border: isActive ? "2px solid var(--md-sys-color-primary)" : "1px solid var(--border-color)",
borderRadius: "14px",
padding: "18px",
textAlign: "left",
cursor: "pointer",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 6px color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent)"
}}>
<Check size={13} strokeWidth={3} />
flexDirection: "column",
gap: "14px",
boxShadow: isActive ? "0 4px 16px color-mix(in srgb, var(--md-sys-color-primary) 25%, transparent)" : "none",
transition: "all 0.25s ease",
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<span style={{ fontWeight: 700, fontSize: "0.88rem", letterSpacing: "-0.01em" }}>{t.name}</span>
{isActive && (
<div style={{
background: "linear-gradient(135deg, var(--md-sys-color-primary), var(--md-sys-color-secondary))",
color: "var(--md-sys-color-on-primary)",
borderRadius: "50%",
width: "22px",
height: "22px",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 6px color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent)"
}}>
<Check size={13} strokeWidth={3} />
</div>
)}
</div>
)}
</div>
{/* Preview circles for primary and secondary colors */}
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<div style={{ width: "26px", height: "26px", borderRadius: "50%", background: t.primary, border: "2px solid rgba(255,255,255,0.25)", boxShadow: "0 2px 6px rgba(0,0,0,0.15)" }} title="Primary" />
<div style={{ width: "26px", height: "26px", borderRadius: "50%", background: t.secondary, border: "2px solid rgba(255,255,255,0.25)", boxShadow: "0 2px 6px rgba(0,0,0,0.15)" }} title="Secondary" />
<div style={{ width: "26px", height: "26px", borderRadius: "50%", background: t.bg, border: "1.5px solid rgba(0,0,0,0.12)", boxShadow: "inset 0 2px 4px rgba(0,0,0,0.06)" }} title="Background" />
<span style={{ marginLeft: "auto", fontSize: "0.7rem", opacity: 0.6, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em" }}>
{t.type}
</span>
</div>
</button>
);
})}
{/* Preview circles for primary and secondary colors */}
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<div style={{ width: "26px", height: "26px", borderRadius: "50%", background: t.primary, border: "2px solid rgba(255,255,255,0.25)", boxShadow: "0 2px 6px rgba(0,0,0,0.15)" }} title="Primary" />
<div style={{ width: "26px", height: "26px", borderRadius: "50%", background: t.secondary, border: "2px solid rgba(255,255,255,0.25)", boxShadow: "0 2px 6px rgba(0,0,0,0.15)" }} title="Secondary" />
<div style={{ width: "26px", height: "26px", borderRadius: "50%", background: t.bg, border: "1.5px solid rgba(0,0,0,0.12)", boxShadow: "inset 0 2px 4px rgba(0,0,0,0.06)" }} title="Background" />
<span style={{ marginLeft: "auto", fontSize: "0.7rem", opacity: 0.6, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em" }}>
{t.type}
</span>
</div>
</button>
);
})}
</div>
{/* Divider separating sub-sections inside the single card */}
<div style={{
height: "1px",
background: "var(--border-color)",
margin: "24px 0"
}} />
{/* Text & Interface Size Sub-section */}
<div className="settings-subsection-title">
<Type size={16} />
Text & Interface Size
</div>
<div className="m3-slider-group" style={{ maxWidth: "480px", marginTop: "14px" }}>
<div className="m3-slider-header">
<span className="m3-slider-label">Font Size</span>
<span className="settings-value-badge">{fontSize || 16}px</span>
</div>
<input
type="range"
className="m3-slider"
value={fontSize || 16}
onChange={(e) => setFontSize(parseInt(e.target.value))}
min="12"
max="24"
step="1"
style={getSliderStyle(fontSize || 16, 12, 24)}
/>
<span className="settings-option-desc">
Adjusts the global font size of the application text, including chat replies and settings labels. Default is 16px.
</span>
</div>
</div>
</div>
</div>
</div>
)}
</>
);
+2 -1
View File
@@ -10,6 +10,7 @@ function formatSidebarDate(value) {
}
function Sidebar({
collapsed = false,
activeTab,
setActiveTab,
specs,
@@ -33,7 +34,7 @@ function Sidebar({
onDeleteTtsOutput
}) {
return (
<div className="sidebar">
<div className={`sidebar ${collapsed ? "collapsed" : ""}`}>
<div>
{/* Sidebar Header */}
<div className="sidebar-logo">
+36 -15
View File
@@ -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 (
<div className="top-status-bar">
<div className="current-model-info">
<div className={getStatusClass()}></div>
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>
{getStatusText()}
</span>
{activeModel && (
<>
<span style={{ color: "var(--md-sys-color-outline-variant)" }}>|</span>
<span style={{ color: "var(--md-sys-color-primary)", fontWeight: 700 }}>
{activeModel}
</span>
</>
)}
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<button
onClick={onToggleSidebar}
className="sidebar-toggle-btn"
title={sidebarVisible ? "Collapse Sidebar" : "Expand Sidebar"}
>
{sidebarVisible ? <PanelLeftClose size={18} /> : <PanelLeft size={18} />}
</button>
<div className="current-model-info">
<div className={getStatusClass()}></div>
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>
{getStatusText()}
</span>
{activeModel && (
<>
<span style={{ color: "var(--md-sys-color-outline-variant)" }}>|</span>
<span style={{ color: "var(--md-sys-color-primary)", fontWeight: 700 }}>
{activeModel}
</span>
</>
)}
</div>
</div>
<div className="telemetry-group" style={{ position: "relative" }}>