Spaces:
Running
Running
| export type ChatWidth = 'centered' | 'wide'; | |
| interface HeaderProps { | |
| onOpenSettings: () => void; | |
| leftSidebarOpen: boolean; | |
| rightSidebarOpen: boolean; | |
| chatWidth: ChatWidth; | |
| onToggleLeftSidebar: () => void; | |
| onToggleRightSidebar: () => void; | |
| onToggleChatWidth: () => void; | |
| disabled?: boolean; | |
| } | |
| export function isSafariUserAgent(userAgent: string): boolean { | |
| return /\bSafari\//.test(userAgent) && !/(?:HeadlessChrome|\bChrome|\bChromium|\bCriOS|\bEdg|\bOPR)\//.test(userAgent); | |
| } | |
| export function Header({ | |
| onOpenSettings, | |
| leftSidebarOpen, | |
| rightSidebarOpen, | |
| chatWidth, | |
| onToggleLeftSidebar, | |
| onToggleRightSidebar, | |
| onToggleChatWidth, | |
| disabled = false, | |
| }: HeaderProps) { | |
| const safari = isSafariUserAgent(globalThis.navigator?.userAgent ?? ''); | |
| return ( | |
| <header className="station-header"> | |
| <div className="brand-block" aria-label="Bonsai local WebGPU chat"> | |
| <span className="brand-glyph" aria-hidden="true">B</span> | |
| <div> | |
| <h1>Bonsai</h1> | |
| <span>Local WebGPU chat</span> | |
| </div> | |
| </div> | |
| <div className={`browser-notice${safari ? ' is-warning' : ''}`} role={safari ? 'status' : undefined}> | |
| <span className="notice-mark" aria-hidden="true" /> | |
| <span> | |
| <strong>{safari ? 'Safari compatibility mode' : 'Private local inference'}</strong> | |
| <span>{safari ? 'Use Chrome or Edge for substantially faster generation.' : 'Prompts and model output stay in this browser.'}</span> | |
| </span> | |
| </div> | |
| <div className="header-actions"> | |
| <div className="layout-controls" role="group" aria-label="Workspace layout"> | |
| <button | |
| type="button" | |
| aria-label={leftSidebarOpen ? 'Hide model sidebar' : 'Show model sidebar'} | |
| aria-pressed={leftSidebarOpen} | |
| onClick={onToggleLeftSidebar} | |
| title={leftSidebarOpen ? 'Hide models' : 'Show models'} | |
| > | |
| <PanelLeftIcon /> | |
| </button> | |
| <button | |
| type="button" | |
| aria-label={chatWidth === 'wide' ? 'Center chat' : 'Use wide chat'} | |
| aria-pressed={chatWidth === 'wide'} | |
| onClick={onToggleChatWidth} | |
| title={chatWidth === 'wide' ? 'Centered chat' : 'Wide chat'} | |
| > | |
| <WidthIcon /> | |
| </button> | |
| <button | |
| type="button" | |
| aria-label={rightSidebarOpen ? 'Hide telemetry sidebar' : 'Show telemetry sidebar'} | |
| aria-pressed={rightSidebarOpen} | |
| onClick={onToggleRightSidebar} | |
| title={rightSidebarOpen ? 'Hide telemetry' : 'Show telemetry'} | |
| > | |
| <PanelRightIcon /> | |
| </button> | |
| </div> | |
| <button className="icon-button" type="button" disabled={disabled} onClick={onOpenSettings} aria-label="Open runtime settings"> | |
| <SettingsIcon /> | |
| </button> | |
| </div> | |
| </header> | |
| ); | |
| } | |
| function PanelLeftIcon() { | |
| return <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2.5" y="3" width="15" height="14" rx="1.5" /><path d="M7.5 3v14" /></svg>; | |
| } | |
| function PanelRightIcon() { | |
| return <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2.5" y="3" width="15" height="14" rx="1.5" /><path d="M12.5 3v14" /></svg>; | |
| } | |
| function WidthIcon() { | |
| return <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 6v8M17 6v8M7 10h6M8.5 8.5 7 10l1.5 1.5M11.5 8.5 13 10l-1.5 1.5" /></svg>; | |
| } | |
| function SettingsIcon() { | |
| return ( | |
| <svg viewBox="0 0 24 24" aria-hidden="true"> | |
| <path d="M4 7h10M18 7h2M4 17h2M10 17h10M14 4v6M10 14v6" /> | |
| <circle cx="16" cy="7" r="2" /> | |
| <circle cx="8" cy="17" r="2" /> | |
| </svg> | |
| ); | |
| } | |