WaveCut/Bonsai-Chat-WebGPU
5
1export type ChatWidth = 'centered' | 'wide';2 3interface HeaderProps {4 onOpenSettings: () => void;5 leftSidebarOpen: boolean;6 rightSidebarOpen: boolean;7 chatWidth: ChatWidth;8 onToggleLeftSidebar: () => void;9 onToggleRightSidebar: () => void;10 onToggleChatWidth: () => void;11 disabled?: boolean;12}13 14export function isSafariUserAgent(userAgent: string): boolean {15 return /\bSafari\//.test(userAgent) && !/(?:HeadlessChrome|\bChrome|\bChromium|\bCriOS|\bEdg|\bOPR)\//.test(userAgent);16}17 18export function Header({19 onOpenSettings,20 leftSidebarOpen,21 rightSidebarOpen,22 chatWidth,23 onToggleLeftSidebar,24 onToggleRightSidebar,25 onToggleChatWidth,26 disabled = false,27}: HeaderProps) {28 const safari = isSafariUserAgent(globalThis.navigator?.userAgent ?? '');29 30 return (31 <header className="station-header">32 <div className="brand-block" aria-label="Bonsai local WebGPU chat">33 <span className="brand-glyph" aria-hidden="true">B</span>34 <div>35 <h1>Bonsai</h1>36 <span>Local WebGPU chat</span>37 </div>38 </div>39 40 <div className={`browser-notice${safari ? ' is-warning' : ''}`} role={safari ? 'status' : undefined}>41 <span className="notice-mark" aria-hidden="true" />42 <span>43 <strong>{safari ? 'Safari compatibility mode' : 'Private local inference'}</strong>44 <span>{safari ? 'Use Chrome or Edge for substantially faster generation.' : 'Prompts and model output stay in this browser.'}</span>45 </span>46 </div>47 48 <div className="header-actions">49 <div className="layout-controls" role="group" aria-label="Workspace layout">50 <button51 type="button"52 aria-label={leftSidebarOpen ? 'Hide model sidebar' : 'Show model sidebar'}53 aria-pressed={leftSidebarOpen}54 onClick={onToggleLeftSidebar}55 title={leftSidebarOpen ? 'Hide models' : 'Show models'}56 >57 <PanelLeftIcon />58 </button>59 <button60 type="button"61 aria-label={chatWidth === 'wide' ? 'Center chat' : 'Use wide chat'}62 aria-pressed={chatWidth === 'wide'}63 onClick={onToggleChatWidth}64 title={chatWidth === 'wide' ? 'Centered chat' : 'Wide chat'}65 >66 <WidthIcon />67 </button>68 <button69 type="button"70 aria-label={rightSidebarOpen ? 'Hide telemetry sidebar' : 'Show telemetry sidebar'}71 aria-pressed={rightSidebarOpen}72 onClick={onToggleRightSidebar}73 title={rightSidebarOpen ? 'Hide telemetry' : 'Show telemetry'}74 >75 <PanelRightIcon />76 </button>77 </div>78 <button className="icon-button" type="button" disabled={disabled} onClick={onOpenSettings} aria-label="Open runtime settings">79 <SettingsIcon />80 </button>81 </div>82 </header>83 );84}85 86function PanelLeftIcon() {87 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>;88}89 90function PanelRightIcon() {91 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>;92}93 94function WidthIcon() {95 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>;96}97 98function SettingsIcon() {99 return (100 <svg viewBox="0 0 24 24" aria-hidden="true">101 <path d="M4 7h10M18 7h2M4 17h2M10 17h10M14 4v6M10 14v6" />102 <circle cx="16" cy="7" r="2" />103 <circle cx="8" cy="17" r="2" />104 </svg>105 );106}107 