feat: implement sidebar navigation and history management components for text, speech, and TTS modules
This commit is contained in:
1 parent
89a7b1fbca
commit
0cf11a72b9
6 files changed
+239
-91
No files matched your search
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
Reference in new issue
Block a user