Spaces:
Running
Running
File size: 6,084 Bytes
6c62075 0ed8124 6c62075 0ed8124 6c62075 0ed8124 21ad36a 0ed8124 6c62075 0ed8124 6c62075 0ed8124 21ad36a 0ed8124 6c62075 0ed8124 6c62075 0ed8124 6c62075 0ed8124 c3633b1 0ed8124 21ad36a 0ed8124 6c62075 0ed8124 6c62075 0ed8124 6c62075 0ed8124 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | import { useEffect, useState } from 'react';
import type { BonsaiShellProps } from '../../lib/contracts';
import { normalizeThemeMode, resolveThemeMode, THEME_STORAGE_KEY, type ThemeMode } from '../theme';
import { ChatSurface } from './ChatSurface';
import { ConversationList } from './ConversationList';
import { Header, type ChatWidth } from './Header';
import { InspectorPanel } from './InspectorPanel';
import { ModelRail } from './ModelRail';
import { SettingsSheet } from './SettingsSheet';
const BROWSER_THEME_COLORS = {
light: '#f3f6f2',
dark: '#111511',
} as const;
export function BonsaiShell(props: BonsaiShellProps) {
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
const [chatWidth, setChatWidth] = useState<ChatWidth>('centered');
const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
try {
return normalizeThemeMode(globalThis.localStorage?.getItem(THEME_STORAGE_KEY));
} catch {
return 'system';
}
});
const [systemDark, setSystemDark] = useState(() => globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false);
const activeModel = props.models.find((model) => model.id === props.activeModelId);
const busy = props.streamState === 'loading' || props.streamState === 'streaming';
const resolvedTheme = resolveThemeMode(themeMode, systemDark);
useEffect(() => {
const media = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
if (!media) return;
const handleChange = (event: MediaQueryListEvent) => setSystemDark(event.matches);
setSystemDark(media.matches);
media.addEventListener('change', handleChange);
return () => media.removeEventListener('change', handleChange);
}, []);
useEffect(() => {
try {
globalThis.localStorage?.setItem(THEME_STORAGE_KEY, themeMode);
} catch {
// Theme still works for the current tab when storage is unavailable.
}
}, [themeMode]);
useEffect(() => {
const themeColor = BROWSER_THEME_COLORS[resolvedTheme];
const meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
const previousMetaColor = meta?.content;
const previousRootBackground = document.documentElement.style.backgroundColor;
const previousBodyBackground = document.body.style.backgroundColor;
const previousColorScheme = document.documentElement.style.colorScheme;
if (meta) meta.content = themeColor;
document.documentElement.style.backgroundColor = themeColor;
document.documentElement.style.colorScheme = resolvedTheme;
document.body.style.backgroundColor = themeColor;
return () => {
if (meta && previousMetaColor !== undefined) meta.content = previousMetaColor;
document.documentElement.style.backgroundColor = previousRootBackground;
document.documentElement.style.colorScheme = previousColorScheme;
document.body.style.backgroundColor = previousBodyBackground;
};
}, [resolvedTheme]);
if (!activeModel) {
throw new Error(`Active model ${props.activeModelId} is missing from the model catalog`);
}
return (
<div className="station-frame" data-theme={resolvedTheme} data-theme-mode={themeMode}>
<Header
onOpenSettings={props.onOpenSettings}
disabled={busy}
leftSidebarOpen={leftSidebarOpen}
rightSidebarOpen={rightSidebarOpen}
chatWidth={chatWidth}
onToggleLeftSidebar={() => setLeftSidebarOpen((open) => !open)}
onToggleRightSidebar={() => setRightSidebarOpen((open) => !open)}
onToggleChatWidth={() => setChatWidth((width) => width === 'centered' ? 'wide' : 'centered')}
/>
<div className={`workspace-grid layout-${chatWidth}${leftSidebarOpen ? '' : ' is-models-collapsed'}${rightSidebarOpen ? '' : ' is-inspector-collapsed'}`}>
<ModelRail
hidden={!leftSidebarOpen}
models={props.models}
activeModelId={props.activeModelId}
busy={busy}
onModelSelect={props.onModelSelect}
/>
{props.conversationListOpen ? <ConversationList
conversations={props.conversations}
activeConversationId={props.activeConversationId}
onNewConversation={props.onNewConversation}
onOpenConversation={props.onOpenConversation}
onOpenConversationSettings={props.onOpenConversationSettings}
onDeleteConversation={props.onDeleteConversation}
/> : <ChatSurface
activeModel={activeModel}
loadedModelId={props.loadedModelId}
messages={props.messages}
streamState={props.streamState}
modelLoadProgress={props.modelLoadProgress}
toolsEnabled={props.toolsEnabled}
runtimeStatus={props.runtimeStatus}
error={props.error}
shardRetry={props.shardRetry}
theme={resolvedTheme}
onLoadModel={props.onLoadModel}
onDismissError={props.onDismissError}
onRetryShard={props.onRetryShard}
onSend={props.onSend}
onRegenerate={props.onRegenerate}
onCancel={props.onCancel}
onToolsEnabledChange={props.onToolsEnabledChange}
onOpenConversationList={props.onOpenConversationList}
onOpenSettings={props.onOpenSettings}
/>}
<InspectorPanel
hidden={!rightSidebarOpen}
telemetry={props.telemetry}
toolEvents={props.toolEvents}
artifact={props.artifact}
onCloseArtifact={props.onCloseArtifact}
/>
</div>
<SettingsSheet
open={props.settingsOpen}
themeMode={themeMode}
onThemeModeChange={setThemeMode}
activeModel={activeModel}
telemetry={props.telemetry}
settings={props.settings}
onSettingsChange={props.onSettingsChange}
onPersistStorage={props.onPersistStorage}
onClearStorage={props.onClearStorage}
onClearConversation={props.onClearConversation}
onClose={props.onCloseSettings}
/>
</div>
);
}
|