File size: 3,681 Bytes
6c62075
 
0ed8124
 
6c62075
 
 
 
 
 
0ed8124
 
 
6c62075
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed8124
 
6c62075
 
0ed8124
6c62075
 
0ed8124
 
 
6c62075
 
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
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>
  );
}