Spaces:
Running
Running
Valeriy Selitskiy commited on
Commit ·
6c62075
1
Parent(s): c3633b1
Release v1.2.1 adaptive chat workspace
Browse files- README.md +13 -1
- dist/assets/index-BY7HNxCd.js +0 -0
- dist/assets/index-BbReSmeP.css +1 -0
- dist/assets/index-DnMbdiUB.css +0 -1
- dist/assets/index-DsVgIwSl.js +0 -0
- dist/index.html +2 -2
- src/app/App.tsx +122 -13
- src/app/components/ArtifactPane.tsx +1 -5
- src/app/components/BonsaiShell.tsx +60 -7
- src/app/components/ChatSurface.tsx +26 -22
- src/app/components/ComposerActivity.test.tsx +64 -0
- src/app/components/ComposerActivity.tsx +97 -171
- src/app/components/ConversationList.tsx +72 -0
- src/app/components/Header.test.ts +11 -0
- src/app/components/Header.tsx +72 -14
- src/app/components/InspectorPanel.tsx +64 -25
- src/app/components/ModelRail.tsx +7 -7
- src/app/components/SettingsSheet.tsx +15 -7
- src/app/styles.css +758 -543
- src/app/theme.test.ts +16 -0
- src/app/theme.ts +12 -0
- src/lib/contracts.ts +17 -0
- src/lib/conversations.test.ts +33 -0
- src/lib/conversations.ts +22 -0
- src/lib/idb.ts +9 -0
README.md
CHANGED
|
@@ -40,9 +40,21 @@ Vite during its container build. The checked-in bundle is produced locally by
|
|
| 40 |
`npm ci && npm run build`; source, tests, lockfile, and exact runtime assets
|
| 41 |
remain in the same Space repository.
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
## Browser requirements
|
| 44 |
|
| 45 |
-
Chrome or Edge with WebGPU is recommended.
|
|
|
|
|
|
|
| 46 |
cross-origin-isolated browser with `SharedArrayBuffer`; the Nginx configuration
|
| 47 |
enables it. Model storage uses the browser cache/OPFS path exposed by wllama and
|
| 48 |
requests persistent storage before the first model byte is downloaded. A browser
|
|
|
|
| 40 |
`npm ci && npm run build`; source, tests, lockfile, and exact runtime assets
|
| 41 |
remain in the same Space repository.
|
| 42 |
|
| 43 |
+
## Chat workspace
|
| 44 |
+
|
| 45 |
+
Chat sessions and their system instructions are saved locally in IndexedDB.
|
| 46 |
+
The chat list can reopen, continue, or delete a session without touching the
|
| 47 |
+
model cache. New chats begin with the bundled private-assistant instructions;
|
| 48 |
+
an empty chat exposes them directly, while an active chat keeps them in Chat
|
| 49 |
+
settings. Appearance can follow the operating system or be pinned to the light
|
| 50 |
+
or dark theme. Both sidebars can collapse, and the transcript can switch
|
| 51 |
+
between centered and full-width layouts.
|
| 52 |
+
|
| 53 |
## Browser requirements
|
| 54 |
|
| 55 |
+
Chrome or Edge with WebGPU is recommended. Safari uses the stable compatibility
|
| 56 |
+
runtime but is substantially slower, so the UI calls this limitation out before
|
| 57 |
+
the model is loaded. CPU-WASM fallback requires a
|
| 58 |
cross-origin-isolated browser with `SharedArrayBuffer`; the Nginx configuration
|
| 59 |
enables it. Model storage uses the browser cache/OPFS path exposed by wllama and
|
| 60 |
requests persistent storage before the first model byte is downloaded. A browser
|
dist/assets/index-BY7HNxCd.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/index-BbReSmeP.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--ink:#141613;--ink-raised:#1b1e1a;--ink-soft:#252921;--paper:#f1f2ed;--paper-deep:#e4e6df;--paper-ghost:#f1f2eda3;--phosphor:#a8d574;--phosphor-dim:#a8d57424;--moss:#738066;--rust:#c27a56;--line-light:#f1f2ed21;--line-dark:#14161326;--display:"Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;--body:"Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;--mono:"Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;--type-utility:clamp(.75rem, .12vw + .72rem, .8125rem);--type-caption:clamp(.8125rem, .15vw + .77rem, .9375rem);--type-body:clamp(1.0625rem, .18vw + 1.02rem, 1.1875rem);--type-heading:clamp(1.55rem, .85vw + 1.35rem, 2.15rem);font-family:var(--body);font-synthesis:none;text-rendering:optimizelegibility}*{box-sizing:border-box}html{background:var(--ink)}body{background:var(--ink);min-width:320px;min-height:100vh;color:var(--paper);font-size:var(--type-body);margin:0}button,textarea,select{font:inherit}button{color:inherit}button:focus-visible,textarea:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--phosphor);outline-offset:3px}button:disabled{cursor:not-allowed}::selection{color:var(--ink);background:var(--phosphor)}.station-frame{--canvas-bg:#f1f2ed;--canvas-raised:#e4e6df;--canvas-field:#f8f9f5;--canvas-text:#141613;--canvas-muted:#14161385;--canvas-line:#14161326;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;isolation:isolate;min-height:100dvh;animation:.58s cubic-bezier(.16,1,.3,1) both station-in}.station-frame[data-theme=dark]{--canvas-bg:#1a1c18;--canvas-raised:#20231f;--canvas-field:#252823;--canvas-text:#edf0e8;--canvas-muted:#edf0e894;--canvas-line:#edf0e821;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}.station-frame:before{display:none}.station-header{background:var(--ink);border-bottom:1px solid var(--line-light);z-index:10;grid-template-columns:260px minmax(320px,1fr) auto;align-items:stretch;min-height:64px;display:grid;position:relative}.brand-block{border-right:1px solid var(--line-light);align-items:center;gap:10px;padding:10px 18px;display:flex}.brand-block h1{font:650 20px/1.1 var(--display);letter-spacing:-.02em;margin:0}.brand-block>div{gap:2px;display:grid}.brand-block>div>span{color:var(--paper-ghost);font-size:var(--type-caption)}.brand-glyph{width:34px;height:34px;color:var(--phosphor);font:650 16px/1 var(--mono);border:1px solid #a8d5746b;border-radius:8px;flex:0 0 34px;place-items:center;display:grid}.brand-mark:before,.brand-mark:after,.brand-mark span:before,.brand-mark span:after{content:"";background:var(--phosphor);transform-origin:bottom;width:1px;position:absolute}.brand-mark:before{height:25px;transform:translate(-1px,4px)rotate(0)}.brand-mark:after{height:13px;transform:translate(4px,-2px)rotate(43deg)}.brand-mark span:before{height:11px;transform:translate(-5px,-1px)rotate(-47deg)}.brand-mark span:after{height:8px;transform:translate(3px,-9px)rotate(72deg)}.eyebrow{color:#e8dfca85;font:600 var(--type-utility)/1 var(--mono);letter-spacing:.18em;text-transform:uppercase;margin:0 0 5px}.eyebrow.ink{color:#11151285}.browser-notice{align-items:center;gap:10px;min-width:0;padding:0 20px;display:flex}.notice-mark{background:var(--phosphor);border-radius:2px;flex:0 0 6px;width:6px;height:6px}.browser-notice>span:last-child{gap:2px;display:grid}.browser-notice strong{color:var(--paper);font:600 var(--type-caption)/1.2 var(--body)}.browser-notice>span>span{color:var(--paper-ghost);font:var(--type-caption)/1.25 var(--body)}.browser-notice.is-warning .notice-mark{background:var(--rust)}.browser-notice.is-warning strong{color:#efb08f}.header-actions{border-left:1px solid var(--line-light);justify-self:end;align-items:center;gap:10px;padding:0 14px;display:flex}.layout-controls{gap:4px;display:flex}.layout-controls button,.icon-button{width:36px;height:36px;color:var(--paper-ghost);border:1px solid var(--line-light);cursor:pointer;background:0 0;border-radius:7px;place-items:center;transition:color .15s,border-color .15s,background .15s;display:grid}.layout-controls button:hover,.icon-button:hover{color:var(--paper);background:#f1f2ed0d;border-color:#f1f2ed4d}.layout-controls button[aria-pressed=true]{color:var(--phosphor);background:var(--phosphor-dim);border-color:#a8d57457}.layout-controls svg,.icon-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:18px}.station-clock{color:var(--paper-ghost);font-family:var(--mono);justify-items:end;padding:0 18px;display:grid}.station-clock small{font-size:var(--type-utility);letter-spacing:.18em}.station-clock span{font-size:var(--type-caption);letter-spacing:.09em;margin-top:4px}.icon-button{flex:0 0 36px}.icon-button:hover{color:var(--paper)}.icon-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:18px}.workspace-grid{background:#10120f;grid-template-columns:260px minmax(0,1fr) 340px;grid-template-areas:"models chat inspector";min-height:calc(100dvh - 64px);display:grid}.workspace-grid.is-models-collapsed{grid-template-columns:0 minmax(0,1fr) 340px}.workspace-grid.is-inspector-collapsed{grid-template-columns:260px minmax(0,1fr) 0}.workspace-grid.is-models-collapsed.is-inspector-collapsed{grid-template-columns:0 minmax(0,1fr) 0}.workspace-grid.layout-centered .chat-surface{justify-self:center;width:min(100%,1120px)}.workspace-grid.layout-wide .chat-surface{width:100%}.model-rail[hidden],.inspector[hidden]{display:none!important}.model-rail,.inspector{background:var(--ink);min-width:0}.model-rail{border-right:1px solid var(--line-light);flex-direction:column;grid-area:models;padding:20px 14px 14px;display:flex;position:relative;overflow:hidden}.model-rail:before{display:none}.rail-heading,.section-heading{z-index:1;align-items:center;gap:10px;display:flex;position:relative}.rail-heading>span,.section-heading>span:first-child{color:var(--phosphor);font:500 var(--type-utility)/1 var(--mono);border-top:1px solid var(--phosphor);padding-top:5px}.rail-heading h2,.section-heading h2{font:600 20px/1.2 var(--display);letter-spacing:-.015em;margin:0}.rail-heading p{color:var(--paper-ghost);font-size:var(--type-caption);margin:4px 0 0;line-height:1.35}.model-list{z-index:1;gap:5px;margin-top:21px;display:grid;position:relative}.model-card{border:1px solid var(--line-light);border-radius:8px;transition:border-color .18s,background .18s}.model-card.is-active{background:var(--phosphor-dim);border-color:#a8d57494}.model-select{text-align:left;width:100%;min-height:72px;color:var(--paper);cursor:pointer;background:0 0;border:0;grid-template-columns:1fr auto;gap:8px;padding:13px 9px 12px;display:grid}.model-card:not(.is-active) .model-select:hover{background:#f1f2ed0b}.model-index{color:#e8dfca61;font:var(--type-utility)/1 var(--mono);padding-top:4px}.model-copy{gap:3px;min-width:0;display:grid}.model-title-line{justify-content:space-between;align-items:center;gap:7px;display:flex}.model-title-line strong{font:600 20px/1 var(--display)}.model-copy em{color:#e8dfca75;font:italic var(--type-caption)/1.2 var(--display);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.model-copy>span:last-child{color:var(--paper-ghost);font-size:var(--type-caption);margin-top:3px}.availability{font:600 var(--type-utility)/1 var(--mono);letter-spacing:0;border:1px solid var(--line-light);padding:2px 4px}.availability-loaded,.availability-loading,.availability-cached{color:var(--phosphor);border-color:#b9ff4552}.availability-remote,.availability-limited{color:var(--rust);border-color:#c96f4366}.model-footprint{color:#e8dfca63;font:var(--type-utility)/1 var(--mono);white-space:nowrap;align-self:end}.model-limit-reason{color:#e8dfca94;font:var(--type-utility)/1.35 var(--mono);margin:-4px 8px 9px;display:block}.rail-footnote{z-index:1;color:#e8dfca4d;font:var(--type-utility)/1.2 var(--mono);letter-spacing:.03em;justify-content:space-between;gap:8px;margin-top:auto;display:flex;position:relative}.chat-surface{min-width:0;height:calc(100dvh - 64px);min-height:calc(100dvh - 64px);color:var(--canvas-text);background:var(--canvas-bg);grid-area:chat;grid-template-rows:auto minmax(0,1fr) auto;display:grid;position:relative;overflow:hidden}.chat-header{border-bottom:1px solid var(--canvas-line);justify-content:space-between;align-items:center;gap:20px;min-height:72px;padding:14px clamp(20px,3vw,40px);display:flex}.chat-header h2{font:650 var(--type-heading)/1.1 var(--display);letter-spacing:-.025em;margin:0;display:inline}.chat-header-main{align-items:center;gap:8px;min-width:0;display:flex}.chat-list-button,.chat-settings-button{min-height:36px;color:var(--canvas-muted);border:1px solid var(--canvas-line);font:600 var(--type-utility)/1 var(--body);white-space:nowrap;cursor:pointer;background:0 0;border-radius:7px;align-items:center;gap:5px;padding:0 9px;display:inline-flex}.chat-list-button:hover,.chat-settings-button:hover{color:var(--canvas-text);border-color:var(--canvas-muted)}.chat-list-button svg{fill:none;stroke:currentColor;stroke-width:1.5px;width:16px}.chat-title{align-items:baseline;gap:10px;min-width:0;display:flex}.botanical-subtitle{color:#11151275;font:var(--type-caption)/1 var(--body)}.chat-header-meta{color:#1115128c;font:var(--type-utility)/1.2 var(--mono);letter-spacing:0;grid-template-columns:repeat(2,auto);gap:5px 13px;display:grid}.live-label{color:#46622f;grid-column:span 2;justify-self:end;align-items:center;gap:7px;display:flex}.live-label i{width:5px;height:5px;box-shadow:none;background:#668a45}.live-label.is-dormant{color:#1115127a}.live-label.is-dormant i{box-shadow:none;background:#1115125c}.runtime-error{z-index:4;color:var(--paper);border:1px solid var(--rust);background:#111512f5;justify-content:space-between;align-items:center;gap:16px;padding:11px 13px;display:flex;position:absolute;top:105px;left:clamp(21px,3vw,44px);right:clamp(21px,3vw,44px);box-shadow:4px 4px #c96f433d}.runtime-error div{gap:3px;display:grid}.runtime-error strong{color:#ff9a68;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.runtime-error span{font:var(--type-caption)/1.35 var(--mono);overflow-wrap:anywhere}.runtime-error button{min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:1px solid #e8dfca4d;flex:none;padding:5px 8px}.runtime-error button:disabled{cursor:wait;opacity:.5}.runtime-error.download-retry button{max-width:132px;line-height:1.25}.chat-ruler{color:#11151266;font:var(--type-utility)/1 var(--mono);background:repeating-linear-gradient(90deg,#0000 0 14px,#11151233 14px 15px) 0 100%/100% 4px no-repeat;border-bottom:1px solid #11151217;justify-content:space-between;padding-inline:clamp(21px,3vw,44px);display:flex}.transcript{scrollbar-color:#11151247 transparent;min-height:0;padding:20px clamp(21px,4vw,64px) 32px;overflow-y:auto}.transcript-date{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:0;align-items:center;gap:10px;margin-bottom:22px;display:flex}.transcript-date:before,.transcript-date:after{display:none}.transcript-empty{text-align:center;color:#11151280;max-width:430px;margin:12vh auto 0}.transcript-empty h3{color:var(--ink);font:650 22px/1.2 var(--display);margin:0 0 8px}.transcript-empty p{font-size:var(--type-body);margin:0;line-height:1.5}.system-instructions-button,.conversation-list-empty button,.new-chat-button{min-height:42px;color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);font:650 var(--type-caption)/1 var(--body);cursor:pointer;border-radius:7px;padding:0 13px}.system-instructions-button{margin-top:18px}.conversation-library{grid-template-rows:auto minmax(0,1fr)}.conversation-list-header>div{gap:4px;display:grid}.conversation-list-header p{color:var(--canvas-muted);font-size:var(--type-caption);margin:0}.conversation-list{scrollbar-color:var(--canvas-muted) transparent;min-height:0;padding:clamp(20px,4vw,48px);overflow-y:auto}.conversation-row{border:1px solid var(--canvas-line);background:var(--canvas-field);border-radius:9px;grid-template-columns:minmax(0,1fr) auto;align-items:stretch;max-width:850px;margin:0 auto 9px;display:grid}.conversation-row.is-current{border-color:#738066b8}.conversation-open{min-width:0;color:var(--canvas-text);text-align:left;cursor:pointer;background:0 0;border:0;gap:6px;padding:15px 16px;display:grid}.conversation-title-line{align-items:center;gap:9px;min-width:0;display:flex}.conversation-title-line strong{font:650 var(--type-body)/1.2 var(--display);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.conversation-title-line small{color:#52712f;font:600 var(--type-utility)/1 var(--body);flex:none}.conversation-open>span:nth-child(2){color:var(--canvas-muted);font-size:var(--type-caption);text-overflow:ellipsis;white-space:nowrap;line-height:1.35;overflow:hidden}.conversation-open>small{color:var(--canvas-muted);font:var(--type-utility)/1.2 var(--mono)}.conversation-actions{border-left:1px solid var(--canvas-line);align-content:center;gap:4px;padding:7px;display:grid}.conversation-actions button{min-height:34px;color:var(--canvas-muted);font:600 var(--type-utility)/1 var(--body);cursor:pointer;background:0 0;border:0;padding:0 8px}.conversation-actions button:hover{color:var(--canvas-text)}.conversation-actions .conversation-delete{color:var(--rust)}.conversation-list-empty{max-width:430px;color:var(--canvas-muted);text-align:center;justify-items:center;gap:10px;margin:15vh auto 0;display:grid}.conversation-list-empty h3,.conversation-list-empty p{margin:0}.conversation-list-empty h3{color:var(--canvas-text);font:650 22px/1.2 var(--display)}.conversation-list-empty button{margin-top:8px}.message{max-width:760px;margin-bottom:25px}.message>header{color:#11151278;font:var(--type-utility)/1.2 var(--mono);letter-spacing:0;align-items:center;gap:8px;margin-bottom:8px;display:flex}.message>header strong{color:var(--ink)}.message>header time{margin-left:auto}.message-ordinal{color:#4d6728;min-width:22px}.message-content{font:var(--type-body)/1.58 var(--body)}.message-content>p{margin:0}.markdown-content>:first-child{margin-top:0}.markdown-content>:last-child{margin-bottom:0}.markdown-content p,.markdown-content ul,.markdown-content ol,.markdown-content blockquote{margin:0 0 10px}.markdown-content ul,.markdown-content ol{padding-left:22px}.markdown-content h3,.markdown-content h4,.markdown-content h5{font-family:var(--display);margin:16px 0 7px;font-weight:500;line-height:1.1}.markdown-content blockquote{color:#111512ad;white-space:pre-wrap;border-left:2px solid #5073248c;padding-left:12px}.markdown-content code{font:.78em/1.4 var(--mono);background:#11151214;padding:1px 4px}.markdown-content pre{max-width:100%;color:var(--paper);background:var(--ink);border-left:2px solid var(--phosphor);margin:11px 0;padding:12px;position:relative;overflow:auto}.markdown-content pre code{white-space:pre;background:0 0;padding:0}.markdown-content a{color:#3f6417;text-underline-offset:2px;text-decoration-thickness:1px}.message-user .markdown-content code{background:#e8dfca1f}.reasoning-block{color:#1115129e;font-size:var(--type-caption);background:#11151208;border:1px solid #11151229;border-radius:8px;margin:0 0 11px;padding:8px 10px}.reasoning-block summary{color:#4d6728;font:600 var(--type-caption)/1.2 var(--body);cursor:pointer}.reasoning-block[open] summary{margin-bottom:8px}.generation-placeholder{color:#11151273;margin:0;font-style:italic}.message-error{color:#8d321b;font:var(--type-caption)/1.4 var(--mono);margin:9px 0 0}.message.has-error{border-color:var(--rust)}.message-assistant{border-left:2px solid #546f3c73;padding-left:22px}.message-assistant .message-content>p:first-letter{float:none;color:inherit;font:inherit;margin:0}.message-user{max-width:min(78%,610px);color:var(--paper);background:var(--ink);border-radius:10px;margin-left:auto;padding:12px 15px 13px}.message-user>header,.message-user>header strong{color:#e8dfca8f}.message-user .message-ordinal{color:var(--phosphor)}.message>footer{color:#1115126e;font:var(--type-utility)/1.2 var(--mono);border-top:1px solid #11151221;gap:12px;margin-top:9px;padding-top:7px;display:flex}.stream-cursor{vertical-align:-2px;background:#507324;width:7px;height:14px;margin-left:4px;animation:.86s step-end infinite cursor-blink;display:inline-block}.tool-call{background:#1115120b;border:1px solid #1115122e;margin-top:13px}.tool-call summary{cursor:pointer;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;padding:8px 10px;list-style:none;display:grid}.tool-call summary::-webkit-details-marker{display:none}.tool-call code{font:var(--type-caption)/1.4 var(--mono)}.tool-call summary>span:last-child{color:#1115127a;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.tool-state,.event-pip{background:var(--moss);border-radius:50%;width:6px;height:6px}.tool-state-complete,.event-complete{background:#55772c}.tool-state-running,.event-running{background:var(--phosphor);box-shadow:0 0 0 3px #5a782b26}.tool-state-error,.event-error{background:var(--rust)}.tool-call>div{border-top:1px solid #11151221;padding:9px 10px}.tool-call>div p{color:#11151294;font-size:var(--type-caption);margin:5px 0 0}.composer{border-top:1px solid var(--canvas-line);background:var(--canvas-raised);padding:12px clamp(21px,3vw,44px) 18px;position:relative}.composer:before{display:none}.composer-activity{color:var(--paper);background:var(--ink);border:1px solid #141613e6;border-radius:8px;gap:10px;margin-bottom:12px;padding:12px 14px;display:grid}.activity-status,.activity-status>div{align-items:center;display:flex}.activity-status{min-width:0;font:var(--type-caption)/1.2 var(--body);justify-content:space-between;gap:12px}.activity-status>div{gap:10px;min-width:0}.activity-status>div>span:last-child{gap:3px;min-width:0;display:grid}.activity-status strong,.activity-status small{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.activity-status strong{color:var(--paper);font-weight:600}.activity-status small{color:var(--paper-ghost);font-size:var(--type-utility)}.activity-status>b{color:var(--phosphor);font:600 var(--type-caption)/1 var(--mono);flex:none}.activity-pulse{background:var(--phosphor);border-radius:2px;flex:0 0 5px;width:5px;height:22px}.activity-track{background:#f1f2ed1f;border-radius:3px;height:6px;overflow:hidden}.activity-track>span{border-radius:inherit;background:var(--phosphor);height:100%;transition:width .12s linear;display:block}.composer-meta{color:#11151275;min-height:24px;font:var(--type-utility)/1 var(--mono);justify-content:space-between;align-items:center;gap:12px;margin-bottom:7px;display:flex}.tool-toggle{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);cursor:pointer;background:0 0;border:0;align-items:center;gap:5px;padding:0 2px;display:flex}.tool-toggle.is-on{color:#425f1f}.regenerate-button{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);cursor:pointer;background:0 0;border:0;margin-right:auto;padding:0 2px}.regenerate-button:disabled{cursor:default;opacity:.45}.tool-toggle svg{fill:none;stroke:currentColor;stroke-width:1.3px;width:12px}.composer-field{border:1px solid var(--canvas-text);background:var(--canvas-field);border-radius:10px;grid-template-columns:1fr auto;min-height:84px;display:grid;position:relative;overflow:hidden}.composer-field label{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:0;position:absolute;top:7px;left:11px}.composer-field textarea{resize:none;min-width:0;color:var(--canvas-text);font:var(--type-body)/1.45 var(--body);background:0 0;border:0;outline:none;padding:23px 13px 9px}.composer-field textarea::placeholder{color:#11151257}.send-button{min-width:112px;min-height:52px;color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);font:700 var(--type-caption)/1 var(--mono);cursor:pointer;border-radius:8px;justify-content:center;align-items:center;gap:7px;margin:7px;transition:background .15s,opacity .15s;display:flex}.send-button:hover:not(:disabled){background:#b6df87}.send-button:disabled{opacity:.34}.send-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:15px}.send-button.stop{color:var(--paper);background:var(--ink)}.send-button.stop span{background:var(--rust);width:7px;height:7px}.send-button.load{background:var(--phosphor)}.station-frame[data-theme=dark] .botanical-subtitle,.station-frame[data-theme=dark] .chat-header-meta,.station-frame[data-theme=dark] .transcript-date,.station-frame[data-theme=dark] .transcript-empty,.station-frame[data-theme=dark] .message>header,.station-frame[data-theme=dark] .message>footer,.station-frame[data-theme=dark] .composer-meta,.station-frame[data-theme=dark] .tool-toggle,.station-frame[data-theme=dark] .regenerate-button,.station-frame[data-theme=dark] .composer-field label{color:var(--canvas-muted)}.station-frame[data-theme=dark] .transcript-empty h3,.station-frame[data-theme=dark] .message>header strong{color:var(--canvas-text)}.station-frame[data-theme=dark] .live-label{color:#a8d574}.station-frame[data-theme=dark] .conversation-title-line small{color:var(--phosphor)}.station-frame[data-theme=dark] .live-label.is-dormant{color:var(--canvas-muted)}.station-frame[data-theme=dark] .live-label.is-dormant i{background:var(--canvas-muted)}.station-frame[data-theme=dark] .transcript{scrollbar-color:#edf0e83d transparent}.station-frame[data-theme=dark] .markdown-content blockquote,.station-frame[data-theme=dark] .reasoning-block,.station-frame[data-theme=dark] .tool-call>div p{color:#edf0e8b3}.station-frame[data-theme=dark] .markdown-content code{background:#edf0e817}.station-frame[data-theme=dark] .markdown-content pre{color:var(--paper);background:#0f110e}.station-frame[data-theme=dark] .markdown-content a,.station-frame[data-theme=dark] .reasoning-block summary{color:var(--phosphor)}.station-frame[data-theme=dark] .reasoning-block,.station-frame[data-theme=dark] .tool-call{border-color:var(--canvas-line);background:#edf0e809}.station-frame[data-theme=dark] .generation-placeholder,.station-frame[data-theme=dark] .tool-call summary>span:last-child,.station-frame[data-theme=dark] .composer-field textarea::placeholder{color:#edf0e866}.station-frame[data-theme=dark] .message-assistant{border-left-color:#a8d57470}.station-frame[data-theme=dark] .message-user{color:var(--canvas-text);border:1px solid var(--canvas-line);background:#11130f}.station-frame[data-theme=dark] .message>footer,.station-frame[data-theme=dark] .tool-call>div{border-color:var(--canvas-line)}.station-frame[data-theme=dark] .tool-toggle.is-on{color:var(--phosphor)}.station-frame[data-theme=dark] .composer-field textarea{color:var(--canvas-text)}.station-frame[data-theme=dark] .composer-field{border-color:#edf0e84d}.station-frame[data-theme=dark] .send-button{color:var(--ink);border-color:#0f110e}.station-frame[data-theme=dark] .send-button.stop{color:var(--canvas-text);border-color:var(--canvas-line);background:#11130f}.inspector{border-left:1px solid var(--line-light);scrollbar-color:#e8dfca2e transparent;grid-area:inspector;height:calc(100dvh - 64px);min-height:calc(100dvh - 64px);overflow-y:auto}.inspector-section{border-bottom:1px solid var(--line-light);padding:20px 18px}.section-heading{justify-content:space-between;align-items:center;margin-bottom:17px}.section-heading.compact{margin-bottom:14px}.section-heading>div{flex:1}.section-heading h2{font-size:20px}.sampling-label{color:var(--paper-ghost);border:1px solid var(--line-light);font:var(--type-utility)/1 var(--body);border-radius:6px;padding:3px 6px}.sampling-label.phase-prefill,.sampling-label.phase-decode{color:var(--phosphor);border-color:#a8d57459}.speed-gauge{grid-template-columns:116px 1fr;align-items:center;gap:15px;display:grid}.gauge-ring{background:radial-gradient(circle, var(--ink) 0 56%, transparent 57%), conic-gradient(var(--phosphor) 0 var(--gauge), #f1f2ed1c var(--gauge) 100%);border-radius:50%;place-items:center;width:116px;height:116px;display:grid;box-shadow:inset 0 0 0 1px #f1f2ed14}.gauge-ring:after{display:none}.gauge-ring>div{z-index:1;justify-items:center;min-width:0;display:grid;position:relative}.gauge-ring strong{color:var(--phosphor);font:600 clamp(24px, 2vw, 29px)/1 var(--display);white-space:nowrap}.gauge-ring small{color:var(--paper-ghost);font:600 10px/1 var(--mono);letter-spacing:.02em;text-transform:uppercase;justify-items:center;gap:2px;margin-top:3px;display:grid}.gauge-ring small span{white-space:nowrap}.speed-gauge dl{margin:0}.speed-gauge dl div{border-bottom:1px solid var(--line-light);font:var(--type-utility)/1 var(--mono);justify-content:space-between;padding:7px 0;display:flex}.speed-gauge dt{color:#e8dfca6e}.speed-gauge dd{color:var(--paper);margin:0}.telemetry-live-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:16px;display:grid}.telemetry-metric{border:1px solid var(--line-light);background:var(--ink-raised);border-radius:8px;gap:6px;min-width:0;padding:10px;display:grid}.telemetry-metric.is-active{border-color:#a8d57466}.telemetry-metric>div:first-child{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.telemetry-metric span,.telemetry-metric small{color:var(--paper-ghost);font-size:var(--type-utility)}.telemetry-metric strong{color:var(--paper);font:600 var(--type-caption)/1 var(--mono);white-space:nowrap}.telemetry-metric.is-active strong{color:var(--phosphor)}.telemetry-meter{background:#f1f2ed1a;border-radius:2px;height:4px;overflow:hidden}.telemetry-meter>span{border-radius:inherit;background:var(--phosphor);height:100%;transition:width .12s linear;display:block}.device-card{border:1px solid var(--line-light);background:var(--ink-raised);border-radius:8px;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;margin:19px 0 14px;padding:10px;display:grid}.device-icon{border:1px solid #b9ff4540;align-items:flex-end;gap:3px;width:28px;height:28px;padding:5px;display:flex}.device-icon i{background:var(--phosphor);width:4px}.device-icon i:first-child{opacity:.45;height:7px}.device-icon i:nth-child(2){height:15px}.device-icon i:nth-child(3){opacity:.7;height:11px}.device-card>div{gap:3px;display:grid}.device-card small,.memory-grid small,.meter small{color:#e8dfca6b;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.device-card strong{font:500 var(--type-caption)/1 var(--mono)}.device-card>span:last-child{color:var(--phosphor);font:600 15px/1 var(--mono);justify-items:end;display:grid}.device-card>span:last-child small{color:#e8dfca5e;font-size:var(--type-utility);margin-top:3px}.meter{margin-top:13px}.meter>div:first-child{font:var(--type-utility)/1 var(--mono);justify-content:space-between;margin-bottom:6px;display:flex}.meter-track{border:1px solid #e8dfca36;height:5px;padding:1px}.meter-track>span{background:var(--phosphor);height:100%;display:block}.memory-grid{background:var(--line-light);grid-template-columns:1fr 1fr;gap:1px;margin-top:14px;display:grid}.memory-grid>div{background:var(--ink);gap:6px;padding:10px;display:grid}.memory-grid strong{font:500 var(--type-caption)/1 var(--mono)}.event-log{gap:0;margin:0;padding:0;list-style:none;display:grid;position:relative}.event-log:before{content:"";background:var(--line-light);width:1px;position:absolute;top:5px;bottom:5px;left:3px}.event-log li{grid-template-columns:8px 64px 1fr;align-items:start;gap:8px;padding:7px 0;display:grid}.event-log li.event-empty{color:#e8dfca5c;font:var(--type-utility)/1.4 var(--mono);padding:5px 0;display:block}.event-pip{z-index:1;border:1px solid var(--ink);margin-top:2px}.event-log time{color:#e8dfca57;font:var(--type-utility)/1.2 var(--mono)}.event-log div{gap:3px;display:grid}.event-log strong{font:500 var(--type-caption)/1 var(--mono)}.event-log small{color:#e8dfca6b;font-size:var(--type-utility)}.artifact-close{border:1px solid var(--line-light);cursor:pointer;background:0 0;border-radius:6px;width:25px;height:25px}.artifact-empty{color:var(--paper-ghost);font:var(--type-caption)/1.5 var(--body);margin:0}.artifact-pane{border:1px solid var(--line-light);background:#f8f5eb}.artifact-tabs{color:var(--paper);border-bottom:1px solid var(--line-light);background:var(--ink-raised);grid-template-columns:auto auto 1fr;align-items:center;display:grid}.artifact-tabs button{border:0;border-right:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:7px 9px}.artifact-tabs button[aria-selected=true]{color:var(--ink);background:var(--phosphor)}.artifact-tabs span{color:#e8dfca73;font:var(--type-utility)/1 var(--mono);text-align:right;text-overflow:ellipsis;white-space:nowrap;padding:0 8px;overflow:hidden}.artifact-pane iframe{background:#fff;border:0;width:100%;height:260px;display:block}.artifact-running{position:relative}.artifact-running>button{z-index:2;color:var(--paper);border:1px solid var(--rust);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:#111512e6;padding:5px 7px;position:absolute;top:7px;right:7px}.artifact-launch{min-height:180px;color:var(--ink);text-align:center;align-content:center;justify-items:center;gap:13px;padding:20px;display:grid}.artifact-launch p{color:#1115129e;max-width:250px;font:var(--type-caption)/1.5 var(--mono);margin:0}.artifact-launch button{color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);min-height:44px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;padding:8px 10px}.artifact-code{height:260px;color:var(--paper);font:var(--type-utility)/1.5 var(--mono);white-space:pre-wrap;background:#0d100e;margin:0;padding:10px;overflow:auto}.settings-layer{z-index:50;background:#080b09a3;justify-content:flex-end;animation:.16s both station-in;display:flex;position:fixed;inset:0}.settings-sheet{width:min(430px,100%);min-height:100%;color:var(--paper);border-left:1px solid var(--phosphor);background:var(--ink);padding:30px 27px;animation:.3s cubic-bezier(.16,1,.3,1) both sheet-in;overflow-y:auto;box-shadow:-24px 0 70px #00000052}.settings-sheet>header{border-bottom:1px solid var(--line-light);justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:37px;padding-bottom:20px;display:flex}.settings-sheet h2{font:400 31px/1 var(--display);margin:0}.sheet-close{border:1px solid var(--line-light);width:35px;height:35px;font:300 22px/1 var(--body);cursor:pointer;background:0 0;border-radius:50%}.sheet-close:hover{color:var(--phosphor);border-color:var(--phosphor)}.setting-group{border-bottom:1px solid var(--line-light);grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;padding:15px 0;display:grid}.setting-group>div,.range-setting>span:first-child{gap:5px;display:grid}.setting-group small,.range-setting small{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.setting-group strong,.range-setting strong{font:500 var(--type-caption)/1 var(--mono)}.setting-locked{color:#e8dfca5c;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.setting-select select{max-width:112px;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);min-height:44px;font:var(--type-utility)/1 var(--mono);border-radius:0;padding:6px 7px}.range-setting{gap:13px;margin-top:29px;display:grid}.range-setting>span:first-child{justify-content:space-between;align-items:end;display:flex}.range-setting input{appearance:none;background:linear-gradient(90deg, var(--phosphor) 0 35%, #e8dfca1c 35%);border:1px solid #e8dfca3d;border-radius:0;width:100%;height:4px;margin:0}.range-setting:first-of-type input{background:var(--phosphor)}.range-setting input::-webkit-slider-thumb{appearance:none;border:2px solid var(--ink);background:var(--phosphor);width:12px;height:18px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab}.range-setting input::-moz-range-thumb{border:2px solid var(--ink);background:var(--phosphor);width:10px;height:16px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab;border-radius:0}.range-ticks{pointer-events:none;justify-content:space-between;margin-top:-13px;display:flex}.range-limits{color:#e8dfca59;font:var(--type-utility)/1 var(--mono);justify-content:space-between;display:flex}.range-limits i{font-style:normal}.system-prompt-setting{gap:9px;margin-top:29px;display:grid}.system-prompt-setting>span{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;display:flex}.system-prompt-setting textarea{resize:vertical;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);font:var(--type-caption)/1.5 var(--mono);padding:10px}.settings-actions{grid-template-columns:1fr 1fr;gap:7px;margin-top:29px;display:grid}.settings-actions button{color:var(--paper);border:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:9px}.settings-actions button:hover{color:var(--phosphor);border-color:var(--phosphor)}.settings-actions .danger-action{color:#e78a62;border-color:#c96f4373;grid-column:span 2}.boot-screen{min-height:100dvh;color:var(--paper);background:var(--ink);justify-content:center;align-items:center;gap:18px;padding:24px;display:flex}.boot-screen h1{color:var(--phosphor);font:400 34px/1 var(--display);margin:0}.boot-screen p:last-child{color:var(--paper-ghost);font:var(--type-caption)/1.5 var(--mono)}.range-ticks i{background:#e8dfca40;width:1px;height:5px}.settings-note{background:#b9ff450f;border:1px solid #b9ff4540;align-items:flex-start;gap:13px;margin-top:42px;padding:15px;display:flex}.settings-note .signal-dot{flex:0 0 7px;margin-top:6px}.settings-note p{color:#e8dfca94;font-size:var(--type-caption);margin:0;line-height:1.5}.settings-note strong{color:var(--phosphor)}@keyframes station-in{0%{opacity:0}to{opacity:1}}@keyframes rise-in{0%{opacity:0;transform:translateY(11px)}to{opacity:1;transform:translateY(0)}}@keyframes note-in{0%{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@keyframes cursor-blink{0%,52%{opacity:1}53%,to{opacity:0}}@keyframes activity-pulse{0%,to{opacity:.55;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}@keyframes sheet-in{0%{opacity:0;transform:translate(34px)}to{opacity:1;transform:translate(0)}}@media (width<=1279px){.station-header{grid-template-columns:240px minmax(260px,1fr) auto}.browser-notice>span>span{display:none}.browser-notice.is-warning>span>span{display:block}.workspace-grid{grid-template-columns:240px minmax(0,1fr);grid-template-areas:"models chat""inspector inspector"}.workspace-grid.is-models-collapsed,.workspace-grid.is-models-collapsed.is-inspector-collapsed{grid-template-columns:0 minmax(0,1fr)}.workspace-grid.is-inspector-collapsed:not(.is-models-collapsed){grid-template-columns:240px minmax(0,1fr)}.model-rail,.chat-surface{height:calc(100dvh - 64px);min-height:calc(100dvh - 64px)}.inspector{border-top:1px solid var(--line-light);border-left:0;grid-template-columns:repeat(2,minmax(0,1fr));height:auto;min-height:auto;display:grid;overflow:visible}.inspector-section{border-right:1px solid var(--line-light);border-bottom:0}}@media (width<=820px){.station-header{grid-template-columns:minmax(0,1fr) auto;min-height:58px;display:grid}.brand-block{border-right:0;min-width:0;padding:10px 14px}.brand-glyph{flex-basis:32px;width:32px;height:32px}.browser-notice{border-top:1px solid var(--line-light);grid-area:2/1/auto/-1;min-height:38px;padding:7px 14px}.header-actions{border-left:0;grid-area:1/2;padding-inline:10px;position:static}.workspace-grid{display:block}.model-rail,.chat-surface,.inspector{height:auto;min-height:auto}.model-rail{border-right:0;border-bottom:1px solid var(--line-light);padding:18px 15px;display:block}.model-list{scroll-snap-type:x mandatory;gap:7px;padding-bottom:7px;display:flex;overflow-x:auto}.model-card{scroll-snap-align:start;min-width:205px}.rail-footnote{display:none}.chat-surface{grid-template-rows:auto minmax(240px,auto) auto;min-height:calc(100dvh - 96px);display:grid;overflow:visible;width:100%!important}.transcript{padding-inline:22px;overflow:visible}.chat-header{padding:17px 19px}.chat-header-meta span:not(.live-label){display:none}.live-label{grid-column:1}.composer{z-index:5;padding-inline:14px;position:relative}.transcript-empty{margin-top:4vh}.inspector{border-top:1px solid var(--line-light);display:block}.inspector-section{border-right:0;border-bottom:1px solid var(--line-light)}}@media (width<=520px){.brand-block h1{font-size:18px}.brand-block>div>span{display:none}.layout-controls{gap:2px}.layout-controls button,.icon-button{width:32px;height:32px}.chat-header{align-items:flex-end}.botanical-subtitle{display:none}.message-user{max-width:91%}.composer-meta>span{display:none}.composer-activity{padding-inline:10px}.activity-status small{max-width:210px}.composer-field{grid-template-columns:1fr;padding-bottom:62px}.send-button{height:52px;position:absolute;bottom:0;right:0}.speed-gauge{grid-template-columns:106px 1fr}.gauge-ring{width:102px;height:102px}.gauge-ring:after{display:none}.telemetry-live-grid{grid-template-columns:1fr}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.bench-page{min-height:100dvh;color:var(--ink);background:var(--paper)}.bench-masthead{min-height:86px;color:var(--paper);border-bottom:1px solid var(--phosphor);background:var(--ink);grid-template-columns:minmax(310px,1fr) minmax(360px,1.2fr) auto;align-items:stretch;display:grid}.bench-masthead .brand-block h1{color:var(--paper)}.bench-masthead-note{border-inline:1px solid var(--line-light);background:linear-gradient(110deg,#b9ff4517,#0000 72%);flex-direction:column;justify-content:center;gap:5px;padding:14px 28px;display:flex}.bench-masthead-note strong{color:var(--phosphor);font:700 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase}.bench-masthead-note span{color:#e8dfca8c;max-width:520px;font:10px/1.35 var(--body)}.bench-return{color:var(--paper);font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;align-items:center;gap:8px;padding:0 25px;text-decoration:none;display:flex}.bench-return:hover{color:var(--phosphor)}.bench-layout{grid-template-columns:minmax(275px,330px) minmax(0,1fr);min-height:calc(100dvh - 86px);display:grid}.bench-controls{color:var(--paper);border-right:1px solid var(--phosphor);background:repeating-radial-gradient(ellipse at -12% 102%, transparent 0 24px, #b9ff450b 25px 26px), var(--ink);padding:28px 22px}.bench-controls h2{font:400 30px/1 var(--display);letter-spacing:-.03em;margin:0}.bench-control-intro{color:#e8dfca80;margin:9px 0 22px;font-size:11px;line-height:1.45}.bench-controls form{gap:15px;display:grid}.bench-field{gap:6px;min-width:0;display:grid}.bench-field>span{color:#e8dfcac7;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-field select,.bench-field input{width:100%;min-width:0;height:39px;color:var(--paper);background:var(--ink-raised);font:10px/1 var(--mono);border:1px solid #e8dfca38;border-radius:0;outline:0;padding:0 10px}.bench-field input::placeholder{color:#e8dfca40}.bench-field select:disabled,.bench-field input:disabled{opacity:.5}.bench-field small{color:#e8dfca59;font:8px/1.2 var(--mono)}.bench-field-pair{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.bench-experimental{background:#c96f430d;border:1px dashed #c96f437a;padding:11px}.bench-experimental summary{color:#e19a76;font:700 8px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;cursor:pointer}.bench-experimental>p{color:#e8dfca75;margin:10px 0;font-size:9px;line-height:1.4}.bench-locks{color:#e8dfca70;font:7px/1.35 var(--mono);gap:5px;margin:10px 0 0;display:grid}.bench-locks div{justify-content:space-between;gap:8px;display:flex}.bench-locks dt,.bench-locks dd{margin:0}.bench-locks dt{color:#e8dfcab8}.bench-gate{border-left:2px solid var(--rust);background:#c96f4314;gap:5px;padding:10px;display:grid}.bench-gate.pass{border-color:var(--phosphor);background:var(--phosphor-dim)}.bench-gate strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase}.bench-gate.pass strong{color:var(--phosphor)}.bench-gate span{color:#e8dfca80;font:8px/1.4 var(--mono);overflow-wrap:anywhere}.bench-run-actions{grid-template-columns:1fr auto;gap:8px;display:grid}.bench-run-button,.bench-stop-button,.bench-section-heading button{border:1px solid var(--ink);background:var(--phosphor);min-height:38px;font:800 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;cursor:pointer;border-radius:0;padding:0 13px}.bench-run-button:disabled{opacity:.35}.bench-stop-button{color:#ffb08a;border-color:var(--rust);background:0 0}.bench-workbench{background:linear-gradient(90deg, #1115120b 1px, transparent 1px) 0 0 / 22px 22px, linear-gradient(#1115120a 1px, transparent 1px) 0 0 / 22px 22px, var(--paper);min-width:0;padding:clamp(26px,4vw,58px)}.bench-hero{border-bottom:1px solid var(--line-dark);grid-template-columns:minmax(320px,1.3fr) minmax(260px,.7fr);align-items:end;gap:34px;padding-bottom:29px;display:grid}.bench-hero h2{max-width:690px;font:400 clamp(42px, 6vw, 82px)/.84 var(--display);letter-spacing:-.065em;margin:0}.bench-hero h2 em{color:#4f7125;font-weight:400}.bench-hero>p{color:#11151294;max-width:430px;margin:0;font-size:11px;line-height:1.55}.bench-progress{gap:8px;padding:16px 0;display:grid}.bench-progress>div{font:8px/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;gap:20px;display:flex}.bench-progress>div span{color:#11151273}.bench-progress-track{background:#11151221;height:3px}.bench-progress-track i{background:#527426;height:100%;transition:width .18s;display:block}.bench-error{color:var(--paper);border-left:3px solid var(--rust);background:var(--ink);gap:5px;margin-bottom:18px;padding:12px 14px;display:grid}.bench-error strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-error span{font:9px/1.4 var(--mono);overflow-wrap:anywhere}.bench-core-section{margin-top:13px}.bench-section-heading{justify-content:space-between;align-items:end;gap:20px;margin-bottom:13px;display:flex}.bench-section-heading h3,.bench-output h3{font:500 24px/1 var(--display);margin:0}.bench-section-heading button{min-height:31px;color:var(--paper);border-color:var(--ink);background:var(--ink)}.bench-core-sample{border:1px solid var(--ink);background:var(--paper-deep);grid-template-columns:repeat(4,minmax(0,1fr));display:grid;position:relative;overflow:hidden}.bench-core-sample:before{content:"";pointer-events:none;opacity:.35;background:repeating-radial-gradient(#0000 0 34px,#1115122b 35px 36px);position:absolute;inset:-130% -5%}.bench-core-sample article{background:#e8dfca91;border-right:1px solid #11151242;flex-direction:column;justify-content:space-between;min-height:128px;padding:17px;display:flex;position:relative}.bench-core-sample article:nth-child(2n){background:#d8cdb499}.bench-core-sample article:last-child{color:var(--paper);background:#111512e8;border-right:0}.bench-core-sample article>span{font:800 8px/1 var(--mono);letter-spacing:.09em;text-transform:uppercase}.bench-core-sample article strong{font:400 clamp(28px, 3vw, 43px)/1 var(--display);letter-spacing:-.04em;margin-block:auto}.bench-core-sample article small{color:#11151280;font:7px/1.2 var(--mono);text-transform:uppercase}.bench-core-sample article:last-child small{color:#e8dfca7a}.bench-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:12px;display:grid}.bench-data-card{border:1px solid var(--line-dark);background:#e8dfcaad;padding:16px}.bench-runtime-card,.bench-tuning-card{grid-column:span 2}.bench-data-card dl{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 26px;margin:0;display:grid}.bench-runtime-card dl{grid-template-columns:minmax(180px,1.5fr) repeat(4,minmax(100px,.7fr))}.bench-tuning-card dl{grid-template-columns:repeat(4,minmax(0,1fr))}.bench-data-card dl div{border-top:1px solid #11151221;gap:4px;min-width:0;padding-top:8px;display:grid}.bench-data-card dt{color:#11151273;font:7px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase}.bench-data-card dd{min-width:0;font:500 16px/1.2 var(--body);text-overflow:ellipsis;white-space:nowrap;margin:0;overflow:hidden}.bench-output{border:1px dashed #1115124d;grid-template-columns:repeat(2,minmax(0,1fr));gap:34px;margin-top:12px;padding:21px;display:grid}.bench-output>div:last-child{border-left:1px solid var(--line-dark);padding-left:34px}.bench-output p:not(.eyebrow){color:#11151299;white-space:pre-wrap;margin:8px 0 0;font-size:11px;line-height:1.5}@media (width<=1050px){.bench-masthead{grid-template-columns:minmax(310px,1fr) auto}.bench-masthead-note{display:none}.bench-layout{grid-template-columns:285px minmax(0,1fr)}.bench-core-sample{grid-template-columns:repeat(2,minmax(0,1fr))}.bench-core-sample article:nth-child(2){border-right:0}.bench-core-sample article:nth-child(-n+2){border-bottom:1px solid #11151242}.bench-runtime-card dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width<=760px){.bench-masthead{align-items:center;display:flex}.bench-masthead .brand-block{flex:1;min-width:0}.bench-return{padding:0 15px;font-size:0}.bench-return span{font-size:17px}.bench-layout{display:block}.bench-controls{border-right:0;border-bottom:1px solid var(--phosphor)}.bench-workbench{padding:28px 16px}.bench-hero,.bench-output{grid-template-columns:1fr;gap:18px}.bench-hero h2{font-size:clamp(42px,14vw,68px)}.bench-detail-grid{display:block}.bench-data-card+.bench-data-card{margin-top:10px}.bench-output>div:last-child{border-top:1px solid var(--line-dark);border-left:0;padding-top:18px;padding-left:0}}@media (width<=480px){.bench-masthead .brand-mark{display:none}.bench-core-sample,.bench-data-card dl,.bench-runtime-card dl,.bench-tuning-card dl{grid-template-columns:1fr}.bench-core-sample article{border-bottom:1px solid #11151242;border-right:0;min-height:105px}.bench-core-sample article:last-child{border-bottom:0}}
|
dist/assets/index-DnMbdiUB.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--ink:#111512;--ink-raised:#191e1a;--ink-soft:#252b25;--paper:#e8dfca;--paper-deep:#d8cdb4;--paper-ghost:#e8dfca9e;--phosphor:#b9ff45;--phosphor-dim:#b9ff4529;--moss:#6f8062;--rust:#c96f43;--line-light:#e8dfca24;--line-dark:#1115122e;--display:"Bodoni 72", Didot, "Iowan Old Style", Baskerville, serif;--body:"Avenir Next Condensed", "Gill Sans", "Trebuchet MS", sans-serif;--mono:"Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;--type-utility:clamp(.6875rem, .15vw + .65rem, .8125rem);--type-caption:clamp(.75rem, .18vw + .7rem, .875rem);--type-body:clamp(1rem, .22vw + .95rem, 1.125rem);--type-heading:clamp(1.75rem, 1.15vw + 1.45rem, 2.5rem);font-family:var(--body);font-synthesis:none;text-rendering:optimizelegibility}*{box-sizing:border-box}html{background:var(--ink)}body{background:linear-gradient(90deg, #1115120f 1px, transparent 1px) 0 0 / 22px 22px, linear-gradient(#1115120b 1px, transparent 1px) 0 0 / 22px 22px, var(--paper);min-width:320px;min-height:100vh;color:var(--paper);font-size:var(--type-body);margin:0}button,textarea,select{font:inherit}button{color:inherit}button:focus-visible,textarea:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--phosphor);outline-offset:3px}button:disabled{cursor:not-allowed}::selection{color:var(--ink);background:var(--phosphor)}.station-frame{isolation:isolate;min-height:100dvh;animation:.58s cubic-bezier(.16,1,.3,1) both station-in}.station-frame:before{z-index:20;pointer-events:none;content:"";opacity:.14;mix-blend-mode:soft-light;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.3'/%3E%3C/svg%3E");position:fixed;inset:0}.station-header{background:var(--ink);border-bottom:1px solid var(--phosphor);z-index:10;grid-template-columns:minmax(330px,1fr) auto minmax(220px,1fr);align-items:stretch;min-height:76px;display:grid;position:relative;box-shadow:0 12px 38px #11151238}.brand-block{align-items:center;gap:14px;padding:12px 24px;display:flex}.brand-block h1{font:400 clamp(25px, 2vw, 34px) / .9 var(--display);letter-spacing:-.035em;margin:0}.brand-block h1 i{color:var(--phosphor);font-weight:400}.brand-mark{background:repeating-radial-gradient(at 52% 62%,#0000 0 4px,#b9ff456b 4.5px 5px);border:1px solid #b9ff457a;border-radius:50%;flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid;transform:rotate(-11deg)}.brand-mark:before,.brand-mark:after,.brand-mark span:before,.brand-mark span:after{content:"";background:var(--phosphor);transform-origin:bottom;width:1px;position:absolute}.brand-mark:before{height:25px;transform:translate(-1px,4px)rotate(0)}.brand-mark:after{height:13px;transform:translate(4px,-2px)rotate(43deg)}.brand-mark span:before{height:11px;transform:translate(-5px,-1px)rotate(-47deg)}.brand-mark span:after{height:8px;transform:translate(3px,-9px)rotate(72deg)}.eyebrow{color:#e8dfca85;font:600 var(--type-utility)/1 var(--mono);letter-spacing:.18em;text-transform:uppercase;margin:0 0 5px}.eyebrow.ink{color:#11151285}.local-promise{border-inline:1px solid var(--line-light);background:linear-gradient(115deg,#b9ff4514,#0000 66%);align-items:center;gap:11px;min-width:360px;padding:0 28px;display:flex}.signal-dot,.live-label i,.sampling-label i{background:var(--phosphor);width:7px;height:7px;box-shadow:0 0 0 5px var(--phosphor-dim), 0 0 17px #b9ff459e;border-radius:50%;display:inline-block}.local-promise span:last-child{gap:3px;display:grid}.local-promise strong{color:var(--phosphor);font:600 var(--type-caption)/1 var(--mono);letter-spacing:.13em;text-transform:uppercase}.local-promise small{color:var(--paper-ghost);font:var(--type-caption)/1.25 var(--body)}.header-actions{justify-self:end;align-items:center;padding-right:18px;display:flex}.station-clock{color:var(--paper-ghost);font-family:var(--mono);justify-items:end;padding:0 18px;display:grid}.station-clock small{font-size:var(--type-utility);letter-spacing:.18em}.station-clock span{font-size:var(--type-caption);letter-spacing:.09em;margin-top:4px}.icon-button{cursor:pointer;background:0 0;border:1px solid #e8dfca3d;border-radius:50%;place-items:center;width:44px;height:44px;transition:color .16s,border-color .16s,transform .16s;display:grid}.icon-button:hover{color:var(--phosphor);border-color:var(--phosphor);transform:rotate(-7deg)}.icon-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:18px}.workspace-grid{grid-template-columns:clamp(280px,19vw,340px) minmax(560px,1fr) clamp(340px,23vw,410px);grid-template-areas:"models chat inspector";min-height:calc(100dvh - 76px);display:grid}.model-rail,.inspector{background:var(--ink);min-width:0}.model-rail{border-right:1px solid var(--line-light);flex-direction:column;grid-area:models;padding:24px 16px 14px;animation:.52s cubic-bezier(.16,1,.3,1) .1s both rise-in;display:flex;position:relative;overflow:hidden}.model-rail:before{z-index:0;content:"";opacity:.18;background:repeating-radial-gradient(ellipse at 51% 47%, transparent 0 7px, var(--moss) 7.5px 8.5px);border-radius:43% 57% 52% 48%;width:265px;height:265px;position:absolute;bottom:44px;left:-103px;transform:rotate(18deg)}.rail-heading,.section-heading{z-index:1;align-items:flex-start;gap:11px;display:flex;position:relative}.rail-heading>span,.section-heading>span:first-child{color:var(--phosphor);font:500 var(--type-utility)/1 var(--mono);border-top:1px solid var(--phosphor);padding-top:5px}.rail-heading h2,.section-heading h2{font:400 26px/1 var(--display);letter-spacing:-.025em;margin:0}.model-list{z-index:1;gap:5px;margin-top:21px;display:grid;position:relative}.model-card{border:1px solid #0000;transition:border-color .18s,background .18s}.model-card.is-active{border-color:var(--phosphor);background:var(--phosphor-dim);box-shadow:inset 3px 0 0 var(--phosphor)}.model-select{text-align:left;width:100%;min-height:76px;color:var(--paper);cursor:pointer;background:0 0;border:0;grid-template-columns:23px 1fr auto;gap:8px;padding:13px 9px 12px;display:grid}.model-card:not(.is-active) .model-select:hover{background:#e8dfca0e}.model-index{color:#e8dfca61;font:var(--type-utility)/1 var(--mono);padding-top:4px}.model-copy{gap:3px;min-width:0;display:grid}.model-title-line{justify-content:space-between;align-items:center;gap:7px;display:flex}.model-title-line strong{font:500 24px/1 var(--display)}.model-copy em{color:#e8dfca75;font:italic var(--type-caption)/1.2 var(--display);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.model-copy>span:last-child{color:var(--paper-ghost);font-size:var(--type-caption);margin-top:3px}.availability{font:600 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--line-light);padding:2px 4px}.availability-loaded,.availability-loading,.availability-cached{color:var(--phosphor);border-color:#b9ff4552}.availability-remote,.availability-limited{color:var(--rust);border-color:#c96f4366}.model-footprint{color:#e8dfca63;font:var(--type-utility)/1 var(--mono);white-space:nowrap;align-self:end}.model-limit-reason{color:#e8dfca94;font:var(--type-utility)/1.35 var(--mono);margin:-4px 8px 9px 30px;display:block}.rail-footnote{z-index:1;color:#e8dfca4d;font:var(--type-utility)/1.2 var(--mono);letter-spacing:.03em;justify-content:space-between;gap:8px;margin-top:auto;display:flex;position:relative}.chat-surface{min-width:0;height:calc(100dvh - 76px);min-height:calc(100dvh - 76px);color:var(--ink);background:radial-gradient(ellipse at 82% 16%, transparent 0 77px, #1115120e 78px 79px, transparent 80px 89px, #1115120b 90px 91px, transparent 92px), linear-gradient(100deg, transparent 0 67%, #11151206 67.2% 67.35%, transparent 67.5%), var(--paper);grid-area:chat;grid-template-rows:auto 19px minmax(0,1fr) auto;animation:.56s cubic-bezier(.16,1,.3,1) .16s both rise-in;display:grid;position:relative;overflow:hidden}.chat-header{border-bottom:1px solid var(--line-dark);justify-content:space-between;align-items:center;gap:20px;min-height:82px;padding:17px clamp(21px,3vw,44px) 13px;display:flex}.chat-header h2{font:500 var(--type-heading)/1 var(--display);letter-spacing:-.045em;margin:0;display:inline}.botanical-subtitle{color:#11151275;font:italic var(--type-caption)/1 var(--display);margin-left:10px}.chat-header-meta{color:#1115128c;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;letter-spacing:.07em;grid-template-columns:repeat(2,auto);gap:5px 13px;display:grid}.live-label{color:#3d551d;grid-column:span 2;justify-self:end;align-items:center;gap:7px;display:flex}.live-label i{background:#5d7e2e;width:5px;height:5px;box-shadow:0 0 0 3px #5a782b21}.live-label.is-dormant{color:#1115127a}.live-label.is-dormant i{background:#1115125c;box-shadow:0 0 0 3px #11151217}.runtime-error{z-index:4;color:var(--paper);border:1px solid var(--rust);background:#111512f5;justify-content:space-between;align-items:center;gap:16px;padding:11px 13px;display:flex;position:absolute;top:105px;left:clamp(21px,3vw,44px);right:clamp(21px,3vw,44px);box-shadow:4px 4px #c96f433d}.runtime-error div{gap:3px;display:grid}.runtime-error strong{color:#ff9a68;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.runtime-error span{font:var(--type-caption)/1.35 var(--mono);overflow-wrap:anywhere}.runtime-error button{min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:1px solid #e8dfca4d;flex:none;padding:5px 8px}.runtime-error button:disabled{cursor:wait;opacity:.5}.runtime-error.download-retry button{max-width:132px;line-height:1.25}.chat-ruler{color:#11151266;font:var(--type-utility)/1 var(--mono);background:repeating-linear-gradient(90deg,#0000 0 14px,#11151233 14px 15px) 0 100%/100% 4px no-repeat;border-bottom:1px solid #11151217;justify-content:space-between;padding-inline:clamp(21px,3vw,44px);display:flex}.transcript{scrollbar-color:#11151247 transparent;min-height:0;padding:20px clamp(21px,4vw,64px) 32px;overflow-y:auto}.transcript-date{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;align-items:center;gap:10px;margin-bottom:22px;display:flex}.transcript-date:before,.transcript-date:after{content:"";background:#11151224;flex:1;height:1px}.transcript-empty{text-align:center;color:#11151280;max-width:430px;margin:12vh auto 0}.transcript-empty>span{color:#507324;font:42px/1 var(--display);display:block}.transcript-empty h3{color:var(--ink);font:400 25px/1 var(--display);margin:8px 0 6px}.transcript-empty p{font-size:var(--type-body);margin:0;line-height:1.5}.message{max-width:690px;margin-bottom:25px;animation:.43s cubic-bezier(.16,1,.3,1) both note-in}.message>header{color:#11151278;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;letter-spacing:.07em;align-items:center;gap:8px;margin-bottom:8px;display:flex}.message>header strong{color:var(--ink)}.message>header time{margin-left:auto}.message-ordinal{color:#4d6728;min-width:22px}.message-content{font:var(--type-body)/1.58 var(--body)}.message-content>p{margin:0}.markdown-content>:first-child{margin-top:0}.markdown-content>:last-child{margin-bottom:0}.markdown-content p,.markdown-content ul,.markdown-content ol,.markdown-content blockquote{margin:0 0 10px}.markdown-content ul,.markdown-content ol{padding-left:22px}.markdown-content h3,.markdown-content h4,.markdown-content h5{font-family:var(--display);margin:16px 0 7px;font-weight:500;line-height:1.1}.markdown-content blockquote{color:#111512ad;white-space:pre-wrap;border-left:2px solid #5073248c;padding-left:12px}.markdown-content code{font:.78em/1.4 var(--mono);background:#11151214;padding:1px 4px}.markdown-content pre{max-width:100%;color:var(--paper);background:var(--ink);border-left:2px solid var(--phosphor);margin:11px 0;padding:12px;position:relative;overflow:auto}.markdown-content pre code{white-space:pre;background:0 0;padding:0}.markdown-content a{color:#3f6417;text-underline-offset:2px;text-decoration-thickness:1px}.message-user .markdown-content code{background:#e8dfca1f}.reasoning-block{color:#1115129e;font-size:var(--type-caption);background:#11151209;border:1px dashed #1115123d;margin:0 0 11px;padding:8px 10px}.reasoning-block summary{color:#4d6728;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;cursor:pointer}.reasoning-block[open] summary{margin-bottom:8px}.generation-placeholder{color:#11151273;margin:0;font-style:italic}.message-error{color:#8d321b;font:var(--type-caption)/1.4 var(--mono);margin:9px 0 0}.message.has-error{border-color:var(--rust)}.message-assistant{border-left:2px solid var(--ink);padding-left:22px}.message-assistant .message-content>p:first-letter{float:left;color:#38511d;font:42px/.75 var(--display);margin:1px 6px 0 0}.message-user{max-width:min(78%,610px);color:var(--paper);background:var(--ink);clip-path:polygon(0 0,calc(100% - 10px) 0,100% 10px,100% 100%,0 100%);margin-left:auto;padding:12px 15px 13px;box-shadow:5px 5px #1115121f}.message-user>header,.message-user>header strong{color:#e8dfca8f}.message-user .message-ordinal{color:var(--phosphor)}.message>footer{color:#1115126e;font:var(--type-utility)/1.2 var(--mono);border-top:1px dashed #11151230;gap:12px;margin-top:9px;padding-top:7px;display:flex}.stream-cursor{vertical-align:-2px;background:#507324;width:7px;height:14px;margin-left:4px;animation:.86s step-end infinite cursor-blink;display:inline-block}.tool-call{background:#1115120b;border:1px solid #1115122e;margin-top:13px}.tool-call summary{cursor:pointer;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;padding:8px 10px;list-style:none;display:grid}.tool-call summary::-webkit-details-marker{display:none}.tool-call code{font:var(--type-caption)/1.4 var(--mono)}.tool-call summary>span:last-child{color:#1115127a;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.tool-state,.event-pip{background:var(--moss);border-radius:50%;width:6px;height:6px}.tool-state-complete,.event-complete{background:#55772c}.tool-state-running,.event-running{background:var(--phosphor);box-shadow:0 0 0 3px #5a782b26}.tool-state-error,.event-error{background:var(--rust)}.tool-call>div{border-top:1px solid #11151221;padding:9px 10px}.tool-call>div p{color:#11151294;font-size:var(--type-caption);margin:5px 0 0}.composer{border-top:1px solid var(--line-dark);-webkit-backdrop-filter:blur(9px);backdrop-filter:blur(9px);background:#d8cdb4d1;padding:12px clamp(21px,3vw,44px) 18px;position:relative;box-shadow:0 -15px 38px #1115120f}.composer:before{content:"";background:repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 12px);opacity:.23;height:5px;position:absolute;top:-5px;left:0;right:0}.composer-activity{height:164px;color:var(--paper);background:linear-gradient(105deg, #b9ff4512, transparent 58%), var(--ink);border:1px solid #111512d1;grid-template-rows:24px 26px 40px 34px;gap:6px;margin-bottom:10px;padding:11px 12px;display:grid;overflow:hidden;box-shadow:4px 4px #1115121f}.activity-stage-line{grid-template-columns:repeat(5,minmax(0,1fr));align-items:start;gap:6px;display:grid}.activity-stage-line span{color:#e8dfca57;min-width:0;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.04em;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap;border-top:1px solid #e8dfca24;align-items:center;gap:5px;padding-top:5px;display:flex;overflow:hidden}.activity-stage-line i{border:1px solid;border-radius:50%;flex:0 0 6px;width:6px;height:6px}.activity-stage-line span.is-complete,.activity-stage-line span.is-current{color:var(--phosphor);border-color:#b9ff4585}.activity-stage-line span.is-complete i{background:var(--phosphor)}.activity-stage-line span.is-current i{box-shadow:0 0 0 4px var(--phosphor-dim)}.activity-status,.activity-status>div{align-items:center;display:flex}.activity-status{border-bottom:1px solid var(--line-light);min-width:0;font:var(--type-utility)/1 var(--mono);justify-content:space-between;gap:12px}.activity-status>div{gap:8px;min-width:0}.activity-status strong{color:var(--paper);letter-spacing:.04em;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap;font-weight:600;overflow:hidden}.activity-status>span{color:#e8dfca80;flex:none}.activity-pulse{background:#e8dfca59;border-radius:50%;flex:0 0 7px;width:7px;height:7px}.composer-activity.is-loading .activity-pulse,.composer-activity.is-inference .activity-pulse{background:var(--phosphor);box-shadow:0 0 0 4px var(--phosphor-dim);animation:1.15s ease-in-out infinite activity-pulse}.load-live-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;align-items:end;gap:12px;display:grid}.activity-track-block,.live-metric{gap:6px;min-width:0;display:grid}.activity-track-block>div,.live-metric>div{font:var(--type-utility)/1 var(--mono);justify-content:space-between;align-items:baseline;gap:8px;display:flex}.activity-track-block span,.live-metric span,.activity-memory span{color:#e8dfca75;text-transform:uppercase}.activity-track-block small,.live-metric small{color:#e8dfca80;font-size:inherit}.activity-memory{min-width:86px;font:var(--type-utility)/1 var(--mono);justify-items:end;gap:6px;display:grid}.activity-memory strong{color:var(--phosphor);font-size:var(--type-caption);font-weight:600}.activity-track{background:#e8dfca0e;border:1px solid #e8dfca2e;height:5px;overflow:hidden}.activity-track>span{background:repeating-linear-gradient(90deg, var(--phosphor) 0 7px, transparent 7px 9px);height:100%;transition:width .12s linear;display:block;box-shadow:0 0 10px #b9ff4557}.inference-live-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;display:grid}.live-metric{border:1px solid var(--line-light);opacity:.64;padding:5px 8px 6px}.live-metric.is-active{opacity:1;background:#b9ff450f;border-color:#b9ff457a}.live-metric strong{color:var(--phosphor);font-size:var(--type-caption);margin-left:auto;font-weight:600}.shard-strip{grid-template-columns:repeat(8,minmax(0,1fr));align-items:end;gap:5px;min-width:0;display:grid}.shard-strip.is-empty{border-top:1px dashed var(--line-light);align-items:center;display:flex}.shard-placeholder{color:#e8dfca52;font:var(--type-utility)/1.2 var(--mono)}.shard-cell{opacity:.55;gap:5px;min-width:0;display:grid}.shard-cell.state-downloading,.shard-cell.state-verifying,.shard-cell.state-complete,.shard-cell.state-cached{opacity:1}.shard-cell>span{color:#e8dfca7a;font:var(--type-utility)/1 var(--mono);justify-content:space-between;gap:3px;display:flex}.shard-cell b{color:var(--paper);font-weight:600}.shard-cell small{font-size:inherit;text-overflow:ellipsis;overflow:hidden}.composer-meta{color:#11151275;min-height:24px;font:var(--type-utility)/1 var(--mono);justify-content:space-between;align-items:center;gap:12px;margin-bottom:7px;display:flex}.tool-toggle{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:0;align-items:center;gap:5px;padding:0 2px;display:flex}.tool-toggle.is-on{color:#425f1f}.regenerate-button{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:0;margin-right:auto;padding:0 2px}.regenerate-button:disabled{cursor:default;opacity:.45}.tool-toggle svg{fill:none;stroke:currentColor;stroke-width:1.3px;width:12px}.composer-field{border:1px solid var(--ink);background:#e8dfcad4;grid-template-columns:1fr auto;min-height:84px;display:grid;position:relative;box-shadow:4px 4px #1115121a}.composer-field label{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;position:absolute;top:7px;left:11px}.composer-field textarea{resize:none;min-width:0;color:var(--ink);font:var(--type-body)/1.45 var(--body);background:0 0;border:0;outline:none;padding:23px 13px 9px}.composer-field textarea::placeholder{color:#11151257}.send-button{min-width:112px;min-height:52px;color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);font:700 var(--type-caption)/1 var(--mono);text-transform:uppercase;cursor:pointer;justify-content:center;align-items:center;gap:7px;margin:7px;transition:transform .15s,box-shadow .15s,background .15s;display:flex}.send-button:hover:not(:disabled){box-shadow:3px 3px 0 var(--ink);transform:translate(-2px,-2px)}.send-button:disabled{opacity:.34}.send-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:15px}.send-button.stop{color:var(--paper);background:var(--ink)}.send-button.stop span{background:var(--rust);width:7px;height:7px}.send-button.load{background:var(--phosphor)}.inspector{border-left:1px solid var(--line-light);scrollbar-color:#e8dfca2e transparent;grid-area:inspector;height:calc(100dvh - 76px);min-height:calc(100dvh - 76px);animation:.57s cubic-bezier(.16,1,.3,1) .22s both rise-in;overflow-y:auto}.inspector-section{border-bottom:1px solid var(--line-light);padding:20px 18px}.section-heading{align-items:center;margin-bottom:17px}.section-heading.compact{margin-bottom:14px}.section-heading>div{flex:1}.section-heading h2{font-size:24px}.sampling-label{color:#e8dfca6b;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;letter-spacing:.06em;align-items:center;gap:6px;display:flex}.sampling-label i{width:4px;height:4px}.speed-gauge{grid-template-columns:116px 1fr;align-items:center;gap:15px;display:grid}.gauge-ring{background:radial-gradient(circle, var(--ink) 0 51%, transparent 52%), conic-gradient(var(--phosphor) 0 var(--gauge), #e8dfca1c var(--gauge) 100%);border-radius:49% 51% 47% 53%;place-items:center;width:116px;height:116px;display:grid;transform:rotate(-9deg);box-shadow:inset 0 0 0 1px #e8dfca1a,0 0 30px #b9ff450e}.gauge-ring:after{content:"";border:1px dashed #e8dfca21;border-radius:45% 55% 52% 48%;width:128px;height:128px;position:absolute}.gauge-ring>div{z-index:1;justify-items:center;display:grid;position:relative;transform:rotate(9deg)}.gauge-ring strong{color:var(--phosphor);font:400 29px/1 var(--display)}.gauge-ring small{color:var(--paper-ghost);font:var(--type-utility)/1 var(--mono);text-transform:uppercase;margin-top:3px}.speed-gauge dl{margin:0}.speed-gauge dl div{border-bottom:1px solid var(--line-light);font:var(--type-utility)/1 var(--mono);justify-content:space-between;padding:7px 0;display:flex}.speed-gauge dt{color:#e8dfca6e}.speed-gauge dd{color:var(--paper);margin:0}.device-card{background:linear-gradient(90deg,#b9ff4514,#0000);border:1px solid #b9ff453b;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;margin:19px 0 14px;padding:10px;display:grid}.device-icon{border:1px solid #b9ff4540;align-items:flex-end;gap:3px;width:28px;height:28px;padding:5px;display:flex}.device-icon i{background:var(--phosphor);width:4px}.device-icon i:first-child{opacity:.45;height:7px}.device-icon i:nth-child(2){height:15px}.device-icon i:nth-child(3){opacity:.7;height:11px}.device-card>div{gap:3px;display:grid}.device-card small,.memory-grid small,.meter small{color:#e8dfca6b;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.device-card strong{font:500 var(--type-caption)/1 var(--mono)}.device-card>span:last-child{color:var(--phosphor);font:600 15px/1 var(--mono);justify-items:end;display:grid}.device-card>span:last-child small{color:#e8dfca5e;font-size:var(--type-utility);margin-top:3px}.meter{margin-top:13px}.meter>div:first-child{font:var(--type-utility)/1 var(--mono);justify-content:space-between;margin-bottom:6px;display:flex}.meter-track{border:1px solid #e8dfca36;height:5px;padding:1px}.meter-track>span{background:repeating-linear-gradient(90deg, var(--phosphor) 0 5px, transparent 5px 7px);height:100%;display:block}.memory-grid{background:var(--line-light);grid-template-columns:1fr 1fr;gap:1px;margin-top:14px;display:grid}.memory-grid>div{background:var(--ink);gap:6px;padding:10px;display:grid}.memory-grid strong{font:500 var(--type-caption)/1 var(--mono)}.event-log{gap:0;margin:0;padding:0;list-style:none;display:grid;position:relative}.event-log:before{content:"";background:var(--line-light);width:1px;position:absolute;top:5px;bottom:5px;left:3px}.event-log li{grid-template-columns:8px 64px 1fr;align-items:start;gap:8px;padding:7px 0;display:grid}.event-log li.event-empty{color:#e8dfca5c;font:var(--type-utility)/1.4 var(--mono);padding:5px 0;display:block}.event-pip{z-index:1;border:1px solid var(--ink);margin-top:2px}.event-log time{color:#e8dfca57;font:var(--type-utility)/1.2 var(--mono)}.event-log div{gap:3px;display:grid}.event-log strong{font:500 var(--type-caption)/1 var(--mono)}.event-log small{color:#e8dfca6b;font-size:var(--type-utility)}.artifact-close{border:1px solid var(--line-light);cursor:pointer;background:0 0;border-radius:50%;width:25px;height:25px}.artifact-empty{color:#e8dfca66;font:var(--type-utility)/1.5 var(--mono);margin:0}.artifact-pane{border:1px solid var(--line-light);background:#f8f5eb}.artifact-tabs{color:var(--paper);border-bottom:1px solid var(--line-light);background:var(--ink-raised);grid-template-columns:auto auto 1fr;align-items:center;display:grid}.artifact-tabs button{border:0;border-right:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:7px 9px}.artifact-tabs button[aria-selected=true]{color:var(--ink);background:var(--phosphor)}.artifact-tabs span{color:#e8dfca73;font:var(--type-utility)/1 var(--mono);text-align:right;text-overflow:ellipsis;white-space:nowrap;padding:0 8px;overflow:hidden}.artifact-pane iframe{background:#fff;border:0;width:100%;height:260px;display:block}.artifact-running{position:relative}.artifact-running>button{z-index:2;color:var(--paper);border:1px solid var(--rust);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:#111512e6;padding:5px 7px;position:absolute;top:7px;right:7px}.artifact-launch{min-height:180px;color:var(--ink);text-align:center;align-content:center;justify-items:center;gap:13px;padding:20px;display:grid}.artifact-launch p{color:#1115129e;max-width:250px;font:var(--type-caption)/1.5 var(--mono);margin:0}.artifact-launch button{color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);min-height:44px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;padding:8px 10px}.artifact-code{height:260px;color:var(--paper);font:var(--type-utility)/1.5 var(--mono);white-space:pre-wrap;background:#0d100e;margin:0;padding:10px;overflow:auto}.settings-layer{z-index:50;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#080b09a3;justify-content:flex-end;animation:.16s both station-in;display:flex;position:fixed;inset:0}.settings-sheet{width:min(430px,100%);min-height:100%;color:var(--paper);border-left:1px solid var(--phosphor);background:repeating-radial-gradient(ellipse at 104% 101%, transparent 0 13px, #b9ff450e 13.5px 14.5px), var(--ink);padding:30px 27px;animation:.3s cubic-bezier(.16,1,.3,1) both sheet-in;overflow-y:auto;box-shadow:-24px 0 70px #00000052}.settings-sheet>header{border-bottom:1px solid var(--line-light);justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:37px;padding-bottom:20px;display:flex}.settings-sheet h2{font:400 31px/1 var(--display);margin:0}.sheet-close{border:1px solid var(--line-light);width:35px;height:35px;font:300 22px/1 var(--body);cursor:pointer;background:0 0;border-radius:50%}.sheet-close:hover{color:var(--phosphor);border-color:var(--phosphor)}.setting-group{border-bottom:1px solid var(--line-light);grid-template-columns:24px 1fr auto;align-items:center;gap:12px;padding:15px 0;display:grid}.setting-number{color:var(--phosphor);font:var(--type-utility)/1 var(--mono)}.setting-group>div,.range-setting>span:first-child{gap:5px;display:grid}.setting-group small,.range-setting small{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.setting-group strong,.range-setting strong{font:500 var(--type-caption)/1 var(--mono)}.setting-locked{color:#e8dfca5c;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.setting-select select{max-width:112px;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);min-height:44px;font:var(--type-utility)/1 var(--mono);border-radius:0;padding:6px 7px}.range-setting{gap:13px;margin-top:29px;display:grid}.range-setting>span:first-child{justify-content:space-between;align-items:end;display:flex}.range-setting input{appearance:none;background:linear-gradient(90deg, var(--phosphor) 0 35%, #e8dfca1c 35%);border:1px solid #e8dfca3d;border-radius:0;width:100%;height:4px;margin:0}.range-setting:first-of-type input{background:var(--phosphor)}.range-setting input::-webkit-slider-thumb{appearance:none;border:2px solid var(--ink);background:var(--phosphor);width:12px;height:18px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab}.range-setting input::-moz-range-thumb{border:2px solid var(--ink);background:var(--phosphor);width:10px;height:16px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab;border-radius:0}.range-ticks{pointer-events:none;justify-content:space-between;margin-top:-13px;display:flex}.range-limits{color:#e8dfca59;font:var(--type-utility)/1 var(--mono);justify-content:space-between;display:flex}.range-limits i{font-style:normal}.system-prompt-setting{gap:9px;margin-top:29px;display:grid}.system-prompt-setting>span{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;display:flex}.system-prompt-setting textarea{resize:vertical;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);font:var(--type-caption)/1.5 var(--mono);padding:10px}.settings-actions{grid-template-columns:1fr 1fr;gap:7px;margin-top:29px;display:grid}.settings-actions button{color:var(--paper);border:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:9px}.settings-actions button:hover{color:var(--phosphor);border-color:var(--phosphor)}.settings-actions .danger-action{color:#e78a62;border-color:#c96f4373;grid-column:span 2}.boot-screen{min-height:100dvh;color:var(--paper);background:var(--ink);justify-content:center;align-items:center;gap:18px;padding:24px;display:flex}.boot-screen h1{color:var(--phosphor);font:400 34px/1 var(--display);margin:0}.boot-screen p:last-child{color:var(--paper-ghost);font:var(--type-caption)/1.5 var(--mono)}.range-ticks i{background:#e8dfca40;width:1px;height:5px}.settings-note{background:#b9ff450f;border:1px solid #b9ff4540;align-items:flex-start;gap:13px;margin-top:42px;padding:15px;display:flex}.settings-note .signal-dot{flex:0 0 7px;margin-top:6px}.settings-note p{color:#e8dfca94;font-size:var(--type-caption);margin:0;line-height:1.5}.settings-note strong{color:var(--phosphor)}@keyframes station-in{0%{opacity:0}to{opacity:1}}@keyframes rise-in{0%{opacity:0;transform:translateY(11px)}to{opacity:1;transform:translateY(0)}}@keyframes note-in{0%{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@keyframes cursor-blink{0%,52%{opacity:1}53%,to{opacity:0}}@keyframes activity-pulse{0%,to{opacity:.55;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}@keyframes sheet-in{0%{opacity:0;transform:translate(34px)}to{opacity:1;transform:translate(0)}}@media (width<=1279px){.station-header{grid-template-columns:minmax(320px,1fr) auto auto}.local-promise{min-width:auto}.local-promise small{display:none}.workspace-grid{grid-template-columns:280px minmax(500px,1fr);grid-template-areas:"models chat""inspector inspector"}.model-rail,.chat-surface{height:calc(100dvh - 76px);min-height:calc(100dvh - 76px)}.inspector{border-top:1px solid var(--phosphor);border-left:0;grid-template-columns:repeat(2,minmax(0,1fr));height:auto;min-height:auto;display:grid;overflow:visible}.inspector-section{border-right:1px solid var(--line-light);border-bottom:0}}@media (width<=820px){.station-header{min-height:70px;display:block}.brand-block{padding:11px 70px 11px 15px}.brand-mark{flex-basis:36px;width:36px;height:36px}.local-promise{border:0;border-top:1px solid var(--line-light);min-height:31px;padding:0 16px}.header-actions{position:absolute;top:10px;right:0}.station-clock{display:none}.workspace-grid{display:block}.model-rail,.chat-surface,.inspector{height:auto;min-height:auto}.model-rail{border-right:0;border-bottom:1px solid var(--phosphor);padding:18px 15px;display:block}.model-list{scroll-snap-type:x mandatory;gap:7px;padding-bottom:7px;display:flex;overflow-x:auto}.model-card{scroll-snap-align:start;min-width:205px}.rail-footnote{display:none}.chat-surface{grid-template-rows:auto 19px minmax(240px,auto) auto;display:grid;overflow:visible}.transcript{padding-inline:22px;overflow:visible}.chat-header{padding:17px 19px}.chat-header-meta span:not(.live-label){display:none}.live-label{grid-column:1}.composer{z-index:5;padding-inline:14px;position:relative}.transcript-empty{margin-top:4vh}.inspector{border-top:1px solid var(--phosphor);display:block}.inspector-section{border-right:0;border-bottom:1px solid var(--line-light)}}@media (width<=520px){.brand-block h1{font-size:24px}.brand-block .eyebrow,.local-promise strong{font-size:var(--type-utility)}.chat-header{align-items:flex-end}.botanical-subtitle{margin:4px 0 0;display:block}.message-user{max-width:91%}.composer-meta>span{display:none}.composer-activity{grid-template-rows:24px 30px 44px 36px;height:176px;padding-inline:9px}.activity-stage-line{gap:3px}.activity-stage-line span{letter-spacing:0;gap:3px}.activity-stage-line i{display:none}.load-live-grid{gap:7px}.activity-memory{min-width:68px}.shard-strip{gap:3px}.shard-cell small{display:none}.composer-field{grid-template-columns:1fr;padding-bottom:62px}.send-button{height:52px;position:absolute;bottom:0;right:0}.speed-gauge{grid-template-columns:106px 1fr}.gauge-ring{width:102px;height:102px}.gauge-ring:after{width:113px;height:113px}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.bench-page{min-height:100dvh;color:var(--ink);background:var(--paper)}.bench-masthead{min-height:86px;color:var(--paper);border-bottom:1px solid var(--phosphor);background:var(--ink);grid-template-columns:minmax(310px,1fr) minmax(360px,1.2fr) auto;align-items:stretch;display:grid}.bench-masthead .brand-block h1{color:var(--paper)}.bench-masthead-note{border-inline:1px solid var(--line-light);background:linear-gradient(110deg,#b9ff4517,#0000 72%);flex-direction:column;justify-content:center;gap:5px;padding:14px 28px;display:flex}.bench-masthead-note strong{color:var(--phosphor);font:700 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase}.bench-masthead-note span{color:#e8dfca8c;max-width:520px;font:10px/1.35 var(--body)}.bench-return{color:var(--paper);font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;align-items:center;gap:8px;padding:0 25px;text-decoration:none;display:flex}.bench-return:hover{color:var(--phosphor)}.bench-layout{grid-template-columns:minmax(275px,330px) minmax(0,1fr);min-height:calc(100dvh - 86px);display:grid}.bench-controls{color:var(--paper);border-right:1px solid var(--phosphor);background:repeating-radial-gradient(ellipse at -12% 102%, transparent 0 24px, #b9ff450b 25px 26px), var(--ink);padding:28px 22px}.bench-controls h2{font:400 30px/1 var(--display);letter-spacing:-.03em;margin:0}.bench-control-intro{color:#e8dfca80;margin:9px 0 22px;font-size:11px;line-height:1.45}.bench-controls form{gap:15px;display:grid}.bench-field{gap:6px;min-width:0;display:grid}.bench-field>span{color:#e8dfcac7;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-field select,.bench-field input{width:100%;min-width:0;height:39px;color:var(--paper);background:var(--ink-raised);font:10px/1 var(--mono);border:1px solid #e8dfca38;border-radius:0;outline:0;padding:0 10px}.bench-field input::placeholder{color:#e8dfca40}.bench-field select:disabled,.bench-field input:disabled{opacity:.5}.bench-field small{color:#e8dfca59;font:8px/1.2 var(--mono)}.bench-field-pair{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.bench-experimental{background:#c96f430d;border:1px dashed #c96f437a;padding:11px}.bench-experimental summary{color:#e19a76;font:700 8px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;cursor:pointer}.bench-experimental>p{color:#e8dfca75;margin:10px 0;font-size:9px;line-height:1.4}.bench-locks{color:#e8dfca70;font:7px/1.35 var(--mono);gap:5px;margin:10px 0 0;display:grid}.bench-locks div{justify-content:space-between;gap:8px;display:flex}.bench-locks dt,.bench-locks dd{margin:0}.bench-locks dt{color:#e8dfcab8}.bench-gate{border-left:2px solid var(--rust);background:#c96f4314;gap:5px;padding:10px;display:grid}.bench-gate.pass{border-color:var(--phosphor);background:var(--phosphor-dim)}.bench-gate strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase}.bench-gate.pass strong{color:var(--phosphor)}.bench-gate span{color:#e8dfca80;font:8px/1.4 var(--mono);overflow-wrap:anywhere}.bench-run-actions{grid-template-columns:1fr auto;gap:8px;display:grid}.bench-run-button,.bench-stop-button,.bench-section-heading button{border:1px solid var(--ink);background:var(--phosphor);min-height:38px;font:800 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;cursor:pointer;border-radius:0;padding:0 13px}.bench-run-button:disabled{opacity:.35}.bench-stop-button{color:#ffb08a;border-color:var(--rust);background:0 0}.bench-workbench{background:linear-gradient(90deg, #1115120b 1px, transparent 1px) 0 0 / 22px 22px, linear-gradient(#1115120a 1px, transparent 1px) 0 0 / 22px 22px, var(--paper);min-width:0;padding:clamp(26px,4vw,58px)}.bench-hero{border-bottom:1px solid var(--line-dark);grid-template-columns:minmax(320px,1.3fr) minmax(260px,.7fr);align-items:end;gap:34px;padding-bottom:29px;display:grid}.bench-hero h2{max-width:690px;font:400 clamp(42px, 6vw, 82px)/.84 var(--display);letter-spacing:-.065em;margin:0}.bench-hero h2 em{color:#4f7125;font-weight:400}.bench-hero>p{color:#11151294;max-width:430px;margin:0;font-size:11px;line-height:1.55}.bench-progress{gap:8px;padding:16px 0;display:grid}.bench-progress>div{font:8px/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;gap:20px;display:flex}.bench-progress>div span{color:#11151273}.bench-progress-track{background:#11151221;height:3px}.bench-progress-track i{background:#527426;height:100%;transition:width .18s;display:block}.bench-error{color:var(--paper);border-left:3px solid var(--rust);background:var(--ink);gap:5px;margin-bottom:18px;padding:12px 14px;display:grid}.bench-error strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-error span{font:9px/1.4 var(--mono);overflow-wrap:anywhere}.bench-core-section{margin-top:13px}.bench-section-heading{justify-content:space-between;align-items:end;gap:20px;margin-bottom:13px;display:flex}.bench-section-heading h3,.bench-output h3{font:500 24px/1 var(--display);margin:0}.bench-section-heading button{min-height:31px;color:var(--paper);border-color:var(--ink);background:var(--ink)}.bench-core-sample{border:1px solid var(--ink);background:var(--paper-deep);grid-template-columns:repeat(4,minmax(0,1fr));display:grid;position:relative;overflow:hidden}.bench-core-sample:before{content:"";pointer-events:none;opacity:.35;background:repeating-radial-gradient(#0000 0 34px,#1115122b 35px 36px);position:absolute;inset:-130% -5%}.bench-core-sample article{background:#e8dfca91;border-right:1px solid #11151242;flex-direction:column;justify-content:space-between;min-height:128px;padding:17px;display:flex;position:relative}.bench-core-sample article:nth-child(2n){background:#d8cdb499}.bench-core-sample article:last-child{color:var(--paper);background:#111512e8;border-right:0}.bench-core-sample article>span{font:800 8px/1 var(--mono);letter-spacing:.09em;text-transform:uppercase}.bench-core-sample article strong{font:400 clamp(28px, 3vw, 43px)/1 var(--display);letter-spacing:-.04em;margin-block:auto}.bench-core-sample article small{color:#11151280;font:7px/1.2 var(--mono);text-transform:uppercase}.bench-core-sample article:last-child small{color:#e8dfca7a}.bench-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:12px;display:grid}.bench-data-card{border:1px solid var(--line-dark);background:#e8dfcaad;padding:16px}.bench-runtime-card,.bench-tuning-card{grid-column:span 2}.bench-data-card dl{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 26px;margin:0;display:grid}.bench-runtime-card dl{grid-template-columns:minmax(180px,1.5fr) repeat(4,minmax(100px,.7fr))}.bench-tuning-card dl{grid-template-columns:repeat(4,minmax(0,1fr))}.bench-data-card dl div{border-top:1px solid #11151221;gap:4px;min-width:0;padding-top:8px;display:grid}.bench-data-card dt{color:#11151273;font:7px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase}.bench-data-card dd{min-width:0;font:500 16px/1.2 var(--body);text-overflow:ellipsis;white-space:nowrap;margin:0;overflow:hidden}.bench-output{border:1px dashed #1115124d;grid-template-columns:repeat(2,minmax(0,1fr));gap:34px;margin-top:12px;padding:21px;display:grid}.bench-output>div:last-child{border-left:1px solid var(--line-dark);padding-left:34px}.bench-output p:not(.eyebrow){color:#11151299;white-space:pre-wrap;margin:8px 0 0;font-size:11px;line-height:1.5}@media (width<=1050px){.bench-masthead{grid-template-columns:minmax(310px,1fr) auto}.bench-masthead-note{display:none}.bench-layout{grid-template-columns:285px minmax(0,1fr)}.bench-core-sample{grid-template-columns:repeat(2,minmax(0,1fr))}.bench-core-sample article:nth-child(2){border-right:0}.bench-core-sample article:nth-child(-n+2){border-bottom:1px solid #11151242}.bench-runtime-card dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width<=760px){.bench-masthead{align-items:center;display:flex}.bench-masthead .brand-block{flex:1;min-width:0}.bench-return{padding:0 15px;font-size:0}.bench-return span{font-size:17px}.bench-layout{display:block}.bench-controls{border-right:0;border-bottom:1px solid var(--phosphor)}.bench-workbench{padding:28px 16px}.bench-hero,.bench-output{grid-template-columns:1fr;gap:18px}.bench-hero h2{font-size:clamp(42px,14vw,68px)}.bench-detail-grid{display:block}.bench-data-card+.bench-data-card{margin-top:10px}.bench-output>div:last-child{border-top:1px solid var(--line-dark);border-left:0;padding-top:18px;padding-left:0}}@media (width<=480px){.bench-masthead .brand-mark{display:none}.bench-core-sample,.bench-data-card dl,.bench-runtime-card dl,.bench-tuning-card dl{grid-template-columns:1fr}.bench-core-sample article{border-bottom:1px solid #11151242;border-right:0;min-height:105px}.bench-core-sample article:last-child{border-bottom:0}}
|
|
|
|
|
|
dist/assets/index-DsVgIwSl.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/index.html
CHANGED
|
@@ -10,8 +10,8 @@
|
|
| 10 |
content="Bonsai local inference field station — private WebGPU language models in your browser."
|
| 11 |
/>
|
| 12 |
<title>Bonsai Field Station</title>
|
| 13 |
-
<script type="module" crossorigin src="/assets/index-
|
| 14 |
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
| 15 |
</head>
|
| 16 |
<body>
|
| 17 |
<div id="root"></div>
|
|
|
|
| 10 |
content="Bonsai local inference field station — private WebGPU language models in your browser."
|
| 11 |
/>
|
| 12 |
<title>Bonsai Field Station</title>
|
| 13 |
+
<script type="module" crossorigin src="/assets/index-DsVgIwSl.js"></script>
|
| 14 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BbReSmeP.css">
|
| 15 |
</head>
|
| 16 |
<body>
|
| 17 |
<div id="root"></div>
|
src/app/App.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
| 18 |
import { AGENT_TOOLS, executeAgentTool, type ArtifactDocument } from '../lib/agent-tools';
|
| 19 |
import type {
|
| 20 |
ChatMessage,
|
|
|
|
| 21 |
ComposerSubmission,
|
| 22 |
ModelLoadProgress,
|
| 23 |
ModelId,
|
|
@@ -31,13 +32,13 @@ import type {
|
|
| 31 |
ToolRun,
|
| 32 |
} from '../lib/contracts';
|
| 33 |
import { formatBytes } from '../lib/format';
|
| 34 |
-
import {
|
|
|
|
| 35 |
import { parseThoughtStream } from '../lib/think-parser';
|
| 36 |
import { parseToolCalls } from '../lib/tool-protocol';
|
| 37 |
import { BonsaiShell } from './components/BonsaiShell';
|
| 38 |
|
| 39 |
const MANIFEST_PATH = 'manifest/models.json';
|
| 40 |
-
const CONVERSATION_ID = 'primary-field-log';
|
| 41 |
const MAX_TOOL_ROUNDS = 5;
|
| 42 |
const MAX_TOOL_CALLS_PER_ROUND = 4;
|
| 43 |
const MAX_TOOL_CALLS_TOTAL = 12;
|
|
@@ -150,6 +151,10 @@ function eventClock(): string {
|
|
| 150 |
return new Date().toISOString().slice(11, 23);
|
| 151 |
}
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
function errorMessage(error: unknown): string {
|
| 154 |
if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
|
| 155 |
return error instanceof Error ? error.message : String(error);
|
|
@@ -275,6 +280,9 @@ export function App() {
|
|
| 275 |
const [loadedModelId, setLoadedModelId] = useState<ModelId | null>(null);
|
| 276 |
const [loadingModelId, setLoadingModelId] = useState<ModelId | null>(null);
|
| 277 |
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
|
|
|
|
|
|
|
|
| 278 |
const [streamState, setStreamState] = useState<StreamState>('idle');
|
| 279 |
const [modelLoadProgress, setModelLoadProgress] = useState<ModelLoadProgress>(INITIAL_MODEL_LOAD_PROGRESS);
|
| 280 |
const [toolsEnabled, setToolsEnabled] = useState(false);
|
|
@@ -415,9 +423,23 @@ export function App() {
|
|
| 415 |
if (!cancelled) setError(`Capability inspection failed: ${errorMessage(capabilityError)}`);
|
| 416 |
}
|
| 417 |
try {
|
| 418 |
-
const
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
} catch {
|
| 422 |
// Private browsing may disable IndexedDB. Chat remains usable for the tab.
|
| 423 |
if (!cancelled) setBootStatus((current) => `${current} · history is session-only`);
|
|
@@ -432,19 +454,24 @@ export function App() {
|
|
| 432 |
|
| 433 |
useEffect(() => {
|
| 434 |
if (!historyReadyRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
const timer = window.setTimeout(() => {
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
title: messages.find((message) => message.role === 'user')?.content.slice(0, 80) || 'Bonsai field log',
|
| 439 |
-
messages,
|
| 440 |
-
modelId: activeModelId,
|
| 441 |
-
updatedAt: Date.now(),
|
| 442 |
-
}).catch(() => setBootStatus((current) => current.includes('history is session-only')
|
| 443 |
? current
|
| 444 |
: `${current} · history is session-only`));
|
| 445 |
}, 250);
|
| 446 |
return () => window.clearTimeout(timer);
|
| 447 |
-
}, [activeModelId, messages]);
|
| 448 |
|
| 449 |
const models = useMemo<ModelOption[]>(() => {
|
| 450 |
if (!manifest) return [];
|
|
@@ -1082,10 +1109,84 @@ export function App() {
|
|
| 1082 |
|
| 1083 |
const handleClearConversation = () => {
|
| 1084 |
if (operationRef.current) return;
|
|
|
|
|
|
|
|
|
|
| 1085 |
setMessages([]);
|
| 1086 |
setToolEvents([]);
|
| 1087 |
setArtifact(null);
|
| 1088 |
setBootStatus('Conversation cleared · model remains local');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1089 |
};
|
| 1090 |
|
| 1091 |
const effectiveLoadedModelId = (() => {
|
|
@@ -1117,6 +1218,9 @@ export function App() {
|
|
| 1117 |
activeModelId={activeModelId}
|
| 1118 |
loadedModelId={effectiveLoadedModelId}
|
| 1119 |
messages={messages}
|
|
|
|
|
|
|
|
|
|
| 1120 |
streamState={streamState}
|
| 1121 |
modelLoadProgress={modelLoadProgress}
|
| 1122 |
telemetry={telemetry}
|
|
@@ -1141,6 +1245,11 @@ export function App() {
|
|
| 1141 |
onPersistStorage={handlePersistStorage}
|
| 1142 |
onClearStorage={handleClearStorage}
|
| 1143 |
onClearConversation={handleClearConversation}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1144 |
onDismissError={() => setError(null)}
|
| 1145 |
onRetryShard={handleLoadModel}
|
| 1146 |
onCloseArtifact={() => setArtifact(null)}
|
|
|
|
| 18 |
import { AGENT_TOOLS, executeAgentTool, type ArtifactDocument } from '../lib/agent-tools';
|
| 19 |
import type {
|
| 20 |
ChatMessage,
|
| 21 |
+
ChatSession,
|
| 22 |
ComposerSubmission,
|
| 23 |
ModelLoadProgress,
|
| 24 |
ModelId,
|
|
|
|
| 32 |
ToolRun,
|
| 33 |
} from '../lib/contracts';
|
| 34 |
import { formatBytes } from '../lib/format';
|
| 35 |
+
import { conversationTitle, normalizeConversationModelId, upsertConversation } from '../lib/conversations';
|
| 36 |
+
import { deleteConversation, listConversations, saveConversation } from '../lib/idb';
|
| 37 |
import { parseThoughtStream } from '../lib/think-parser';
|
| 38 |
import { parseToolCalls } from '../lib/tool-protocol';
|
| 39 |
import { BonsaiShell } from './components/BonsaiShell';
|
| 40 |
|
| 41 |
const MANIFEST_PATH = 'manifest/models.json';
|
|
|
|
| 42 |
const MAX_TOOL_ROUNDS = 5;
|
| 43 |
const MAX_TOOL_CALLS_PER_ROUND = 4;
|
| 44 |
const MAX_TOOL_CALLS_TOTAL = 12;
|
|
|
|
| 151 |
return new Date().toISOString().slice(11, 23);
|
| 152 |
}
|
| 153 |
|
| 154 |
+
function createConversationId(): string {
|
| 155 |
+
return crypto.randomUUID();
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
function errorMessage(error: unknown): string {
|
| 159 |
if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
|
| 160 |
return error instanceof Error ? error.message : String(error);
|
|
|
|
| 280 |
const [loadedModelId, setLoadedModelId] = useState<ModelId | null>(null);
|
| 281 |
const [loadingModelId, setLoadingModelId] = useState<ModelId | null>(null);
|
| 282 |
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
| 283 |
+
const [conversations, setConversations] = useState<ChatSession[]>([]);
|
| 284 |
+
const [activeConversationId, setActiveConversationId] = useState(createConversationId);
|
| 285 |
+
const [conversationListOpen, setConversationListOpen] = useState(false);
|
| 286 |
const [streamState, setStreamState] = useState<StreamState>('idle');
|
| 287 |
const [modelLoadProgress, setModelLoadProgress] = useState<ModelLoadProgress>(INITIAL_MODEL_LOAD_PROGRESS);
|
| 288 |
const [toolsEnabled, setToolsEnabled] = useState(false);
|
|
|
|
| 423 |
if (!cancelled) setError(`Capability inspection failed: ${errorMessage(capabilityError)}`);
|
| 424 |
}
|
| 425 |
try {
|
| 426 |
+
const storedConversations = (await listConversations<ChatMessage>()).map((conversation) => ({
|
| 427 |
+
...conversation,
|
| 428 |
+
modelId: normalizeConversationModelId(conversation.modelId),
|
| 429 |
+
systemPrompt: typeof conversation.systemPrompt === 'string'
|
| 430 |
+
? conversation.systemPrompt
|
| 431 |
+
: INITIAL_SETTINGS.systemPrompt,
|
| 432 |
+
}));
|
| 433 |
+
const latest = storedConversations[0];
|
| 434 |
+
if (!cancelled) {
|
| 435 |
+
setConversations(storedConversations);
|
| 436 |
+
if (latest) {
|
| 437 |
+
setActiveConversationId(latest.id);
|
| 438 |
+
setActiveModelId(latest.modelId);
|
| 439 |
+
setMessages(latest.messages);
|
| 440 |
+
setSettings((current) => ({ ...current, systemPrompt: latest.systemPrompt }));
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
} catch {
|
| 444 |
// Private browsing may disable IndexedDB. Chat remains usable for the tab.
|
| 445 |
if (!cancelled) setBootStatus((current) => `${current} · history is session-only`);
|
|
|
|
| 454 |
|
| 455 |
useEffect(() => {
|
| 456 |
if (!historyReadyRef.current) return;
|
| 457 |
+
const alreadySaved = conversations.some((conversation) => conversation.id === activeConversationId);
|
| 458 |
+
if (messages.length === 0 && settings.systemPrompt === INITIAL_SETTINGS.systemPrompt && !alreadySaved) return;
|
| 459 |
+
const nextConversation: ChatSession = {
|
| 460 |
+
id: activeConversationId,
|
| 461 |
+
title: conversationTitle(messages),
|
| 462 |
+
messages,
|
| 463 |
+
modelId: activeModelId,
|
| 464 |
+
systemPrompt: settings.systemPrompt,
|
| 465 |
+
updatedAt: Date.now(),
|
| 466 |
+
};
|
| 467 |
const timer = window.setTimeout(() => {
|
| 468 |
+
setConversations((current) => upsertConversation(current, nextConversation));
|
| 469 |
+
void saveConversation(nextConversation).catch(() => setBootStatus((current) => current.includes('history is session-only')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
? current
|
| 471 |
: `${current} · history is session-only`));
|
| 472 |
}, 250);
|
| 473 |
return () => window.clearTimeout(timer);
|
| 474 |
+
}, [activeConversationId, activeModelId, messages, settings.systemPrompt]);
|
| 475 |
|
| 476 |
const models = useMemo<ModelOption[]>(() => {
|
| 477 |
if (!manifest) return [];
|
|
|
|
| 1109 |
|
| 1110 |
const handleClearConversation = () => {
|
| 1111 |
if (operationRef.current) return;
|
| 1112 |
+
const clearedId = activeConversationId;
|
| 1113 |
+
setConversations((current) => current.filter((conversation) => conversation.id !== clearedId));
|
| 1114 |
+
setActiveConversationId(createConversationId());
|
| 1115 |
setMessages([]);
|
| 1116 |
setToolEvents([]);
|
| 1117 |
setArtifact(null);
|
| 1118 |
setBootStatus('Conversation cleared · model remains local');
|
| 1119 |
+
void deleteConversation(clearedId).catch(() => setBootStatus('Conversation cleared for this tab · local history is unavailable'));
|
| 1120 |
+
};
|
| 1121 |
+
|
| 1122 |
+
const handleOpenConversationList = () => {
|
| 1123 |
+
if (operationRef.current) return;
|
| 1124 |
+
const alreadySaved = conversations.some((conversation) => conversation.id === activeConversationId);
|
| 1125 |
+
if (messages.length > 0 || settings.systemPrompt !== INITIAL_SETTINGS.systemPrompt || alreadySaved) {
|
| 1126 |
+
const nextConversation: ChatSession = {
|
| 1127 |
+
id: activeConversationId,
|
| 1128 |
+
title: conversationTitle(messages),
|
| 1129 |
+
messages,
|
| 1130 |
+
modelId: activeModelId,
|
| 1131 |
+
systemPrompt: settings.systemPrompt,
|
| 1132 |
+
updatedAt: Date.now(),
|
| 1133 |
+
};
|
| 1134 |
+
setConversations((current) => upsertConversation(current, nextConversation));
|
| 1135 |
+
void saveConversation(nextConversation).catch(() => undefined);
|
| 1136 |
+
}
|
| 1137 |
+
setConversationListOpen(true);
|
| 1138 |
+
};
|
| 1139 |
+
|
| 1140 |
+
const handleNewConversation = () => {
|
| 1141 |
+
if (operationRef.current) return;
|
| 1142 |
+
setActiveConversationId(createConversationId());
|
| 1143 |
+
setMessages([]);
|
| 1144 |
+
setToolEvents([]);
|
| 1145 |
+
setArtifact(null);
|
| 1146 |
+
setError(null);
|
| 1147 |
+
setStreamState('idle');
|
| 1148 |
+
setSettings((current) => ({ ...current, systemPrompt: INITIAL_SETTINGS.systemPrompt }));
|
| 1149 |
+
setConversationListOpen(false);
|
| 1150 |
+
setBootStatus('New local chat · model remains available');
|
| 1151 |
+
};
|
| 1152 |
+
|
| 1153 |
+
const handleOpenConversation = (conversationId: string) => {
|
| 1154 |
+
if (operationRef.current) return;
|
| 1155 |
+
const conversation = conversations.find((candidate) => candidate.id === conversationId);
|
| 1156 |
+
if (!conversation) return;
|
| 1157 |
+
setActiveConversationId(conversation.id);
|
| 1158 |
+
setMessages(conversation.messages);
|
| 1159 |
+
setToolEvents([]);
|
| 1160 |
+
setArtifact(null);
|
| 1161 |
+
setError(null);
|
| 1162 |
+
setStreamState('idle');
|
| 1163 |
+
setSettings((current) => ({ ...current, systemPrompt: conversation.systemPrompt }));
|
| 1164 |
+
handleModelSelect(conversation.modelId);
|
| 1165 |
+
setConversationListOpen(false);
|
| 1166 |
+
setBootStatus(`Opened local chat · ${conversation.title}`);
|
| 1167 |
+
};
|
| 1168 |
+
|
| 1169 |
+
const handleOpenConversationSettings = (conversationId: string) => {
|
| 1170 |
+
if (operationRef.current) return;
|
| 1171 |
+
handleOpenConversation(conversationId);
|
| 1172 |
+
setSettingsOpen(true);
|
| 1173 |
+
};
|
| 1174 |
+
|
| 1175 |
+
const handleDeleteConversation = (conversationId: string) => {
|
| 1176 |
+
if (operationRef.current) return;
|
| 1177 |
+
const conversation = conversations.find((candidate) => candidate.id === conversationId);
|
| 1178 |
+
if (!conversation || !window.confirm(`Delete “${conversation.title}” from this browser?`)) return;
|
| 1179 |
+
setConversations((current) => current.filter((candidate) => candidate.id !== conversationId));
|
| 1180 |
+
if (conversationId === activeConversationId) {
|
| 1181 |
+
setActiveConversationId(createConversationId());
|
| 1182 |
+
setMessages([]);
|
| 1183 |
+
setToolEvents([]);
|
| 1184 |
+
setArtifact(null);
|
| 1185 |
+
setSettings((current) => ({ ...current, systemPrompt: INITIAL_SETTINGS.systemPrompt }));
|
| 1186 |
+
}
|
| 1187 |
+
void deleteConversation(conversationId)
|
| 1188 |
+
.then(() => setBootStatus('Local chat deleted'))
|
| 1189 |
+
.catch(() => setBootStatus('Chat removed for this tab · local history is unavailable'));
|
| 1190 |
};
|
| 1191 |
|
| 1192 |
const effectiveLoadedModelId = (() => {
|
|
|
|
| 1218 |
activeModelId={activeModelId}
|
| 1219 |
loadedModelId={effectiveLoadedModelId}
|
| 1220 |
messages={messages}
|
| 1221 |
+
conversations={conversations}
|
| 1222 |
+
activeConversationId={activeConversationId}
|
| 1223 |
+
conversationListOpen={conversationListOpen}
|
| 1224 |
streamState={streamState}
|
| 1225 |
modelLoadProgress={modelLoadProgress}
|
| 1226 |
telemetry={telemetry}
|
|
|
|
| 1245 |
onPersistStorage={handlePersistStorage}
|
| 1246 |
onClearStorage={handleClearStorage}
|
| 1247 |
onClearConversation={handleClearConversation}
|
| 1248 |
+
onOpenConversationList={handleOpenConversationList}
|
| 1249 |
+
onNewConversation={handleNewConversation}
|
| 1250 |
+
onOpenConversation={handleOpenConversation}
|
| 1251 |
+
onOpenConversationSettings={handleOpenConversationSettings}
|
| 1252 |
+
onDeleteConversation={handleDeleteConversation}
|
| 1253 |
onDismissError={() => setError(null)}
|
| 1254 |
onRetryShard={handleLoadModel}
|
| 1255 |
onCloseArtifact={() => setArtifact(null)}
|
src/app/components/ArtifactPane.tsx
CHANGED
|
@@ -25,11 +25,7 @@ export function ArtifactPane({ artifact, onClose }: ArtifactPaneProps) {
|
|
| 25 |
return (
|
| 26 |
<section className="inspector-section artifact-section" aria-labelledby="artifact-title">
|
| 27 |
<div className="section-heading compact">
|
| 28 |
-
<
|
| 29 |
-
<div>
|
| 30 |
-
<p className="eyebrow">Isolated terrarium</p>
|
| 31 |
-
<h2 id="artifact-title">Artifact</h2>
|
| 32 |
-
</div>
|
| 33 |
{artifact && <button className="artifact-close" type="button" onClick={onClose} aria-label="Close artifact">×</button>}
|
| 34 |
</div>
|
| 35 |
{!artifact ? (
|
|
|
|
| 25 |
return (
|
| 26 |
<section className="inspector-section artifact-section" aria-labelledby="artifact-title">
|
| 27 |
<div className="section-heading compact">
|
| 28 |
+
<h2 id="artifact-title">Artifact</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
{artifact && <button className="artifact-close" type="button" onClick={onClose} aria-label="Close artifact">×</button>}
|
| 30 |
</div>
|
| 31 |
{!artifact ? (
|
src/app/components/BonsaiShell.tsx
CHANGED
|
@@ -1,35 +1,83 @@
|
|
|
|
|
| 1 |
import type { BonsaiShellProps } from '../../lib/contracts';
|
|
|
|
| 2 |
import { ChatSurface } from './ChatSurface';
|
| 3 |
-
import {
|
|
|
|
| 4 |
import { InspectorPanel } from './InspectorPanel';
|
| 5 |
import { ModelRail } from './ModelRail';
|
| 6 |
import { SettingsSheet } from './SettingsSheet';
|
| 7 |
|
| 8 |
export function BonsaiShell(props: BonsaiShellProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
const activeModel = props.models.find((model) => model.id === props.activeModelId);
|
| 10 |
const busy = props.streamState === 'loading' || props.streamState === 'streaming';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
if (!activeModel) {
|
| 13 |
throw new Error(`Active model ${props.activeModelId} is missing from the model catalog`);
|
| 14 |
}
|
| 15 |
|
| 16 |
return (
|
| 17 |
-
<div className="station-frame">
|
| 18 |
-
<Header
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
<ModelRail
|
|
|
|
| 21 |
models={props.models}
|
| 22 |
activeModelId={props.activeModelId}
|
| 23 |
busy={busy}
|
| 24 |
onModelSelect={props.onModelSelect}
|
| 25 |
/>
|
| 26 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
activeModel={activeModel}
|
| 28 |
loadedModelId={props.loadedModelId}
|
| 29 |
messages={props.messages}
|
| 30 |
streamState={props.streamState}
|
| 31 |
modelLoadProgress={props.modelLoadProgress}
|
| 32 |
-
telemetry={props.telemetry}
|
| 33 |
toolsEnabled={props.toolsEnabled}
|
| 34 |
runtimeStatus={props.runtimeStatus}
|
| 35 |
error={props.error}
|
|
@@ -41,8 +89,11 @@ export function BonsaiShell(props: BonsaiShellProps) {
|
|
| 41 |
onRegenerate={props.onRegenerate}
|
| 42 |
onCancel={props.onCancel}
|
| 43 |
onToolsEnabledChange={props.onToolsEnabledChange}
|
| 44 |
-
|
|
|
|
|
|
|
| 45 |
<InspectorPanel
|
|
|
|
| 46 |
telemetry={props.telemetry}
|
| 47 |
toolEvents={props.toolEvents}
|
| 48 |
artifact={props.artifact}
|
|
@@ -51,6 +102,8 @@ export function BonsaiShell(props: BonsaiShellProps) {
|
|
| 51 |
</div>
|
| 52 |
<SettingsSheet
|
| 53 |
open={props.settingsOpen}
|
|
|
|
|
|
|
| 54 |
activeModel={activeModel}
|
| 55 |
telemetry={props.telemetry}
|
| 56 |
settings={props.settings}
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react';
|
| 2 |
import type { BonsaiShellProps } from '../../lib/contracts';
|
| 3 |
+
import { normalizeThemeMode, resolveThemeMode, THEME_STORAGE_KEY, type ThemeMode } from '../theme';
|
| 4 |
import { ChatSurface } from './ChatSurface';
|
| 5 |
+
import { ConversationList } from './ConversationList';
|
| 6 |
+
import { Header, type ChatWidth } from './Header';
|
| 7 |
import { InspectorPanel } from './InspectorPanel';
|
| 8 |
import { ModelRail } from './ModelRail';
|
| 9 |
import { SettingsSheet } from './SettingsSheet';
|
| 10 |
|
| 11 |
export function BonsaiShell(props: BonsaiShellProps) {
|
| 12 |
+
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
|
| 13 |
+
const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
|
| 14 |
+
const [chatWidth, setChatWidth] = useState<ChatWidth>('centered');
|
| 15 |
+
const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
|
| 16 |
+
try {
|
| 17 |
+
return normalizeThemeMode(globalThis.localStorage?.getItem(THEME_STORAGE_KEY));
|
| 18 |
+
} catch {
|
| 19 |
+
return 'system';
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
const [systemDark, setSystemDark] = useState(() => globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false);
|
| 23 |
const activeModel = props.models.find((model) => model.id === props.activeModelId);
|
| 24 |
const busy = props.streamState === 'loading' || props.streamState === 'streaming';
|
| 25 |
+
const resolvedTheme = resolveThemeMode(themeMode, systemDark);
|
| 26 |
+
|
| 27 |
+
useEffect(() => {
|
| 28 |
+
const media = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
|
| 29 |
+
if (!media) return;
|
| 30 |
+
const handleChange = (event: MediaQueryListEvent) => setSystemDark(event.matches);
|
| 31 |
+
setSystemDark(media.matches);
|
| 32 |
+
media.addEventListener('change', handleChange);
|
| 33 |
+
return () => media.removeEventListener('change', handleChange);
|
| 34 |
+
}, []);
|
| 35 |
+
|
| 36 |
+
useEffect(() => {
|
| 37 |
+
try {
|
| 38 |
+
globalThis.localStorage?.setItem(THEME_STORAGE_KEY, themeMode);
|
| 39 |
+
} catch {
|
| 40 |
+
// Theme still works for the current tab when storage is unavailable.
|
| 41 |
+
}
|
| 42 |
+
}, [themeMode]);
|
| 43 |
|
| 44 |
if (!activeModel) {
|
| 45 |
throw new Error(`Active model ${props.activeModelId} is missing from the model catalog`);
|
| 46 |
}
|
| 47 |
|
| 48 |
return (
|
| 49 |
+
<div className="station-frame" data-theme={resolvedTheme} data-theme-mode={themeMode}>
|
| 50 |
+
<Header
|
| 51 |
+
onOpenSettings={props.onOpenSettings}
|
| 52 |
+
disabled={busy}
|
| 53 |
+
leftSidebarOpen={leftSidebarOpen}
|
| 54 |
+
rightSidebarOpen={rightSidebarOpen}
|
| 55 |
+
chatWidth={chatWidth}
|
| 56 |
+
onToggleLeftSidebar={() => setLeftSidebarOpen((open) => !open)}
|
| 57 |
+
onToggleRightSidebar={() => setRightSidebarOpen((open) => !open)}
|
| 58 |
+
onToggleChatWidth={() => setChatWidth((width) => width === 'centered' ? 'wide' : 'centered')}
|
| 59 |
+
/>
|
| 60 |
+
<div className={`workspace-grid layout-${chatWidth}${leftSidebarOpen ? '' : ' is-models-collapsed'}${rightSidebarOpen ? '' : ' is-inspector-collapsed'}`}>
|
| 61 |
<ModelRail
|
| 62 |
+
hidden={!leftSidebarOpen}
|
| 63 |
models={props.models}
|
| 64 |
activeModelId={props.activeModelId}
|
| 65 |
busy={busy}
|
| 66 |
onModelSelect={props.onModelSelect}
|
| 67 |
/>
|
| 68 |
+
{props.conversationListOpen ? <ConversationList
|
| 69 |
+
conversations={props.conversations}
|
| 70 |
+
activeConversationId={props.activeConversationId}
|
| 71 |
+
onNewConversation={props.onNewConversation}
|
| 72 |
+
onOpenConversation={props.onOpenConversation}
|
| 73 |
+
onOpenConversationSettings={props.onOpenConversationSettings}
|
| 74 |
+
onDeleteConversation={props.onDeleteConversation}
|
| 75 |
+
/> : <ChatSurface
|
| 76 |
activeModel={activeModel}
|
| 77 |
loadedModelId={props.loadedModelId}
|
| 78 |
messages={props.messages}
|
| 79 |
streamState={props.streamState}
|
| 80 |
modelLoadProgress={props.modelLoadProgress}
|
|
|
|
| 81 |
toolsEnabled={props.toolsEnabled}
|
| 82 |
runtimeStatus={props.runtimeStatus}
|
| 83 |
error={props.error}
|
|
|
|
| 89 |
onRegenerate={props.onRegenerate}
|
| 90 |
onCancel={props.onCancel}
|
| 91 |
onToolsEnabledChange={props.onToolsEnabledChange}
|
| 92 |
+
onOpenConversationList={props.onOpenConversationList}
|
| 93 |
+
onOpenSettings={props.onOpenSettings}
|
| 94 |
+
/>}
|
| 95 |
<InspectorPanel
|
| 96 |
+
hidden={!rightSidebarOpen}
|
| 97 |
telemetry={props.telemetry}
|
| 98 |
toolEvents={props.toolEvents}
|
| 99 |
artifact={props.artifact}
|
|
|
|
| 102 |
</div>
|
| 103 |
<SettingsSheet
|
| 104 |
open={props.settingsOpen}
|
| 105 |
+
themeMode={themeMode}
|
| 106 |
+
onThemeModeChange={setThemeMode}
|
| 107 |
activeModel={activeModel}
|
| 108 |
telemetry={props.telemetry}
|
| 109 |
settings={props.settings}
|
src/app/components/ChatSurface.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import type {
|
|
| 7 |
ModelOption,
|
| 8 |
ShardRetryNotice,
|
| 9 |
StreamState,
|
| 10 |
-
TelemetrySnapshot,
|
| 11 |
ToolRun,
|
| 12 |
} from '../../lib/contracts';
|
| 13 |
import { ComposerActivity } from './ComposerActivity';
|
|
@@ -19,7 +18,6 @@ interface ChatSurfaceProps {
|
|
| 19 |
messages: ChatMessage[];
|
| 20 |
streamState: StreamState;
|
| 21 |
modelLoadProgress: ModelLoadProgress;
|
| 22 |
-
telemetry: TelemetrySnapshot;
|
| 23 |
toolsEnabled: boolean;
|
| 24 |
runtimeStatus: string;
|
| 25 |
error: string | null;
|
|
@@ -31,6 +29,8 @@ interface ChatSurfaceProps {
|
|
| 31 |
onRegenerate: () => void;
|
| 32 |
onCancel: () => void;
|
| 33 |
onToolsEnabledChange: (enabled: boolean) => void;
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
export function ChatSurface({
|
|
@@ -39,7 +39,6 @@ export function ChatSurface({
|
|
| 39 |
messages,
|
| 40 |
streamState,
|
| 41 |
modelLoadProgress,
|
| 42 |
-
telemetry,
|
| 43 |
toolsEnabled,
|
| 44 |
runtimeStatus,
|
| 45 |
error,
|
|
@@ -51,6 +50,8 @@ export function ChatSurface({
|
|
| 51 |
onRegenerate,
|
| 52 |
onCancel,
|
| 53 |
onToolsEnabledChange,
|
|
|
|
|
|
|
| 54 |
}: ChatSurfaceProps) {
|
| 55 |
const [prompt, setPrompt] = useState('');
|
| 56 |
const isLoading = streamState === 'loading';
|
|
@@ -83,22 +84,23 @@ export function ChatSurface({
|
|
| 83 |
return (
|
| 84 |
<main className="chat-surface">
|
| 85 |
<header className="chat-header">
|
| 86 |
-
<div>
|
| 87 |
-
<
|
| 88 |
-
|
| 89 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
</div>
|
| 91 |
<div className="chat-header-meta">
|
| 92 |
-
<span>{activeModel.
|
| 93 |
<span>{Math.round(activeModel.contextLimit / 1024)}K context</span>
|
| 94 |
<span className={`live-label${isLoaded ? '' : ' is-dormant'}`}><i /> {isLoaded ? 'local graph' : 'not loaded'}</span>
|
| 95 |
</div>
|
| 96 |
</header>
|
| 97 |
|
| 98 |
-
<div className="chat-ruler" aria-hidden="true">
|
| 99 |
-
<span>00</span><span>¼</span><span>½</span><span>¾</span><span>{Math.round(activeModel.contextLimit / 1024)}K</span>
|
| 100 |
-
</div>
|
| 101 |
-
|
| 102 |
{shardRetry && (
|
| 103 |
<div className="runtime-error download-retry" role="alert">
|
| 104 |
<div>
|
|
@@ -119,26 +121,24 @@ export function ChatSurface({
|
|
| 119 |
)}
|
| 120 |
|
| 121 |
<section className="transcript" aria-label="Conversation transcript" aria-live="polite">
|
| 122 |
-
<div className="transcript-date"><span>
|
| 123 |
{messages.length === 0 && (
|
| 124 |
<div className="transcript-empty">
|
| 125 |
-
<span aria-hidden="true">⌁</span>
|
| 126 |
<h3>No observations yet</h3>
|
| 127 |
<p>Choose a model, keep the backend on Auto, and send a prompt. The model is downloaded once and runs inside this browser.</p>
|
|
|
|
| 128 |
</div>
|
| 129 |
)}
|
| 130 |
-
{messages.map((message
|
| 131 |
-
<Message key={message.id} message={message}
|
| 132 |
))}
|
| 133 |
</section>
|
| 134 |
|
| 135 |
<form className="composer" onSubmit={handleSubmit}>
|
| 136 |
<ComposerActivity
|
| 137 |
modelId={activeModel.id}
|
| 138 |
-
isLoaded={isLoaded}
|
| 139 |
streamState={streamState}
|
| 140 |
progress={modelLoadProgress}
|
| 141 |
-
telemetry={telemetry}
|
| 142 |
/>
|
| 143 |
<div className="composer-meta">
|
| 144 |
<button
|
|
@@ -157,9 +157,10 @@ export function ChatSurface({
|
|
| 157 |
<span>{runtimeStatus}</span>
|
| 158 |
</div>
|
| 159 |
<div className="composer-field">
|
| 160 |
-
<label htmlFor="bonsai-prompt">
|
| 161 |
<textarea
|
| 162 |
id="bonsai-prompt"
|
|
|
|
| 163 |
value={prompt}
|
| 164 |
onChange={(event) => setPrompt(event.target.value)}
|
| 165 |
onKeyDown={handleKeyDown}
|
|
@@ -186,13 +187,12 @@ export function ChatSurface({
|
|
| 186 |
);
|
| 187 |
}
|
| 188 |
|
| 189 |
-
function Message({ message
|
| 190 |
const streaming = message.state === 'streaming';
|
| 191 |
return (
|
| 192 |
<article className={`message message-${message.role}${streaming ? ' is-streaming' : ''}${message.error ? ' has-error' : ''}`}>
|
| 193 |
<header>
|
| 194 |
-
<
|
| 195 |
-
<strong>{message.role === 'assistant' ? 'Bonsai observation' : message.role === 'system' ? 'Station protocol' : 'Field operator'}</strong>
|
| 196 |
<time>{message.timestamp}</time>
|
| 197 |
</header>
|
| 198 |
<div className="message-content">
|
|
@@ -246,6 +246,10 @@ function ToolIcon() {
|
|
| 246 |
);
|
| 247 |
}
|
| 248 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
function ArrowIcon() {
|
| 250 |
return (
|
| 251 |
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
|
|
| 7 |
ModelOption,
|
| 8 |
ShardRetryNotice,
|
| 9 |
StreamState,
|
|
|
|
| 10 |
ToolRun,
|
| 11 |
} from '../../lib/contracts';
|
| 12 |
import { ComposerActivity } from './ComposerActivity';
|
|
|
|
| 18 |
messages: ChatMessage[];
|
| 19 |
streamState: StreamState;
|
| 20 |
modelLoadProgress: ModelLoadProgress;
|
|
|
|
| 21 |
toolsEnabled: boolean;
|
| 22 |
runtimeStatus: string;
|
| 23 |
error: string | null;
|
|
|
|
| 29 |
onRegenerate: () => void;
|
| 30 |
onCancel: () => void;
|
| 31 |
onToolsEnabledChange: (enabled: boolean) => void;
|
| 32 |
+
onOpenConversationList: () => void;
|
| 33 |
+
onOpenSettings: () => void;
|
| 34 |
}
|
| 35 |
|
| 36 |
export function ChatSurface({
|
|
|
|
| 39 |
messages,
|
| 40 |
streamState,
|
| 41 |
modelLoadProgress,
|
|
|
|
| 42 |
toolsEnabled,
|
| 43 |
runtimeStatus,
|
| 44 |
error,
|
|
|
|
| 50 |
onRegenerate,
|
| 51 |
onCancel,
|
| 52 |
onToolsEnabledChange,
|
| 53 |
+
onOpenConversationList,
|
| 54 |
+
onOpenSettings,
|
| 55 |
}: ChatSurfaceProps) {
|
| 56 |
const [prompt, setPrompt] = useState('');
|
| 57 |
const isLoading = streamState === 'loading';
|
|
|
|
| 84 |
return (
|
| 85 |
<main className="chat-surface">
|
| 86 |
<header className="chat-header">
|
| 87 |
+
<div className="chat-header-main">
|
| 88 |
+
<button className="chat-list-button" type="button" disabled={isBusy} onClick={onOpenConversationList} aria-label="Open chat list">
|
| 89 |
+
<BackIcon /> Chats
|
| 90 |
+
</button>
|
| 91 |
+
<button className="chat-settings-button" type="button" disabled={isBusy} onClick={onOpenSettings}>Chat settings</button>
|
| 92 |
+
<div className="chat-title">
|
| 93 |
+
<h2>{activeModel.label}</h2>
|
| 94 |
+
<span className="botanical-subtitle">{activeModel.botanicalName}</span>
|
| 95 |
+
</div>
|
| 96 |
</div>
|
| 97 |
<div className="chat-header-meta">
|
| 98 |
+
<span>{activeModel.footprint} download</span>
|
| 99 |
<span>{Math.round(activeModel.contextLimit / 1024)}K context</span>
|
| 100 |
<span className={`live-label${isLoaded ? '' : ' is-dormant'}`}><i /> {isLoaded ? 'local graph' : 'not loaded'}</span>
|
| 101 |
</div>
|
| 102 |
</header>
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
{shardRetry && (
|
| 105 |
<div className="runtime-error download-retry" role="alert">
|
| 106 |
<div>
|
|
|
|
| 121 |
)}
|
| 122 |
|
| 123 |
<section className="transcript" aria-label="Conversation transcript" aria-live="polite">
|
| 124 |
+
<div className="transcript-date"><span>{logDate}</span></div>
|
| 125 |
{messages.length === 0 && (
|
| 126 |
<div className="transcript-empty">
|
|
|
|
| 127 |
<h3>No observations yet</h3>
|
| 128 |
<p>Choose a model, keep the backend on Auto, and send a prompt. The model is downloaded once and runs inside this browser.</p>
|
| 129 |
+
<button className="system-instructions-button" type="button" onClick={onOpenSettings}>Set system instructions</button>
|
| 130 |
</div>
|
| 131 |
)}
|
| 132 |
+
{messages.map((message) => (
|
| 133 |
+
<Message key={message.id} message={message} />
|
| 134 |
))}
|
| 135 |
</section>
|
| 136 |
|
| 137 |
<form className="composer" onSubmit={handleSubmit}>
|
| 138 |
<ComposerActivity
|
| 139 |
modelId={activeModel.id}
|
|
|
|
| 140 |
streamState={streamState}
|
| 141 |
progress={modelLoadProgress}
|
|
|
|
| 142 |
/>
|
| 143 |
<div className="composer-meta">
|
| 144 |
<button
|
|
|
|
| 157 |
<span>{runtimeStatus}</span>
|
| 158 |
</div>
|
| 159 |
<div className="composer-field">
|
| 160 |
+
<label htmlFor="bonsai-prompt">Prompt</label>
|
| 161 |
<textarea
|
| 162 |
id="bonsai-prompt"
|
| 163 |
+
aria-label="Ask the local specimen"
|
| 164 |
value={prompt}
|
| 165 |
onChange={(event) => setPrompt(event.target.value)}
|
| 166 |
onKeyDown={handleKeyDown}
|
|
|
|
| 187 |
);
|
| 188 |
}
|
| 189 |
|
| 190 |
+
function Message({ message }: { message: ChatMessage }) {
|
| 191 |
const streaming = message.state === 'streaming';
|
| 192 |
return (
|
| 193 |
<article className={`message message-${message.role}${streaming ? ' is-streaming' : ''}${message.error ? ' has-error' : ''}`}>
|
| 194 |
<header>
|
| 195 |
+
<strong>{message.role === 'assistant' ? 'Bonsai' : message.role === 'system' ? 'System' : 'You'}</strong>
|
|
|
|
| 196 |
<time>{message.timestamp}</time>
|
| 197 |
</header>
|
| 198 |
<div className="message-content">
|
|
|
|
| 246 |
);
|
| 247 |
}
|
| 248 |
|
| 249 |
+
function BackIcon() {
|
| 250 |
+
return <svg viewBox="0 0 20 20" aria-hidden="true"><path d="m11.5 5-5 5 5 5M7 10h7" /></svg>;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
function ArrowIcon() {
|
| 254 |
return (
|
| 255 |
<svg viewBox="0 0 20 20" aria-hidden="true">
|
src/app/components/ComposerActivity.test.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { renderToStaticMarkup } from 'react-dom/server';
|
| 2 |
+
import { describe, expect, it } from 'vitest';
|
| 3 |
+
import type { ModelLoadProgress, ModelShardProgress } from '../../lib/contracts';
|
| 4 |
+
import { ComposerActivity, describeLoadProgress, overallLoadProgress } from './ComposerActivity';
|
| 5 |
+
|
| 6 |
+
function shard(index: number, state: ModelShardProgress['state']): ModelShardProgress {
|
| 7 |
+
return {
|
| 8 |
+
index,
|
| 9 |
+
path: `shard-${index + 1}.gguf`,
|
| 10 |
+
loadedBytes: state === 'queued' ? 0 : 100,
|
| 11 |
+
verifiedBytes: state === 'complete' || state === 'cached' ? 100 : 0,
|
| 12 |
+
totalBytes: 100,
|
| 13 |
+
state,
|
| 14 |
+
};
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
function progress(overrides: Partial<ModelLoadProgress> = {}): ModelLoadProgress {
|
| 18 |
+
return {
|
| 19 |
+
modelId: 'bonsai-27b',
|
| 20 |
+
modelLabel: 'Bonsai 27B',
|
| 21 |
+
stage: 'download',
|
| 22 |
+
stageProgress: 0.5,
|
| 23 |
+
downloadedBytes: 400,
|
| 24 |
+
totalBytes: 800,
|
| 25 |
+
residentBytes: 0,
|
| 26 |
+
nativeStage: null,
|
| 27 |
+
shards: [
|
| 28 |
+
shard(0, 'complete'),
|
| 29 |
+
shard(1, 'downloading'),
|
| 30 |
+
...Array.from({ length: 6 }, (_, index) => shard(index + 2, 'queued')),
|
| 31 |
+
],
|
| 32 |
+
...overrides,
|
| 33 |
+
};
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
describe('ComposerActivity', () => {
|
| 37 |
+
it('combines the load pipeline into one weighted overall progress value', () => {
|
| 38 |
+
expect(overallLoadProgress(progress())).toBeCloseTo(0.39625);
|
| 39 |
+
expect(overallLoadProgress(progress({ stage: 'cache', stageProgress: 0.2 }))).toBeCloseTo(0.39625);
|
| 40 |
+
expect(overallLoadProgress(progress({ stage: 'load', stageProgress: 0.5 }))).toBeCloseTo(0.95);
|
| 41 |
+
expect(overallLoadProgress(progress({ stage: 'ready' }))).toBe(1);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
it('annotates the active shard and total ready shard count', () => {
|
| 45 |
+
expect(describeLoadProgress(progress())).toEqual({
|
| 46 |
+
label: 'Downloading shard 2 of 8',
|
| 47 |
+
detail: '1 of 8 shards ready · 400 B / 800 B',
|
| 48 |
+
});
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
it('renders exactly one progressbar during load and disappears afterwards', () => {
|
| 52 |
+
const loading = renderToStaticMarkup(
|
| 53 |
+
<ComposerActivity modelId="bonsai-27b" streamState="loading" progress={progress()} />,
|
| 54 |
+
);
|
| 55 |
+
expect(loading.match(/role="progressbar"/g)).toHaveLength(1);
|
| 56 |
+
expect(loading).not.toContain('Model transfer');
|
| 57 |
+
expect(loading).not.toContain('Load pipeline');
|
| 58 |
+
|
| 59 |
+
const ready = renderToStaticMarkup(
|
| 60 |
+
<ComposerActivity modelId="bonsai-27b" streamState="complete" progress={progress({ stage: 'ready' })} />,
|
| 61 |
+
);
|
| 62 |
+
expect(ready).toBe('');
|
| 63 |
+
});
|
| 64 |
+
});
|
src/app/components/ComposerActivity.tsx
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
|
|
| 1 |
import type {
|
| 2 |
ModelId,
|
| 3 |
ModelLoadProgress,
|
| 4 |
StreamState,
|
| 5 |
-
TelemetrySnapshot,
|
| 6 |
} from '../../lib/contracts';
|
| 7 |
import { formatBytes, formatPercent } from '../../lib/format';
|
| 8 |
|
| 9 |
interface ComposerActivityProps {
|
| 10 |
modelId: ModelId;
|
| 11 |
-
isLoaded: boolean;
|
| 12 |
streamState: StreamState;
|
| 13 |
progress: ModelLoadProgress;
|
| 14 |
-
telemetry: TelemetrySnapshot;
|
| 15 |
}
|
| 16 |
|
| 17 |
-
const LOAD_STAGES = [
|
| 18 |
-
['manifest', 'Manifest'],
|
| 19 |
-
['cache', 'Cache'],
|
| 20 |
-
['download', 'Download'],
|
| 21 |
-
['verify', 'Verify'],
|
| 22 |
-
['load', 'Allocate'],
|
| 23 |
-
] as const;
|
| 24 |
-
|
| 25 |
-
type LoadStage = typeof LOAD_STAGES[number][0];
|
| 26 |
-
|
| 27 |
function ratio(value: number, total: number): number {
|
| 28 |
return total > 0 ? Math.min(1, Math.max(0, value / total)) : 0;
|
| 29 |
}
|
|
@@ -33,173 +21,111 @@ function humanizeStage(value: string | null): string {
|
|
| 33 |
return value.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
| 34 |
}
|
| 35 |
|
| 36 |
-
export function
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
const
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
const
|
| 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 |
-
: isLoaded ? 'Model ready in this tab' : 'Model is not loaded';
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
|
|
|
|
|
|
| 96 |
<div className="activity-status" aria-live="polite" aria-atomic="true">
|
| 97 |
<div>
|
| 98 |
<span className="activity-pulse" aria-hidden="true" />
|
| 99 |
-
<
|
|
|
|
|
|
|
|
|
|
| 100 |
</div>
|
| 101 |
-
{
|
| 102 |
-
<span>{formatPercent(pipelineProgress)} · {formatBytes(progress.downloadedBytes)} / {formatBytes(progress.totalBytes)}</span>
|
| 103 |
-
)}
|
| 104 |
-
{isInferring && telemetry.inferencePhase === 'prefill' && (
|
| 105 |
-
<span>{telemetry.promptProcessed.toLocaleString()} / {telemetry.promptTotal.toLocaleString()} tokens</span>
|
| 106 |
-
)}
|
| 107 |
-
{isInferring && telemetry.inferencePhase === 'decode' && (
|
| 108 |
-
<span>{telemetry.completionTokens.toLocaleString()} generated tokens</span>
|
| 109 |
-
)}
|
| 110 |
-
{!isLoading && !isInferring && isLoaded && progress.residentBytes > 0 && (
|
| 111 |
-
<span>{formatBytes(progress.residentBytes)} allocated</span>
|
| 112 |
-
)}
|
| 113 |
</div>
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
/>
|
| 124 |
-
<LiveMetric
|
| 125 |
-
label="Decode"
|
| 126 |
-
value={`${telemetry.decodeTokensPerSecond.toFixed(1)} tok/s`}
|
| 127 |
-
detail={`${telemetry.completionTokens.toLocaleString()} tok`}
|
| 128 |
-
progress={Math.min(1, telemetry.decodeTokensPerSecond / 20)}
|
| 129 |
-
active={telemetry.inferencePhase === 'decode'}
|
| 130 |
-
/>
|
| 131 |
-
</div>
|
| 132 |
-
) : (
|
| 133 |
-
<div className="load-live-grid">
|
| 134 |
-
<div className="activity-track-block">
|
| 135 |
-
<div><span>Model transfer</span><small>{formatPercent(downloadRatio)}</small></div>
|
| 136 |
-
<ProgressTrack label="Model transfer" value={downloadRatio} />
|
| 137 |
-
</div>
|
| 138 |
-
<div className="activity-track-block">
|
| 139 |
-
<div><span>Load pipeline</span><small>{formatPercent(pipelineProgress)}</small></div>
|
| 140 |
-
<ProgressTrack label="Overall model load pipeline" value={pipelineProgress} />
|
| 141 |
-
</div>
|
| 142 |
-
<div className="activity-memory">
|
| 143 |
-
<span>Resident</span>
|
| 144 |
-
<strong>{formatBytes(tracksModel ? progress.residentBytes : 0)}</strong>
|
| 145 |
-
</div>
|
| 146 |
-
</div>
|
| 147 |
-
)}
|
| 148 |
-
|
| 149 |
-
<div className={`shard-strip${displayShards.length === 0 ? ' is-empty' : ''}`} aria-label="Model shard progress">
|
| 150 |
-
{displayShards.length === 0
|
| 151 |
-
? <span className="shard-placeholder">Shard detail appears during cache inspection and transfer.</span>
|
| 152 |
-
: displayShards.map((shard) => {
|
| 153 |
-
const shardRatio = shard.state === 'complete' || shard.state === 'cached'
|
| 154 |
-
? 1
|
| 155 |
-
: shard.state === 'verifying'
|
| 156 |
-
? ratio(shard.verifiedBytes, shard.totalBytes)
|
| 157 |
-
: ratio(shard.loadedBytes, shard.totalBytes);
|
| 158 |
-
return (
|
| 159 |
-
<div className={`shard-cell state-${shard.state}`} key={`${shard.index}-${shard.path}`} title={shard.path}>
|
| 160 |
-
<span><b>S{shard.index + 1}</b><small>{formatPercent(shardRatio)}</small></span>
|
| 161 |
-
<ProgressTrack label={`Shard ${shard.index + 1} ${shard.state}`} value={shardRatio} />
|
| 162 |
-
</div>
|
| 163 |
-
);
|
| 164 |
-
})}
|
| 165 |
</div>
|
| 166 |
</section>
|
| 167 |
);
|
| 168 |
}
|
| 169 |
-
|
| 170 |
-
function displayShardsTotal(
|
| 171 |
-
shards: ModelLoadProgress['shards'],
|
| 172 |
-
field: 'loadedBytes' | 'verifiedBytes',
|
| 173 |
-
): number {
|
| 174 |
-
return shards.reduce((total, shard) => total + shard[field], 0);
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
function LiveMetric({
|
| 178 |
-
label,
|
| 179 |
-
value,
|
| 180 |
-
detail,
|
| 181 |
-
progress,
|
| 182 |
-
active,
|
| 183 |
-
}: {
|
| 184 |
-
label: string;
|
| 185 |
-
value: string;
|
| 186 |
-
detail: string;
|
| 187 |
-
progress: number;
|
| 188 |
-
active: boolean;
|
| 189 |
-
}) {
|
| 190 |
-
return (
|
| 191 |
-
<div className={`live-metric${active ? ' is-active' : ''}`}>
|
| 192 |
-
<div><span>{label}</span><strong>{value}</strong><small>{detail}</small></div>
|
| 193 |
-
<ProgressTrack label={`${label} live progress`} value={progress} />
|
| 194 |
-
</div>
|
| 195 |
-
);
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
function ProgressTrack({ label, value }: { label: string; value: number }) {
|
| 199 |
-
const percentage = Math.round(Math.min(1, Math.max(0, value)) * 100);
|
| 200 |
-
return (
|
| 201 |
-
<div className="activity-track" role="progressbar" aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={percentage}>
|
| 202 |
-
<span style={{ width: `${percentage}%` }} />
|
| 203 |
-
</div>
|
| 204 |
-
);
|
| 205 |
-
}
|
|
|
|
| 1 |
+
import { useRef } from 'react';
|
| 2 |
import type {
|
| 3 |
ModelId,
|
| 4 |
ModelLoadProgress,
|
| 5 |
StreamState,
|
|
|
|
| 6 |
} from '../../lib/contracts';
|
| 7 |
import { formatBytes, formatPercent } from '../../lib/format';
|
| 8 |
|
| 9 |
interface ComposerActivityProps {
|
| 10 |
modelId: ModelId;
|
|
|
|
| 11 |
streamState: StreamState;
|
| 12 |
progress: ModelLoadProgress;
|
|
|
|
| 13 |
}
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
function ratio(value: number, total: number): number {
|
| 16 |
return total > 0 ? Math.min(1, Math.max(0, value / total)) : 0;
|
| 17 |
}
|
|
|
|
| 21 |
return value.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
| 22 |
}
|
| 23 |
|
| 24 |
+
export function overallLoadProgress(progress: ModelLoadProgress): number {
|
| 25 |
+
const stageProgress = Math.min(1, Math.max(0, progress.stageProgress));
|
| 26 |
+
const loadedShardBytes = progress.shards.reduce((total, shard) => (
|
| 27 |
+
total + (shard.state === 'cached' || shard.state === 'complete'
|
| 28 |
+
? shard.totalBytes
|
| 29 |
+
: Math.min(shard.loadedBytes, shard.totalBytes))
|
| 30 |
+
), 0);
|
| 31 |
+
const verifiedBytes = progress.shards.reduce((total, shard) => (
|
| 32 |
+
total + (shard.state === 'cached' || shard.state === 'complete'
|
| 33 |
+
? shard.totalBytes
|
| 34 |
+
: Math.min(shard.verifiedBytes, shard.totalBytes))
|
| 35 |
+
), 0);
|
| 36 |
+
const downloadRatio = ratio(Math.max(progress.downloadedBytes, loadedShardBytes), progress.totalBytes);
|
| 37 |
+
const verifiedRatio = ratio(verifiedBytes, progress.totalBytes);
|
| 38 |
+
const transferProgress = Math.min(0.9, 0.08 + downloadRatio * 0.57 + verifiedRatio * 0.25);
|
| 39 |
+
|
| 40 |
+
switch (progress.stage) {
|
| 41 |
+
case 'manifest':
|
| 42 |
+
return stageProgress * 0.03;
|
| 43 |
+
case 'cache':
|
| 44 |
+
return Math.max(0.03 + stageProgress * 0.05, transferProgress);
|
| 45 |
+
case 'download':
|
| 46 |
+
case 'verify':
|
| 47 |
+
return transferProgress;
|
| 48 |
+
case 'load':
|
| 49 |
+
return 0.9 + stageProgress * 0.1;
|
| 50 |
+
case 'ready':
|
| 51 |
+
return 1;
|
| 52 |
+
default:
|
| 53 |
+
return 0;
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
|
| 57 |
+
export function describeLoadProgress(progress: ModelLoadProgress): { label: string; detail: string } {
|
| 58 |
+
const shardCount = progress.shards.length;
|
| 59 |
+
const readyShards = progress.shards.filter((shard) => shard.state === 'cached' || shard.state === 'complete').length;
|
| 60 |
+
const activeShard = progress.shards.find((shard) => shard.state === 'downloading' || shard.state === 'verifying');
|
| 61 |
+
const shardDetail = shardCount > 0 ? `${readyShards} of ${shardCount} shards ready` : 'Preparing shard map';
|
| 62 |
+
const byteDetail = progress.totalBytes > 0
|
| 63 |
+
? `${formatBytes(progress.downloadedBytes)} / ${formatBytes(progress.totalBytes)}`
|
| 64 |
+
: '';
|
|
|
|
| 65 |
|
| 66 |
+
switch (progress.stage) {
|
| 67 |
+
case 'manifest':
|
| 68 |
+
return { label: 'Reading model manifest', detail: progress.modelLabel || 'Pinned release metadata' };
|
| 69 |
+
case 'cache':
|
| 70 |
+
return { label: 'Checking local cache', detail: shardCount > 0 ? shardDetail : 'Looking for existing shards' };
|
| 71 |
+
case 'download':
|
| 72 |
+
return {
|
| 73 |
+
label: activeShard ? `Downloading shard ${activeShard.index + 1} of ${shardCount}` : 'Downloading model shards',
|
| 74 |
+
detail: [shardDetail, byteDetail].filter(Boolean).join(' · '),
|
| 75 |
+
};
|
| 76 |
+
case 'verify':
|
| 77 |
+
return {
|
| 78 |
+
label: activeShard ? `Verifying shard ${activeShard.index + 1} of ${shardCount}` : 'Verifying model shards',
|
| 79 |
+
detail: [shardDetail, byteDetail].filter(Boolean).join(' · '),
|
| 80 |
+
};
|
| 81 |
+
case 'load':
|
| 82 |
+
return {
|
| 83 |
+
label: progress.nativeStage ? humanizeStage(progress.nativeStage) : 'Allocating model',
|
| 84 |
+
detail: progress.residentBytes > 0 ? `${formatBytes(progress.residentBytes)} resident` : 'Building the local graph',
|
| 85 |
+
};
|
| 86 |
+
default:
|
| 87 |
+
return { label: 'Preparing model', detail: progress.modelLabel };
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export function ComposerActivity({ modelId, streamState, progress }: ComposerActivityProps) {
|
| 92 |
+
const displayedProgress = useRef<{ modelId: ModelId | null; value: number }>({ modelId: null, value: 0 });
|
| 93 |
+
const isLoading = streamState === 'loading' && progress.modelId === modelId;
|
| 94 |
+
if (!isLoading) {
|
| 95 |
+
displayedProgress.current = { modelId: null, value: 0 };
|
| 96 |
+
return null;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const measuredProgress = overallLoadProgress(progress);
|
| 100 |
+
const overallProgress = displayedProgress.current.modelId === modelId
|
| 101 |
+
? Math.max(displayedProgress.current.value, measuredProgress)
|
| 102 |
+
: measuredProgress;
|
| 103 |
+
displayedProgress.current = { modelId, value: overallProgress };
|
| 104 |
+
const status = describeLoadProgress(progress);
|
| 105 |
+
const percentage = Math.round(overallProgress * 100);
|
| 106 |
|
| 107 |
+
return (
|
| 108 |
+
<section className="composer-activity" aria-label="Model load progress">
|
| 109 |
<div className="activity-status" aria-live="polite" aria-atomic="true">
|
| 110 |
<div>
|
| 111 |
<span className="activity-pulse" aria-hidden="true" />
|
| 112 |
+
<span>
|
| 113 |
+
<strong>{status.label}</strong>
|
| 114 |
+
<small>{status.detail}</small>
|
| 115 |
+
</span>
|
| 116 |
</div>
|
| 117 |
+
<b>{formatPercent(overallProgress)}</b>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
</div>
|
| 119 |
+
<div
|
| 120 |
+
className="activity-track"
|
| 121 |
+
role="progressbar"
|
| 122 |
+
aria-label={`${status.label}: ${status.detail}`}
|
| 123 |
+
aria-valuemin={0}
|
| 124 |
+
aria-valuemax={100}
|
| 125 |
+
aria-valuenow={percentage}
|
| 126 |
+
>
|
| 127 |
+
<span style={{ width: `${percentage}%` }} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
</div>
|
| 129 |
</section>
|
| 130 |
);
|
| 131 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/components/ConversationList.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ChatSession } from '../../lib/contracts';
|
| 2 |
+
import { conversationPreview } from '../../lib/conversations';
|
| 3 |
+
|
| 4 |
+
interface ConversationListProps {
|
| 5 |
+
conversations: ChatSession[];
|
| 6 |
+
activeConversationId: string;
|
| 7 |
+
onNewConversation: () => void;
|
| 8 |
+
onOpenConversation: (conversationId: string) => void;
|
| 9 |
+
onOpenConversationSettings: (conversationId: string) => void;
|
| 10 |
+
onDeleteConversation: (conversationId: string) => void;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function ConversationList({
|
| 14 |
+
conversations,
|
| 15 |
+
activeConversationId,
|
| 16 |
+
onNewConversation,
|
| 17 |
+
onOpenConversation,
|
| 18 |
+
onOpenConversationSettings,
|
| 19 |
+
onDeleteConversation,
|
| 20 |
+
}: ConversationListProps) {
|
| 21 |
+
return (
|
| 22 |
+
<main className="chat-surface conversation-library">
|
| 23 |
+
<header className="chat-header conversation-list-header">
|
| 24 |
+
<div>
|
| 25 |
+
<h2>Chats</h2>
|
| 26 |
+
<p>Saved only in this browser</p>
|
| 27 |
+
</div>
|
| 28 |
+
<button className="new-chat-button" type="button" onClick={onNewConversation}>New chat</button>
|
| 29 |
+
</header>
|
| 30 |
+
<section className="conversation-list" aria-label="Saved chats">
|
| 31 |
+
{conversations.length === 0 ? (
|
| 32 |
+
<div className="conversation-list-empty">
|
| 33 |
+
<h3>No saved chats</h3>
|
| 34 |
+
<p>Your first prompt creates a local session automatically.</p>
|
| 35 |
+
<button type="button" onClick={onNewConversation}>Start a chat</button>
|
| 36 |
+
</div>
|
| 37 |
+
) : conversations.map((conversation) => (
|
| 38 |
+
<article className={`conversation-row${conversation.id === activeConversationId ? ' is-current' : ''}`} key={conversation.id}>
|
| 39 |
+
<button className="conversation-open" type="button" onClick={() => onOpenConversation(conversation.id)}>
|
| 40 |
+
<span className="conversation-title-line">
|
| 41 |
+
<strong>{conversation.title}</strong>
|
| 42 |
+
{conversation.id === activeConversationId && <small>Current</small>}
|
| 43 |
+
</span>
|
| 44 |
+
<span>{conversationPreview(conversation.messages)}</span>
|
| 45 |
+
<small>{conversation.modelId.replace('bonsai-', 'Bonsai ')} · {formatConversationTime(conversation.updatedAt)}</small>
|
| 46 |
+
</button>
|
| 47 |
+
<div className="conversation-actions">
|
| 48 |
+
<button type="button" onClick={() => onOpenConversationSettings(conversation.id)}>Settings</button>
|
| 49 |
+
<button
|
| 50 |
+
className="conversation-delete"
|
| 51 |
+
type="button"
|
| 52 |
+
aria-label={`Delete chat: ${conversation.title}`}
|
| 53 |
+
onClick={() => onDeleteConversation(conversation.id)}
|
| 54 |
+
>
|
| 55 |
+
Delete
|
| 56 |
+
</button>
|
| 57 |
+
</div>
|
| 58 |
+
</article>
|
| 59 |
+
))}
|
| 60 |
+
</section>
|
| 61 |
+
</main>
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function formatConversationTime(timestamp: number): string {
|
| 66 |
+
return new Intl.DateTimeFormat('en', {
|
| 67 |
+
month: 'short',
|
| 68 |
+
day: 'numeric',
|
| 69 |
+
hour: '2-digit',
|
| 70 |
+
minute: '2-digit',
|
| 71 |
+
}).format(new Date(timestamp));
|
| 72 |
+
}
|
src/app/components/Header.test.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from 'vitest';
|
| 2 |
+
import { isSafariUserAgent } from './Header';
|
| 3 |
+
|
| 4 |
+
describe('Safari performance notice routing', () => {
|
| 5 |
+
it('detects desktop Safari without matching Chromium user agents', () => {
|
| 6 |
+
expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/605.1.15 Version/26.5 Safari/605.1.15')).toBe(true);
|
| 7 |
+
expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36')).toBe(false);
|
| 8 |
+
expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/537.36 HeadlessChrome/150.0.0.0 Safari/537.36')).toBe(false);
|
| 9 |
+
expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/537.36 Edg/150.0.0.0 Safari/537.36')).toBe(false);
|
| 10 |
+
});
|
| 11 |
+
});
|
src/app/components/Header.tsx
CHANGED
|
@@ -1,33 +1,79 @@
|
|
|
|
|
|
|
|
| 1 |
interface HeaderProps {
|
| 2 |
onOpenSettings: () => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
disabled?: boolean;
|
| 4 |
}
|
| 5 |
|
| 6 |
-
export function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
return (
|
| 8 |
<header className="station-header">
|
| 9 |
-
<div className="brand-block" aria-label="Bonsai
|
| 10 |
-
<span className="brand-
|
| 11 |
-
<span />
|
| 12 |
-
</span>
|
| 13 |
<div>
|
| 14 |
-
<
|
| 15 |
-
<
|
| 16 |
</div>
|
| 17 |
</div>
|
| 18 |
|
| 19 |
-
<div className=
|
| 20 |
-
<span className="
|
| 21 |
<span>
|
| 22 |
-
<strong>Private
|
| 23 |
-
<
|
| 24 |
</span>
|
| 25 |
</div>
|
| 26 |
|
| 27 |
<div className="header-actions">
|
| 28 |
-
<div className="
|
| 29 |
-
<
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</div>
|
| 32 |
<button className="icon-button" type="button" disabled={disabled} onClick={onOpenSettings} aria-label="Open runtime settings">
|
| 33 |
<SettingsIcon />
|
|
@@ -37,6 +83,18 @@ export function Header({ onOpenSettings, disabled = false }: HeaderProps) {
|
|
| 37 |
);
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
function SettingsIcon() {
|
| 41 |
return (
|
| 42 |
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
| 1 |
+
export type ChatWidth = 'centered' | 'wide';
|
| 2 |
+
|
| 3 |
interface 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 |
|
| 14 |
+
export function isSafariUserAgent(userAgent: string): boolean {
|
| 15 |
+
return /\bSafari\//.test(userAgent) && !/(?:HeadlessChrome|\bChrome|\bChromium|\bCriOS|\bEdg|\bOPR)\//.test(userAgent);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export 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 |
+
<button
|
| 51 |
+
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 |
+
<button
|
| 60 |
+
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 |
+
<button
|
| 69 |
+
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 />
|
|
|
|
| 83 |
);
|
| 84 |
}
|
| 85 |
|
| 86 |
+
function 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 |
+
|
| 90 |
+
function 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 |
+
|
| 94 |
+
function 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 |
+
|
| 98 |
function SettingsIcon() {
|
| 99 |
return (
|
| 100 |
<svg viewBox="0 0 24 24" aria-hidden="true">
|
src/app/components/InspectorPanel.tsx
CHANGED
|
@@ -1,59 +1,75 @@
|
|
| 1 |
import type { ArtifactDocument } from '../../lib/agent-tools';
|
| 2 |
import type { TelemetrySnapshot, ToolEvent } from '../../lib/contracts';
|
| 3 |
-
import { formatBytes } from '../../lib/format';
|
| 4 |
import { ArtifactPane } from './ArtifactPane';
|
| 5 |
|
| 6 |
interface InspectorPanelProps {
|
|
|
|
| 7 |
telemetry: TelemetrySnapshot;
|
| 8 |
toolEvents: ToolEvent[];
|
| 9 |
artifact: ArtifactDocument | null;
|
| 10 |
onCloseArtifact: () => void;
|
| 11 |
}
|
| 12 |
|
| 13 |
-
export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifact }: InspectorPanelProps) {
|
| 14 |
const contextRatio = telemetry.contextLimit > 0 ? telemetry.contextUsed / telemetry.contextLimit : 0;
|
|
|
|
| 15 |
const activeRate = telemetry.inferencePhase === 'prefill'
|
| 16 |
? telemetry.prefillTokensPerSecond
|
| 17 |
: telemetry.decodeTokensPerSecond || telemetry.tokensPerSecond;
|
| 18 |
const speedRatio = Math.min(1, Math.max(0, activeRate / 50));
|
| 19 |
const gaugeLabel = telemetry.inferencePhase === 'prefill'
|
| 20 |
-
? '
|
| 21 |
: telemetry.inferencePhase === 'decode'
|
| 22 |
-
? '
|
| 23 |
-
: '
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
return (
|
| 26 |
-
<aside className="inspector" aria-label="Runtime inspector">
|
| 27 |
<section className="inspector-section telemetry-section" aria-labelledby="telemetry-title">
|
| 28 |
<div className="section-heading">
|
| 29 |
-
<
|
| 30 |
-
<
|
| 31 |
-
<p className="eyebrow">Live cambium</p>
|
| 32 |
-
<h2 id="telemetry-title">Telemetry</h2>
|
| 33 |
-
</div>
|
| 34 |
-
<span className={`sampling-label phase-${telemetry.inferencePhase}`}><i /> {
|
| 35 |
-
telemetry.inferencePhase === 'idle'
|
| 36 |
-
? 'ready'
|
| 37 |
-
: telemetry.inferencePhase === 'complete' ? 'average' : 'live'
|
| 38 |
-
}</span>
|
| 39 |
</div>
|
| 40 |
|
| 41 |
<div className="speed-gauge">
|
| 42 |
<div className="gauge-ring" style={{ '--gauge': `${Math.round(speedRatio * 100)}%` } as React.CSSProperties}>
|
| 43 |
<div>
|
| 44 |
<strong>{activeRate.toFixed(1)}</strong>
|
| 45 |
-
<small>{gaugeLabel}</small>
|
| 46 |
</div>
|
| 47 |
</div>
|
| 48 |
<dl>
|
| 49 |
<div><dt>Backend</dt><dd>{telemetry.backend}</dd></div>
|
| 50 |
-
<div><dt>Prefill</dt><dd>{telemetry.prefillTokensPerSecond.toFixed(1)} tok/s</dd></div>
|
| 51 |
-
<div><dt>Decode</dt><dd>{telemetry.decodeTokensPerSecond.toFixed(1)} tok/s</dd></div>
|
| 52 |
<div><dt>First token</dt><dd>{telemetry.timeToFirstTokenMs} ms</dd></div>
|
| 53 |
<div><dt>Graph splits</dt><dd>{telemetry.graphSplits ?? '—'}</dd></div>
|
| 54 |
</dl>
|
| 55 |
</div>
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
<div className="device-card">
|
| 58 |
<span className="device-icon" aria-hidden="true"><i /><i /><i /></span>
|
| 59 |
<div>
|
|
@@ -63,7 +79,7 @@ export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifac
|
|
| 63 |
<span>{telemetry.gpuLayers}<small>layers</small></span>
|
| 64 |
</div>
|
| 65 |
|
| 66 |
-
<Meter label="Context
|
| 67 |
<div className="memory-grid">
|
| 68 |
<div><small>Model allocation</small><strong>{formatBytes(telemetry.modelMemoryBytes)}</strong></div>
|
| 69 |
<div><small>KV allocation</small><strong>{formatBytes(telemetry.kvMemoryBytes)}</strong></div>
|
|
@@ -72,11 +88,7 @@ export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifac
|
|
| 72 |
|
| 73 |
<section className="inspector-section tool-log-section" aria-labelledby="tool-log-title">
|
| 74 |
<div className="section-heading compact">
|
| 75 |
-
<
|
| 76 |
-
<div>
|
| 77 |
-
<p className="eyebrow">Instrument trace</p>
|
| 78 |
-
<h2 id="tool-log-title">Tool log</h2>
|
| 79 |
-
</div>
|
| 80 |
</div>
|
| 81 |
<ol className="event-log">
|
| 82 |
{toolEvents.length === 0 && <li className="event-empty">No tool calls in this session.</li>}
|
|
@@ -95,6 +107,33 @@ export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifac
|
|
| 95 |
);
|
| 96 |
}
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
function Meter({ label, value, detail }: { label: string; value: number; detail: string }) {
|
| 99 |
const percentage = Math.round(Math.min(1, Math.max(0, value)) * 100);
|
| 100 |
return (
|
|
|
|
| 1 |
import type { ArtifactDocument } from '../../lib/agent-tools';
|
| 2 |
import type { TelemetrySnapshot, ToolEvent } from '../../lib/contracts';
|
| 3 |
+
import { formatBytes, formatPercent } from '../../lib/format';
|
| 4 |
import { ArtifactPane } from './ArtifactPane';
|
| 5 |
|
| 6 |
interface InspectorPanelProps {
|
| 7 |
+
hidden?: boolean;
|
| 8 |
telemetry: TelemetrySnapshot;
|
| 9 |
toolEvents: ToolEvent[];
|
| 10 |
artifact: ArtifactDocument | null;
|
| 11 |
onCloseArtifact: () => void;
|
| 12 |
}
|
| 13 |
|
| 14 |
+
export function InspectorPanel({ hidden = false, telemetry, toolEvents, artifact, onCloseArtifact }: InspectorPanelProps) {
|
| 15 |
const contextRatio = telemetry.contextLimit > 0 ? telemetry.contextUsed / telemetry.contextLimit : 0;
|
| 16 |
+
const prefillRatio = telemetry.promptTotal > 0 ? telemetry.promptProcessed / telemetry.promptTotal : 0;
|
| 17 |
const activeRate = telemetry.inferencePhase === 'prefill'
|
| 18 |
? telemetry.prefillTokensPerSecond
|
| 19 |
: telemetry.decodeTokensPerSecond || telemetry.tokensPerSecond;
|
| 20 |
const speedRatio = Math.min(1, Math.max(0, activeRate / 50));
|
| 21 |
const gaugeLabel = telemetry.inferencePhase === 'prefill'
|
| 22 |
+
? 'Prefill'
|
| 23 |
: telemetry.inferencePhase === 'decode'
|
| 24 |
+
? 'Decode'
|
| 25 |
+
: 'Average';
|
| 26 |
+
const samplingLabel = telemetry.inferencePhase === 'idle'
|
| 27 |
+
? 'Ready'
|
| 28 |
+
: telemetry.inferencePhase === 'complete' ? 'Average' : 'Live';
|
| 29 |
|
| 30 |
return (
|
| 31 |
+
<aside className="inspector" aria-label="Runtime inspector" hidden={hidden}>
|
| 32 |
<section className="inspector-section telemetry-section" aria-labelledby="telemetry-title">
|
| 33 |
<div className="section-heading">
|
| 34 |
+
<h2 id="telemetry-title">Telemetry</h2>
|
| 35 |
+
<span className={`sampling-label phase-${telemetry.inferencePhase}`}>{samplingLabel}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
</div>
|
| 37 |
|
| 38 |
<div className="speed-gauge">
|
| 39 |
<div className="gauge-ring" style={{ '--gauge': `${Math.round(speedRatio * 100)}%` } as React.CSSProperties}>
|
| 40 |
<div>
|
| 41 |
<strong>{activeRate.toFixed(1)}</strong>
|
| 42 |
+
<small><span>{gaugeLabel}</span><span>tok/s</span></small>
|
| 43 |
</div>
|
| 44 |
</div>
|
| 45 |
<dl>
|
| 46 |
<div><dt>Backend</dt><dd>{telemetry.backend}</dd></div>
|
|
|
|
|
|
|
| 47 |
<div><dt>First token</dt><dd>{telemetry.timeToFirstTokenMs} ms</dd></div>
|
| 48 |
<div><dt>Graph splits</dt><dd>{telemetry.graphSplits ?? '—'}</dd></div>
|
| 49 |
</dl>
|
| 50 |
</div>
|
| 51 |
|
| 52 |
+
<div className="telemetry-live-grid" aria-label="Live inference rates">
|
| 53 |
+
<TelemetryMetric
|
| 54 |
+
label="Prefill"
|
| 55 |
+
value={`${telemetry.prefillTokensPerSecond.toFixed(1)} tok/s`}
|
| 56 |
+
detail={telemetry.promptTotal > 0
|
| 57 |
+
? `${telemetry.promptProcessed.toLocaleString()} / ${telemetry.promptTotal.toLocaleString()} tokens`
|
| 58 |
+
: 'No prompt yet'}
|
| 59 |
+
ratio={prefillRatio}
|
| 60 |
+
active={telemetry.inferencePhase === 'prefill'}
|
| 61 |
+
progressLabel={telemetry.promptTotal > 0 ? formatPercent(prefillRatio) : 'Waiting'}
|
| 62 |
+
/>
|
| 63 |
+
<TelemetryMetric
|
| 64 |
+
label="Decode"
|
| 65 |
+
value={`${telemetry.decodeTokensPerSecond.toFixed(1)} tok/s`}
|
| 66 |
+
detail={`${telemetry.completionTokens.toLocaleString()} generated tokens`}
|
| 67 |
+
ratio={telemetry.decodeTokensPerSecond / 50}
|
| 68 |
+
active={telemetry.inferencePhase === 'decode'}
|
| 69 |
+
progressLabel="Rate / 50 tok/s"
|
| 70 |
+
/>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
<div className="device-card">
|
| 74 |
<span className="device-icon" aria-hidden="true"><i /><i /><i /></span>
|
| 75 |
<div>
|
|
|
|
| 79 |
<span>{telemetry.gpuLayers}<small>layers</small></span>
|
| 80 |
</div>
|
| 81 |
|
| 82 |
+
<Meter label="Context" value={contextRatio} detail={`${telemetry.contextUsed.toLocaleString()} / ${telemetry.contextLimit.toLocaleString()}`} />
|
| 83 |
<div className="memory-grid">
|
| 84 |
<div><small>Model allocation</small><strong>{formatBytes(telemetry.modelMemoryBytes)}</strong></div>
|
| 85 |
<div><small>KV allocation</small><strong>{formatBytes(telemetry.kvMemoryBytes)}</strong></div>
|
|
|
|
| 88 |
|
| 89 |
<section className="inspector-section tool-log-section" aria-labelledby="tool-log-title">
|
| 90 |
<div className="section-heading compact">
|
| 91 |
+
<h2 id="tool-log-title">Tool log</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</div>
|
| 93 |
<ol className="event-log">
|
| 94 |
{toolEvents.length === 0 && <li className="event-empty">No tool calls in this session.</li>}
|
|
|
|
| 107 |
);
|
| 108 |
}
|
| 109 |
|
| 110 |
+
function TelemetryMetric({
|
| 111 |
+
label,
|
| 112 |
+
value,
|
| 113 |
+
detail,
|
| 114 |
+
ratio,
|
| 115 |
+
active,
|
| 116 |
+
progressLabel,
|
| 117 |
+
}: {
|
| 118 |
+
label: string;
|
| 119 |
+
value: string;
|
| 120 |
+
detail: string;
|
| 121 |
+
ratio: number;
|
| 122 |
+
active: boolean;
|
| 123 |
+
progressLabel: string;
|
| 124 |
+
}) {
|
| 125 |
+
const percentage = Math.round(Math.min(1, Math.max(0, ratio)) * 100);
|
| 126 |
+
return (
|
| 127 |
+
<div className={`telemetry-metric${active ? ' is-active' : ''}`}>
|
| 128 |
+
<div><span>{label}</span><strong>{value}</strong></div>
|
| 129 |
+
<small>{detail}</small>
|
| 130 |
+
<div className="telemetry-meter" role="meter" aria-label={`${label}: ${progressLabel}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={percentage}>
|
| 131 |
+
<span style={{ width: `${percentage}%` }} />
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
function Meter({ label, value, detail }: { label: string; value: number; detail: string }) {
|
| 138 |
const percentage = Math.round(Math.min(1, Math.max(0, value)) * 100);
|
| 139 |
return (
|
src/app/components/ModelRail.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import type { ModelId, ModelOption } from '../../lib/contracts';
|
| 2 |
|
| 3 |
interface ModelRailProps {
|
|
|
|
| 4 |
models: ModelOption[];
|
| 5 |
activeModelId: ModelId;
|
| 6 |
busy: boolean;
|
|
@@ -16,23 +17,23 @@ const availabilityLabels: Record<ModelOption['availability'], string> = {
|
|
| 16 |
};
|
| 17 |
|
| 18 |
export function ModelRail({
|
|
|
|
| 19 |
models,
|
| 20 |
activeModelId,
|
| 21 |
busy,
|
| 22 |
onModelSelect,
|
| 23 |
}: ModelRailProps) {
|
| 24 |
return (
|
| 25 |
-
<aside className="model-rail" aria-labelledby="model-rail-title">
|
| 26 |
<div className="rail-heading">
|
| 27 |
-
<span>01</span>
|
| 28 |
<div>
|
| 29 |
-
<
|
| 30 |
-
<
|
| 31 |
</div>
|
| 32 |
</div>
|
| 33 |
|
| 34 |
<div className="model-list" role="list" aria-label="Available Bonsai models">
|
| 35 |
-
{models.map((model
|
| 36 |
const active = model.id === activeModelId;
|
| 37 |
return (
|
| 38 |
<article className={`model-card${active ? ' is-active' : ''}`} key={model.id} role="listitem">
|
|
@@ -43,7 +44,6 @@ export function ModelRail({
|
|
| 43 |
onClick={() => onModelSelect(model.id)}
|
| 44 |
aria-pressed={active}
|
| 45 |
>
|
| 46 |
-
<span className="model-index">{String(index + 1).padStart(2, '0')}</span>
|
| 47 |
<span className="model-copy">
|
| 48 |
<span className="model-title-line">
|
| 49 |
<strong>{model.parameters}</strong>
|
|
@@ -64,7 +64,7 @@ export function ModelRail({
|
|
| 64 |
|
| 65 |
<footer className="rail-footnote">
|
| 66 |
<span>GGUF · Q1_0</span>
|
| 67 |
-
<span>
|
| 68 |
</footer>
|
| 69 |
</aside>
|
| 70 |
);
|
|
|
|
| 1 |
import type { ModelId, ModelOption } from '../../lib/contracts';
|
| 2 |
|
| 3 |
interface ModelRailProps {
|
| 4 |
+
hidden?: boolean;
|
| 5 |
models: ModelOption[];
|
| 6 |
activeModelId: ModelId;
|
| 7 |
busy: boolean;
|
|
|
|
| 17 |
};
|
| 18 |
|
| 19 |
export function ModelRail({
|
| 20 |
+
hidden = false,
|
| 21 |
models,
|
| 22 |
activeModelId,
|
| 23 |
busy,
|
| 24 |
onModelSelect,
|
| 25 |
}: ModelRailProps) {
|
| 26 |
return (
|
| 27 |
+
<aside className="model-rail" aria-labelledby="model-rail-title" hidden={hidden}>
|
| 28 |
<div className="rail-heading">
|
|
|
|
| 29 |
<div>
|
| 30 |
+
<h2 id="model-rail-title">Models</h2>
|
| 31 |
+
<p>Choose what runs in this browser.</p>
|
| 32 |
</div>
|
| 33 |
</div>
|
| 34 |
|
| 35 |
<div className="model-list" role="list" aria-label="Available Bonsai models">
|
| 36 |
+
{models.map((model) => {
|
| 37 |
const active = model.id === activeModelId;
|
| 38 |
return (
|
| 39 |
<article className={`model-card${active ? ' is-active' : ''}`} key={model.id} role="listitem">
|
|
|
|
| 44 |
onClick={() => onModelSelect(model.id)}
|
| 45 |
aria-pressed={active}
|
| 46 |
>
|
|
|
|
| 47 |
<span className="model-copy">
|
| 48 |
<span className="model-title-line">
|
| 49 |
<strong>{model.parameters}</strong>
|
|
|
|
| 64 |
|
| 65 |
<footer className="rail-footnote">
|
| 66 |
<span>GGUF · Q1_0</span>
|
| 67 |
+
<span>Commit pinned</span>
|
| 68 |
</footer>
|
| 69 |
</aside>
|
| 70 |
);
|
src/app/components/SettingsSheet.tsx
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
import { useEffect, useRef } from 'react';
|
| 2 |
import type { ModelOption, RuntimeSettings, TelemetrySnapshot } from '../../lib/contracts';
|
|
|
|
| 3 |
|
| 4 |
interface SettingsSheetProps {
|
| 5 |
open: boolean;
|
|
|
|
|
|
|
| 6 |
activeModel: ModelOption;
|
| 7 |
telemetry: TelemetrySnapshot;
|
| 8 |
settings: RuntimeSettings;
|
|
@@ -15,6 +18,8 @@ interface SettingsSheetProps {
|
|
| 15 |
|
| 16 |
export function SettingsSheet({
|
| 17 |
open,
|
|
|
|
|
|
|
| 18 |
activeModel,
|
| 19 |
telemetry,
|
| 20 |
settings,
|
|
@@ -52,15 +57,19 @@ export function SettingsSheet({
|
|
| 52 |
}}>
|
| 53 |
<section className="settings-sheet" role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
| 54 |
<header>
|
| 55 |
-
<
|
| 56 |
-
<p className="eyebrow">Instrument calibration</p>
|
| 57 |
-
<h2 id="settings-title">Runtime settings</h2>
|
| 58 |
-
</div>
|
| 59 |
<button ref={closeButtonRef} className="sheet-close" type="button" onClick={onClose} aria-label="Close settings">×</button>
|
| 60 |
</header>
|
| 61 |
|
| 62 |
<label className="setting-group setting-select">
|
| 63 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<div><small>Compute backend</small><strong>Auto prefers WebGPU</strong></div>
|
| 65 |
<select value={settings.backend} onChange={(event) => update('backend', event.target.value as RuntimeSettings['backend'])}>
|
| 66 |
<option value="auto">Auto</option>
|
|
@@ -69,7 +78,6 @@ export function SettingsSheet({
|
|
| 69 |
</select>
|
| 70 |
</label>
|
| 71 |
<div className="setting-group">
|
| 72 |
-
<span className="setting-number">02</span>
|
| 73 |
<div><small>Selected specimen</small><strong>{activeModel.label}</strong></div>
|
| 74 |
<span className="setting-locked">{activeModel.footprint}</span>
|
| 75 |
</div>
|
|
@@ -100,7 +108,7 @@ export function SettingsSheet({
|
|
| 100 |
</label>
|
| 101 |
|
| 102 |
<label className="system-prompt-setting">
|
| 103 |
-
<span><small>System
|
| 104 |
<textarea rows={5} value={settings.systemPrompt} onChange={(event) => update('systemPrompt', event.target.value.slice(0, 8192))} />
|
| 105 |
</label>
|
| 106 |
|
|
|
|
| 1 |
import { useEffect, useRef } from 'react';
|
| 2 |
import type { ModelOption, RuntimeSettings, TelemetrySnapshot } from '../../lib/contracts';
|
| 3 |
+
import type { ThemeMode } from '../theme';
|
| 4 |
|
| 5 |
interface SettingsSheetProps {
|
| 6 |
open: boolean;
|
| 7 |
+
themeMode: ThemeMode;
|
| 8 |
+
onThemeModeChange: (theme: ThemeMode) => void;
|
| 9 |
activeModel: ModelOption;
|
| 10 |
telemetry: TelemetrySnapshot;
|
| 11 |
settings: RuntimeSettings;
|
|
|
|
| 18 |
|
| 19 |
export function SettingsSheet({
|
| 20 |
open,
|
| 21 |
+
themeMode,
|
| 22 |
+
onThemeModeChange,
|
| 23 |
activeModel,
|
| 24 |
telemetry,
|
| 25 |
settings,
|
|
|
|
| 57 |
}}>
|
| 58 |
<section className="settings-sheet" role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
| 59 |
<header>
|
| 60 |
+
<h2 id="settings-title">Chat settings</h2>
|
|
|
|
|
|
|
|
|
|
| 61 |
<button ref={closeButtonRef} className="sheet-close" type="button" onClick={onClose} aria-label="Close settings">×</button>
|
| 62 |
</header>
|
| 63 |
|
| 64 |
<label className="setting-group setting-select">
|
| 65 |
+
<div><small>Appearance</small><strong>Follow the system or choose a theme</strong></div>
|
| 66 |
+
<select aria-label="Color theme" value={themeMode} onChange={(event) => onThemeModeChange(event.target.value as ThemeMode)}>
|
| 67 |
+
<option value="system">System</option>
|
| 68 |
+
<option value="light">Light</option>
|
| 69 |
+
<option value="dark">Dark</option>
|
| 70 |
+
</select>
|
| 71 |
+
</label>
|
| 72 |
+
<label className="setting-group setting-select">
|
| 73 |
<div><small>Compute backend</small><strong>Auto prefers WebGPU</strong></div>
|
| 74 |
<select value={settings.backend} onChange={(event) => update('backend', event.target.value as RuntimeSettings['backend'])}>
|
| 75 |
<option value="auto">Auto</option>
|
|
|
|
| 78 |
</select>
|
| 79 |
</label>
|
| 80 |
<div className="setting-group">
|
|
|
|
| 81 |
<div><small>Selected specimen</small><strong>{activeModel.label}</strong></div>
|
| 82 |
<span className="setting-locked">{activeModel.footprint}</span>
|
| 83 |
</div>
|
|
|
|
| 108 |
</label>
|
| 109 |
|
| 110 |
<label className="system-prompt-setting">
|
| 111 |
+
<span><small>System instructions</small><strong>Saved with this chat</strong></span>
|
| 112 |
<textarea rows={5} value={settings.systemPrompt} onChange={(event) => update('systemPrompt', event.target.value.slice(0, 8192))} />
|
| 113 |
</label>
|
| 114 |
|
src/app/styles.css
CHANGED
|
@@ -1,24 +1,24 @@
|
|
| 1 |
:root {
|
| 2 |
color-scheme: dark;
|
| 3 |
-
--ink: #
|
| 4 |
-
--ink-raised: #
|
| 5 |
-
--ink-soft: #
|
| 6 |
-
--paper: #
|
| 7 |
-
--paper-deep: #
|
| 8 |
-
--paper-ghost: rgba(
|
| 9 |
-
--phosphor: #
|
| 10 |
-
--phosphor-dim: rgba(
|
| 11 |
-
--moss: #
|
| 12 |
-
--rust: #
|
| 13 |
-
--line-light: rgba(
|
| 14 |
-
--line-dark: rgba(
|
| 15 |
-
--display: "
|
| 16 |
-
--body: "Avenir Next
|
| 17 |
--mono: "Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;
|
| 18 |
-
--type-utility: clamp(0.
|
| 19 |
-
--type-caption: clamp(0.
|
| 20 |
-
--type-body: clamp(
|
| 21 |
-
--type-heading: clamp(1.
|
| 22 |
font-family: var(--body);
|
| 23 |
font-synthesis: none;
|
| 24 |
text-rendering: optimizeLegibility;
|
|
@@ -36,10 +36,7 @@ body {
|
|
| 36 |
margin: 0;
|
| 37 |
min-width: 320px;
|
| 38 |
min-height: 100vh;
|
| 39 |
-
background:
|
| 40 |
-
linear-gradient(90deg, rgba(17, 21, 18, 0.06) 1px, transparent 1px) 0 0 / 22px 22px,
|
| 41 |
-
linear-gradient(rgba(17, 21, 18, 0.045) 1px, transparent 1px) 0 0 / 22px 22px,
|
| 42 |
-
var(--paper);
|
| 43 |
color: var(--paper);
|
| 44 |
font-size: var(--type-body);
|
| 45 |
}
|
|
@@ -72,62 +69,77 @@ button:disabled {
|
|
| 72 |
}
|
| 73 |
|
| 74 |
.station-frame {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
min-height: 100dvh;
|
|
|
|
| 76 |
isolation: isolate;
|
| 77 |
animation: station-in 580ms cubic-bezier(.16, 1, .3, 1) both;
|
| 78 |
}
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
.station-frame::before {
|
| 81 |
-
|
| 82 |
-
z-index: 20;
|
| 83 |
-
inset: 0;
|
| 84 |
-
pointer-events: none;
|
| 85 |
-
content: "";
|
| 86 |
-
opacity: 0.14;
|
| 87 |
-
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.3'/%3E%3C/svg%3E");
|
| 88 |
-
mix-blend-mode: soft-light;
|
| 89 |
}
|
| 90 |
|
| 91 |
.station-header {
|
| 92 |
-
min-height:
|
| 93 |
display: grid;
|
| 94 |
-
grid-template-columns: minmax(
|
| 95 |
align-items: stretch;
|
| 96 |
background: var(--ink);
|
| 97 |
-
border-bottom: 1px solid var(--
|
| 98 |
-
box-shadow: 0 12px 38px rgba(17, 21, 18, 0.22);
|
| 99 |
position: relative;
|
| 100 |
z-index: 10;
|
| 101 |
}
|
| 102 |
|
| 103 |
.brand-block {
|
| 104 |
display: flex;
|
| 105 |
-
gap:
|
| 106 |
align-items: center;
|
| 107 |
-
padding:
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
.brand-block h1 {
|
| 111 |
margin: 0;
|
| 112 |
-
font:
|
| 113 |
-
letter-spacing: -0.
|
| 114 |
}
|
| 115 |
|
| 116 |
-
.brand-block
|
| 117 |
-
|
| 118 |
-
|
| 119 |
}
|
| 120 |
|
| 121 |
-
.brand-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
display: grid;
|
| 126 |
place-items: center;
|
| 127 |
-
|
| 128 |
-
border
|
| 129 |
-
|
| 130 |
-
|
| 131 |
}
|
| 132 |
|
| 133 |
.brand-mark::before,
|
|
@@ -173,49 +185,92 @@ button:disabled {
|
|
| 173 |
color: rgba(17, 21, 18, 0.52);
|
| 174 |
}
|
| 175 |
|
| 176 |
-
.
|
| 177 |
display: flex;
|
| 178 |
align-items: center;
|
| 179 |
-
gap:
|
| 180 |
-
min-width:
|
| 181 |
-
padding: 0
|
| 182 |
-
border-inline: 1px solid var(--line-light);
|
| 183 |
-
background: linear-gradient(115deg, rgba(185, 255, 69, 0.08), transparent 66%);
|
| 184 |
}
|
| 185 |
|
| 186 |
-
.
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
height: 7px;
|
| 192 |
-
border-radius: 50%;
|
| 193 |
background: var(--phosphor);
|
| 194 |
-
box-shadow: 0 0 0 5px var(--phosphor-dim), 0 0 17px rgba(185, 255, 69, 0.62);
|
| 195 |
}
|
| 196 |
|
| 197 |
-
.
|
| 198 |
display: grid;
|
| 199 |
-
gap:
|
| 200 |
}
|
| 201 |
|
| 202 |
-
.
|
| 203 |
-
color: var(--
|
| 204 |
-
font: 600 var(--type-caption)/1 var(--
|
| 205 |
-
letter-spacing: 0.13em;
|
| 206 |
-
text-transform: uppercase;
|
| 207 |
}
|
| 208 |
|
| 209 |
-
.
|
| 210 |
color: var(--paper-ghost);
|
| 211 |
font: var(--type-caption)/1.25 var(--body);
|
| 212 |
}
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
.header-actions {
|
| 215 |
justify-self: end;
|
| 216 |
display: flex;
|
| 217 |
align-items: center;
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
.station-clock {
|
|
@@ -238,21 +293,11 @@ button:disabled {
|
|
| 238 |
}
|
| 239 |
|
| 240 |
.icon-button {
|
| 241 |
-
|
| 242 |
-
height: 44px;
|
| 243 |
-
display: grid;
|
| 244 |
-
place-items: center;
|
| 245 |
-
border: 1px solid rgba(232, 223, 202, 0.24);
|
| 246 |
-
border-radius: 50%;
|
| 247 |
-
background: transparent;
|
| 248 |
-
cursor: pointer;
|
| 249 |
-
transition: color 160ms ease, border-color 160ms ease, transform 160ms ease;
|
| 250 |
}
|
| 251 |
|
| 252 |
.icon-button:hover {
|
| 253 |
-
color: var(--
|
| 254 |
-
border-color: var(--phosphor);
|
| 255 |
-
transform: rotate(-7deg);
|
| 256 |
}
|
| 257 |
|
| 258 |
.icon-button svg {
|
|
@@ -263,10 +308,37 @@ button:disabled {
|
|
| 263 |
}
|
| 264 |
|
| 265 |
.workspace-grid {
|
| 266 |
-
min-height: calc(100dvh -
|
| 267 |
display: grid;
|
| 268 |
-
grid-template-columns:
|
| 269 |
grid-template-areas: "models chat inspector";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
}
|
| 271 |
|
| 272 |
.model-rail,
|
|
@@ -282,22 +354,11 @@ button:disabled {
|
|
| 282 |
display: flex;
|
| 283 |
flex-direction: column;
|
| 284 |
border-right: 1px solid var(--line-light);
|
| 285 |
-
padding:
|
| 286 |
-
animation: rise-in 520ms 100ms cubic-bezier(.16, 1, .3, 1) both;
|
| 287 |
}
|
| 288 |
|
| 289 |
.model-rail::before {
|
| 290 |
-
|
| 291 |
-
z-index: 0;
|
| 292 |
-
left: -103px;
|
| 293 |
-
bottom: 44px;
|
| 294 |
-
width: 265px;
|
| 295 |
-
height: 265px;
|
| 296 |
-
border-radius: 43% 57% 52% 48%;
|
| 297 |
-
content: "";
|
| 298 |
-
opacity: 0.18;
|
| 299 |
-
background: repeating-radial-gradient(ellipse at 51% 47%, transparent 0 7px, var(--moss) 7.5px 8.5px);
|
| 300 |
-
transform: rotate(18deg);
|
| 301 |
}
|
| 302 |
|
| 303 |
.rail-heading,
|
|
@@ -305,8 +366,8 @@ button:disabled {
|
|
| 305 |
position: relative;
|
| 306 |
z-index: 1;
|
| 307 |
display: flex;
|
| 308 |
-
align-items:
|
| 309 |
-
gap:
|
| 310 |
}
|
| 311 |
|
| 312 |
.rail-heading > span,
|
|
@@ -320,8 +381,15 @@ button:disabled {
|
|
| 320 |
.rail-heading h2,
|
| 321 |
.section-heading h2 {
|
| 322 |
margin: 0;
|
| 323 |
-
font:
|
| 324 |
-
letter-spacing: -0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
}
|
| 326 |
|
| 327 |
.model-list {
|
|
@@ -333,22 +401,22 @@ button:disabled {
|
|
| 333 |
}
|
| 334 |
|
| 335 |
.model-card {
|
| 336 |
-
border: 1px solid
|
|
|
|
| 337 |
transition: border-color 180ms ease, background 180ms ease;
|
| 338 |
}
|
| 339 |
|
| 340 |
.model-card.is-active {
|
| 341 |
-
border-color:
|
| 342 |
background: var(--phosphor-dim);
|
| 343 |
-
box-shadow: inset 3px 0 0 var(--phosphor);
|
| 344 |
}
|
| 345 |
|
| 346 |
.model-select {
|
| 347 |
width: 100%;
|
| 348 |
display: grid;
|
| 349 |
-
grid-template-columns:
|
| 350 |
gap: 8px;
|
| 351 |
-
min-height:
|
| 352 |
padding: 13px 9px 12px;
|
| 353 |
text-align: left;
|
| 354 |
color: var(--paper);
|
|
@@ -358,7 +426,7 @@ button:disabled {
|
|
| 358 |
}
|
| 359 |
|
| 360 |
.model-card:not(.is-active) .model-select:hover {
|
| 361 |
-
background: rgba(
|
| 362 |
}
|
| 363 |
|
| 364 |
.model-index {
|
|
@@ -381,7 +449,7 @@ button:disabled {
|
|
| 381 |
}
|
| 382 |
|
| 383 |
.model-title-line strong {
|
| 384 |
-
font:
|
| 385 |
}
|
| 386 |
|
| 387 |
.model-copy em {
|
|
@@ -401,8 +469,7 @@ button:disabled {
|
|
| 401 |
.availability {
|
| 402 |
padding: 2px 4px;
|
| 403 |
font: 600 var(--type-utility)/1 var(--mono);
|
| 404 |
-
letter-spacing: 0
|
| 405 |
-
text-transform: uppercase;
|
| 406 |
border: 1px solid var(--line-light);
|
| 407 |
}
|
| 408 |
|
|
@@ -428,7 +495,7 @@ button:disabled {
|
|
| 428 |
|
| 429 |
.model-limit-reason {
|
| 430 |
display: block;
|
| 431 |
-
margin: -4px 8px 9px
|
| 432 |
color: rgba(232, 223, 202, 0.58);
|
| 433 |
font: var(--type-utility)/1.35 var(--mono);
|
| 434 |
}
|
|
@@ -448,17 +515,13 @@ button:disabled {
|
|
| 448 |
.chat-surface {
|
| 449 |
grid-area: chat;
|
| 450 |
min-width: 0;
|
| 451 |
-
min-height: calc(100dvh -
|
| 452 |
-
height: calc(100dvh -
|
| 453 |
display: grid;
|
| 454 |
-
grid-template-rows: auto
|
| 455 |
overflow: hidden;
|
| 456 |
-
color: var(--
|
| 457 |
-
background:
|
| 458 |
-
radial-gradient(ellipse at 82% 16%, transparent 0 77px, rgba(17, 21, 18, 0.055) 78px 79px, transparent 80px 89px, rgba(17, 21, 18, 0.045) 90px 91px, transparent 92px),
|
| 459 |
-
linear-gradient(100deg, transparent 0 67%, rgba(17, 21, 18, 0.025) 67.2% 67.35%, transparent 67.5%),
|
| 460 |
-
var(--paper);
|
| 461 |
-
animation: rise-in 560ms 160ms cubic-bezier(.16, 1, .3, 1) both;
|
| 462 |
position: relative;
|
| 463 |
}
|
| 464 |
|
|
@@ -467,22 +530,64 @@ button:disabled {
|
|
| 467 |
align-items: center;
|
| 468 |
justify-content: space-between;
|
| 469 |
gap: 20px;
|
| 470 |
-
min-height:
|
| 471 |
-
padding:
|
| 472 |
-
border-bottom: 1px solid var(--
|
| 473 |
}
|
| 474 |
|
| 475 |
.chat-header h2 {
|
| 476 |
display: inline;
|
| 477 |
margin: 0;
|
| 478 |
-
font:
|
| 479 |
-
letter-spacing: -0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
}
|
| 481 |
|
| 482 |
.botanical-subtitle {
|
| 483 |
-
margin-left: 10px;
|
| 484 |
color: rgba(17, 21, 18, 0.46);
|
| 485 |
-
font:
|
| 486 |
}
|
| 487 |
|
| 488 |
.chat-header-meta {
|
|
@@ -491,8 +596,7 @@ button:disabled {
|
|
| 491 |
gap: 5px 13px;
|
| 492 |
color: rgba(17, 21, 18, 0.55);
|
| 493 |
font: var(--type-utility)/1.2 var(--mono);
|
| 494 |
-
|
| 495 |
-
letter-spacing: 0.07em;
|
| 496 |
}
|
| 497 |
|
| 498 |
.live-label {
|
|
@@ -501,14 +605,14 @@ button:disabled {
|
|
| 501 |
display: flex;
|
| 502 |
align-items: center;
|
| 503 |
gap: 7px;
|
| 504 |
-
color: #
|
| 505 |
}
|
| 506 |
|
| 507 |
.live-label i {
|
| 508 |
width: 5px;
|
| 509 |
height: 5px;
|
| 510 |
-
box-shadow:
|
| 511 |
-
background: #
|
| 512 |
}
|
| 513 |
|
| 514 |
.live-label.is-dormant {
|
|
@@ -517,7 +621,7 @@ button:disabled {
|
|
| 517 |
|
| 518 |
.live-label.is-dormant i {
|
| 519 |
background: rgba(17, 21, 18, 0.36);
|
| 520 |
-
box-shadow:
|
| 521 |
}
|
| 522 |
|
| 523 |
.runtime-error {
|
|
@@ -599,16 +703,12 @@ button:disabled {
|
|
| 599 |
margin-bottom: 22px;
|
| 600 |
color: rgba(17, 21, 18, 0.45);
|
| 601 |
font: var(--type-utility)/1 var(--mono);
|
| 602 |
-
letter-spacing: 0
|
| 603 |
-
text-transform: uppercase;
|
| 604 |
}
|
| 605 |
|
| 606 |
.transcript-date::before,
|
| 607 |
.transcript-date::after {
|
| 608 |
-
|
| 609 |
-
height: 1px;
|
| 610 |
-
flex: 1;
|
| 611 |
-
background: rgba(17, 21, 18, 0.14);
|
| 612 |
}
|
| 613 |
|
| 614 |
.transcript-empty {
|
|
@@ -618,16 +718,10 @@ button:disabled {
|
|
| 618 |
color: rgba(17, 21, 18, 0.5);
|
| 619 |
}
|
| 620 |
|
| 621 |
-
.transcript-empty > span {
|
| 622 |
-
display: block;
|
| 623 |
-
color: #507324;
|
| 624 |
-
font: 42px/1 var(--display);
|
| 625 |
-
}
|
| 626 |
-
|
| 627 |
.transcript-empty h3 {
|
| 628 |
-
margin:
|
| 629 |
color: var(--ink);
|
| 630 |
-
font:
|
| 631 |
}
|
| 632 |
|
| 633 |
.transcript-empty p {
|
|
@@ -636,10 +730,159 @@ button:disabled {
|
|
| 636 |
line-height: 1.5;
|
| 637 |
}
|
| 638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
.message {
|
| 640 |
-
max-width:
|
| 641 |
margin-bottom: 25px;
|
| 642 |
-
animation: note-in 430ms cubic-bezier(.16, 1, .3, 1) both;
|
| 643 |
}
|
| 644 |
|
| 645 |
.message > header {
|
|
@@ -649,8 +892,7 @@ button:disabled {
|
|
| 649 |
margin-bottom: 8px;
|
| 650 |
color: rgba(17, 21, 18, 0.47);
|
| 651 |
font: var(--type-utility)/1.2 var(--mono);
|
| 652 |
-
|
| 653 |
-
letter-spacing: 0.07em;
|
| 654 |
}
|
| 655 |
|
| 656 |
.message > header strong {
|
|
@@ -747,16 +989,15 @@ button:disabled {
|
|
| 747 |
margin: 0 0 11px;
|
| 748 |
padding: 8px 10px;
|
| 749 |
color: rgba(17, 21, 18, 0.62);
|
| 750 |
-
border: 1px
|
| 751 |
-
|
|
|
|
| 752 |
font-size: var(--type-caption);
|
| 753 |
}
|
| 754 |
|
| 755 |
.reasoning-block summary {
|
| 756 |
color: #4d6728;
|
| 757 |
-
font:
|
| 758 |
-
letter-spacing: 0.08em;
|
| 759 |
-
text-transform: uppercase;
|
| 760 |
cursor: pointer;
|
| 761 |
}
|
| 762 |
|
|
@@ -782,14 +1023,14 @@ button:disabled {
|
|
| 782 |
|
| 783 |
.message-assistant {
|
| 784 |
padding-left: 22px;
|
| 785 |
-
border-left: 2px solid
|
| 786 |
}
|
| 787 |
|
| 788 |
.message-assistant .message-content > p::first-letter {
|
| 789 |
-
float:
|
| 790 |
-
margin:
|
| 791 |
-
color:
|
| 792 |
-
font:
|
| 793 |
}
|
| 794 |
|
| 795 |
.message-user {
|
|
@@ -798,8 +1039,7 @@ button:disabled {
|
|
| 798 |
padding: 12px 15px 13px;
|
| 799 |
color: var(--paper);
|
| 800 |
background: var(--ink);
|
| 801 |
-
|
| 802 |
-
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
|
| 803 |
}
|
| 804 |
|
| 805 |
.message-user > header,
|
|
@@ -817,7 +1057,7 @@ button:disabled {
|
|
| 817 |
margin-top: 9px;
|
| 818 |
padding-top: 7px;
|
| 819 |
color: rgba(17, 21, 18, 0.43);
|
| 820 |
-
border-top: 1px
|
| 821 |
font: var(--type-utility)/1.2 var(--mono);
|
| 822 |
}
|
| 823 |
|
|
@@ -885,287 +1125,107 @@ button:disabled {
|
|
| 885 |
background: var(--rust);
|
| 886 |
}
|
| 887 |
|
| 888 |
-
.tool-call > div {
|
| 889 |
-
padding: 9px 10px;
|
| 890 |
-
border-top: 1px solid rgba(17, 21, 18, 0.13);
|
| 891 |
-
}
|
| 892 |
-
|
| 893 |
-
.tool-call > div p {
|
| 894 |
-
margin: 5px 0 0;
|
| 895 |
-
color: rgba(17, 21, 18, 0.58);
|
| 896 |
-
font-size: var(--type-caption);
|
| 897 |
-
}
|
| 898 |
-
|
| 899 |
-
.composer {
|
| 900 |
-
position: relative;
|
| 901 |
-
padding: 12px clamp(21px, 3vw, 44px) 18px;
|
| 902 |
-
border-top: 1px solid var(--line-dark);
|
| 903 |
-
background: rgba(216, 205, 180, 0.82);
|
| 904 |
-
box-shadow: 0 -15px 38px rgba(17, 21, 18, 0.06);
|
| 905 |
-
backdrop-filter: blur(9px);
|
| 906 |
-
}
|
| 907 |
-
|
| 908 |
-
.composer::before {
|
| 909 |
-
position: absolute;
|
| 910 |
-
top: -5px;
|
| 911 |
-
right: 0;
|
| 912 |
-
left: 0;
|
| 913 |
-
height: 5px;
|
| 914 |
-
content: "";
|
| 915 |
-
background: repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 12px);
|
| 916 |
-
opacity: 0.23;
|
| 917 |
-
}
|
| 918 |
-
|
| 919 |
-
.composer-activity {
|
| 920 |
-
height: 164px;
|
| 921 |
-
display: grid;
|
| 922 |
-
grid-template-rows: 24px 26px 40px 34px;
|
| 923 |
-
gap: 6px;
|
| 924 |
-
margin-bottom: 10px;
|
| 925 |
-
padding: 11px 12px;
|
| 926 |
-
overflow: hidden;
|
| 927 |
-
color: var(--paper);
|
| 928 |
-
border: 1px solid rgba(17, 21, 18, 0.82);
|
| 929 |
-
background:
|
| 930 |
-
linear-gradient(105deg, rgba(185, 255, 69, 0.07), transparent 58%),
|
| 931 |
-
var(--ink);
|
| 932 |
-
box-shadow: 4px 4px 0 rgba(17, 21, 18, 0.12);
|
| 933 |
-
}
|
| 934 |
-
|
| 935 |
-
.activity-stage-line {
|
| 936 |
-
display: grid;
|
| 937 |
-
grid-template-columns: repeat(5, minmax(0, 1fr));
|
| 938 |
-
gap: 6px;
|
| 939 |
-
align-items: start;
|
| 940 |
-
}
|
| 941 |
-
|
| 942 |
-
.activity-stage-line span {
|
| 943 |
-
min-width: 0;
|
| 944 |
-
display: flex;
|
| 945 |
-
align-items: center;
|
| 946 |
-
gap: 5px;
|
| 947 |
-
padding-top: 5px;
|
| 948 |
-
overflow: hidden;
|
| 949 |
-
color: rgba(232, 223, 202, 0.34);
|
| 950 |
-
border-top: 1px solid rgba(232, 223, 202, 0.14);
|
| 951 |
-
font: 700 var(--type-utility)/1 var(--mono);
|
| 952 |
-
letter-spacing: 0.04em;
|
| 953 |
-
text-overflow: ellipsis;
|
| 954 |
-
text-transform: uppercase;
|
| 955 |
-
white-space: nowrap;
|
| 956 |
-
}
|
| 957 |
-
|
| 958 |
-
.activity-stage-line i {
|
| 959 |
-
width: 6px;
|
| 960 |
-
height: 6px;
|
| 961 |
-
flex: 0 0 6px;
|
| 962 |
-
border: 1px solid currentColor;
|
| 963 |
-
border-radius: 50%;
|
| 964 |
-
}
|
| 965 |
-
|
| 966 |
-
.activity-stage-line span.is-complete,
|
| 967 |
-
.activity-stage-line span.is-current {
|
| 968 |
-
color: var(--phosphor);
|
| 969 |
-
border-color: rgba(185, 255, 69, 0.52);
|
| 970 |
-
}
|
| 971 |
-
|
| 972 |
-
.activity-stage-line span.is-complete i {
|
| 973 |
-
background: var(--phosphor);
|
| 974 |
-
}
|
| 975 |
-
|
| 976 |
-
.activity-stage-line span.is-current i {
|
| 977 |
-
box-shadow: 0 0 0 4px var(--phosphor-dim);
|
| 978 |
-
}
|
| 979 |
-
|
| 980 |
-
.activity-status,
|
| 981 |
-
.activity-status > div {
|
| 982 |
-
display: flex;
|
| 983 |
-
align-items: center;
|
| 984 |
-
}
|
| 985 |
-
|
| 986 |
-
.activity-status {
|
| 987 |
-
min-width: 0;
|
| 988 |
-
justify-content: space-between;
|
| 989 |
-
gap: 12px;
|
| 990 |
-
border-bottom: 1px solid var(--line-light);
|
| 991 |
-
font: var(--type-utility)/1 var(--mono);
|
| 992 |
-
}
|
| 993 |
-
|
| 994 |
-
.activity-status > div {
|
| 995 |
-
min-width: 0;
|
| 996 |
-
gap: 8px;
|
| 997 |
-
}
|
| 998 |
-
|
| 999 |
-
.activity-status strong {
|
| 1000 |
-
overflow: hidden;
|
| 1001 |
-
color: var(--paper);
|
| 1002 |
-
font-weight: 600;
|
| 1003 |
-
letter-spacing: 0.04em;
|
| 1004 |
-
text-overflow: ellipsis;
|
| 1005 |
-
text-transform: uppercase;
|
| 1006 |
-
white-space: nowrap;
|
| 1007 |
-
}
|
| 1008 |
-
|
| 1009 |
-
.activity-status > span {
|
| 1010 |
-
flex: 0 0 auto;
|
| 1011 |
-
color: rgba(232, 223, 202, 0.5);
|
| 1012 |
-
}
|
| 1013 |
-
|
| 1014 |
-
.activity-pulse {
|
| 1015 |
-
width: 7px;
|
| 1016 |
-
height: 7px;
|
| 1017 |
-
flex: 0 0 7px;
|
| 1018 |
-
border-radius: 50%;
|
| 1019 |
-
background: rgba(232, 223, 202, 0.35);
|
| 1020 |
-
}
|
| 1021 |
-
|
| 1022 |
-
.composer-activity.is-loading .activity-pulse,
|
| 1023 |
-
.composer-activity.is-inference .activity-pulse {
|
| 1024 |
-
background: var(--phosphor);
|
| 1025 |
-
box-shadow: 0 0 0 4px var(--phosphor-dim);
|
| 1026 |
-
animation: activity-pulse 1.15s ease-in-out infinite;
|
| 1027 |
-
}
|
| 1028 |
-
|
| 1029 |
-
.load-live-grid {
|
| 1030 |
-
display: grid;
|
| 1031 |
-
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
|
| 1032 |
-
gap: 12px;
|
| 1033 |
-
align-items: end;
|
| 1034 |
-
}
|
| 1035 |
-
|
| 1036 |
-
.activity-track-block,
|
| 1037 |
-
.live-metric {
|
| 1038 |
-
min-width: 0;
|
| 1039 |
-
display: grid;
|
| 1040 |
-
gap: 6px;
|
| 1041 |
-
}
|
| 1042 |
-
|
| 1043 |
-
.activity-track-block > div,
|
| 1044 |
-
.live-metric > div {
|
| 1045 |
-
display: flex;
|
| 1046 |
-
align-items: baseline;
|
| 1047 |
-
justify-content: space-between;
|
| 1048 |
-
gap: 8px;
|
| 1049 |
-
font: var(--type-utility)/1 var(--mono);
|
| 1050 |
-
}
|
| 1051 |
-
|
| 1052 |
-
.activity-track-block span,
|
| 1053 |
-
.live-metric span,
|
| 1054 |
-
.activity-memory span {
|
| 1055 |
-
color: rgba(232, 223, 202, 0.46);
|
| 1056 |
-
text-transform: uppercase;
|
| 1057 |
-
}
|
| 1058 |
-
|
| 1059 |
-
.activity-track-block small,
|
| 1060 |
-
.live-metric small {
|
| 1061 |
-
color: rgba(232, 223, 202, 0.5);
|
| 1062 |
-
font-size: inherit;
|
| 1063 |
-
}
|
| 1064 |
-
|
| 1065 |
-
.activity-memory {
|
| 1066 |
-
min-width: 86px;
|
| 1067 |
-
display: grid;
|
| 1068 |
-
gap: 6px;
|
| 1069 |
-
justify-items: end;
|
| 1070 |
-
font: var(--type-utility)/1 var(--mono);
|
| 1071 |
-
}
|
| 1072 |
-
|
| 1073 |
-
.activity-memory strong {
|
| 1074 |
-
color: var(--phosphor);
|
| 1075 |
-
font-size: var(--type-caption);
|
| 1076 |
-
font-weight: 600;
|
| 1077 |
-
}
|
| 1078 |
-
|
| 1079 |
-
.activity-track {
|
| 1080 |
-
height: 5px;
|
| 1081 |
-
overflow: hidden;
|
| 1082 |
-
border: 1px solid rgba(232, 223, 202, 0.18);
|
| 1083 |
-
background: rgba(232, 223, 202, 0.055);
|
| 1084 |
-
}
|
| 1085 |
-
|
| 1086 |
-
.activity-track > span {
|
| 1087 |
-
display: block;
|
| 1088 |
-
height: 100%;
|
| 1089 |
-
background: repeating-linear-gradient(90deg, var(--phosphor) 0 7px, transparent 7px 9px);
|
| 1090 |
-
box-shadow: 0 0 10px rgba(185, 255, 69, 0.34);
|
| 1091 |
-
transition: width 120ms linear;
|
| 1092 |
-
}
|
| 1093 |
-
|
| 1094 |
-
.inference-live-grid {
|
| 1095 |
-
display: grid;
|
| 1096 |
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1097 |
-
gap: 12px;
|
| 1098 |
}
|
| 1099 |
|
| 1100 |
-
.
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
}
|
| 1105 |
|
| 1106 |
-
.
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
|
|
|
| 1110 |
}
|
| 1111 |
|
| 1112 |
-
.
|
| 1113 |
-
|
| 1114 |
-
color: var(--phosphor);
|
| 1115 |
-
font-size: var(--type-caption);
|
| 1116 |
-
font-weight: 600;
|
| 1117 |
}
|
| 1118 |
|
| 1119 |
-
.
|
| 1120 |
-
min-width: 0;
|
| 1121 |
display: grid;
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1125 |
}
|
| 1126 |
|
| 1127 |
-
.
|
|
|
|
| 1128 |
display: flex;
|
| 1129 |
align-items: center;
|
| 1130 |
-
border-top: 1px dashed var(--line-light);
|
| 1131 |
}
|
| 1132 |
|
| 1133 |
-
.
|
| 1134 |
-
|
| 1135 |
-
|
|
|
|
|
|
|
| 1136 |
}
|
| 1137 |
|
| 1138 |
-
.
|
| 1139 |
min-width: 0;
|
| 1140 |
-
|
| 1141 |
-
gap: 5px;
|
| 1142 |
-
opacity: 0.55;
|
| 1143 |
}
|
| 1144 |
|
| 1145 |
-
.
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
opacity: 1;
|
| 1150 |
}
|
| 1151 |
|
| 1152 |
-
.
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
font: var(--type-utility)/1 var(--mono);
|
| 1158 |
}
|
| 1159 |
|
| 1160 |
-
.
|
| 1161 |
color: var(--paper);
|
| 1162 |
font-weight: 600;
|
| 1163 |
}
|
| 1164 |
|
| 1165 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1166 |
overflow: hidden;
|
| 1167 |
-
|
| 1168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
}
|
| 1170 |
|
| 1171 |
.composer-meta {
|
|
@@ -1189,7 +1249,6 @@ button:disabled {
|
|
| 1189 |
border: 0;
|
| 1190 |
background: transparent;
|
| 1191 |
font: 700 var(--type-utility)/1 var(--mono);
|
| 1192 |
-
text-transform: uppercase;
|
| 1193 |
cursor: pointer;
|
| 1194 |
}
|
| 1195 |
|
|
@@ -1205,7 +1264,6 @@ button:disabled {
|
|
| 1205 |
border: 0;
|
| 1206 |
background: transparent;
|
| 1207 |
font: 700 var(--type-utility)/1 var(--mono);
|
| 1208 |
-
text-transform: uppercase;
|
| 1209 |
cursor: pointer;
|
| 1210 |
}
|
| 1211 |
|
|
@@ -1226,9 +1284,10 @@ button:disabled {
|
|
| 1226 |
grid-template-columns: 1fr auto;
|
| 1227 |
position: relative;
|
| 1228 |
min-height: 84px;
|
| 1229 |
-
border: 1px solid var(--
|
| 1230 |
-
|
| 1231 |
-
|
|
|
|
| 1232 |
}
|
| 1233 |
|
| 1234 |
.composer-field label {
|
|
@@ -1237,15 +1296,14 @@ button:disabled {
|
|
| 1237 |
left: 11px;
|
| 1238 |
color: rgba(17, 21, 18, 0.45);
|
| 1239 |
font: var(--type-utility)/1 var(--mono);
|
| 1240 |
-
letter-spacing: 0
|
| 1241 |
-
text-transform: uppercase;
|
| 1242 |
}
|
| 1243 |
|
| 1244 |
.composer-field textarea {
|
| 1245 |
min-width: 0;
|
| 1246 |
resize: none;
|
| 1247 |
padding: 23px 13px 9px;
|
| 1248 |
-
color: var(--
|
| 1249 |
border: 0;
|
| 1250 |
outline: none;
|
| 1251 |
background: transparent;
|
|
@@ -1266,16 +1324,15 @@ button:disabled {
|
|
| 1266 |
margin: 7px;
|
| 1267 |
color: var(--ink);
|
| 1268 |
border: 1px solid var(--ink);
|
|
|
|
| 1269 |
background: var(--phosphor);
|
| 1270 |
font: 700 var(--type-caption)/1 var(--mono);
|
| 1271 |
-
text-transform: uppercase;
|
| 1272 |
cursor: pointer;
|
| 1273 |
-
transition:
|
| 1274 |
}
|
| 1275 |
|
| 1276 |
.send-button:hover:not(:disabled) {
|
| 1277 |
-
|
| 1278 |
-
box-shadow: 3px 3px 0 var(--ink);
|
| 1279 |
}
|
| 1280 |
|
| 1281 |
.send-button:disabled {
|
|
@@ -1304,14 +1361,121 @@ button:disabled {
|
|
| 1304 |
background: var(--phosphor);
|
| 1305 |
}
|
| 1306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1307 |
.inspector {
|
| 1308 |
grid-area: inspector;
|
| 1309 |
-
min-height: calc(100dvh -
|
| 1310 |
-
height: calc(100dvh -
|
| 1311 |
overflow-y: auto;
|
| 1312 |
border-left: 1px solid var(--line-light);
|
| 1313 |
scrollbar-color: rgba(232, 223, 202, 0.18) transparent;
|
| 1314 |
-
animation: rise-in 570ms 220ms cubic-bezier(.16, 1, .3, 1) both;
|
| 1315 |
}
|
| 1316 |
|
| 1317 |
.inspector-section {
|
|
@@ -1322,6 +1486,7 @@ button:disabled {
|
|
| 1322 |
.section-heading {
|
| 1323 |
align-items: center;
|
| 1324 |
margin-bottom: 17px;
|
|
|
|
| 1325 |
}
|
| 1326 |
|
| 1327 |
.section-heading.compact {
|
|
@@ -1333,22 +1498,21 @@ button:disabled {
|
|
| 1333 |
}
|
| 1334 |
|
| 1335 |
.section-heading h2 {
|
| 1336 |
-
font-size:
|
| 1337 |
}
|
| 1338 |
|
| 1339 |
.sampling-label {
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
font: var(--type-utility)/1 var(--
|
| 1345 |
-
text-transform: uppercase;
|
| 1346 |
-
letter-spacing: 0.06em;
|
| 1347 |
}
|
| 1348 |
|
| 1349 |
-
.sampling-label
|
| 1350 |
-
|
| 1351 |
-
|
|
|
|
| 1352 |
}
|
| 1353 |
|
| 1354 |
.speed-gauge {
|
|
@@ -1363,21 +1527,15 @@ button:disabled {
|
|
| 1363 |
height: 116px;
|
| 1364 |
display: grid;
|
| 1365 |
place-items: center;
|
| 1366 |
-
border-radius:
|
| 1367 |
background:
|
| 1368 |
-
radial-gradient(circle, var(--ink) 0
|
| 1369 |
-
conic-gradient(var(--phosphor) 0 var(--gauge), rgba(
|
| 1370 |
-
|
| 1371 |
-
box-shadow: inset 0 0 0 1px rgba(232, 223, 202, 0.1), 0 0 30px rgba(185, 255, 69, 0.055);
|
| 1372 |
}
|
| 1373 |
|
| 1374 |
.gauge-ring::after {
|
| 1375 |
-
|
| 1376 |
-
width: 128px;
|
| 1377 |
-
height: 128px;
|
| 1378 |
-
border: 1px dashed rgba(232, 223, 202, 0.13);
|
| 1379 |
-
border-radius: 45% 55% 52% 48%;
|
| 1380 |
-
content: "";
|
| 1381 |
}
|
| 1382 |
|
| 1383 |
.gauge-ring > div {
|
|
@@ -1385,21 +1543,30 @@ button:disabled {
|
|
| 1385 |
z-index: 1;
|
| 1386 |
display: grid;
|
| 1387 |
justify-items: center;
|
| 1388 |
-
|
| 1389 |
}
|
| 1390 |
|
| 1391 |
.gauge-ring strong {
|
| 1392 |
color: var(--phosphor);
|
| 1393 |
-
font:
|
|
|
|
| 1394 |
}
|
| 1395 |
|
| 1396 |
.gauge-ring small {
|
|
|
|
|
|
|
|
|
|
| 1397 |
margin-top: 3px;
|
| 1398 |
color: var(--paper-ghost);
|
| 1399 |
-
font:
|
|
|
|
| 1400 |
text-transform: uppercase;
|
| 1401 |
}
|
| 1402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1403 |
.speed-gauge dl {
|
| 1404 |
margin: 0;
|
| 1405 |
}
|
|
@@ -1421,6 +1588,65 @@ button:disabled {
|
|
| 1421 |
color: var(--paper);
|
| 1422 |
}
|
| 1423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1424 |
.device-card {
|
| 1425 |
display: grid;
|
| 1426 |
grid-template-columns: auto 1fr auto;
|
|
@@ -1428,8 +1654,9 @@ button:disabled {
|
|
| 1428 |
gap: 10px;
|
| 1429 |
margin: 19px 0 14px;
|
| 1430 |
padding: 10px;
|
| 1431 |
-
border: 1px solid
|
| 1432 |
-
|
|
|
|
| 1433 |
}
|
| 1434 |
|
| 1435 |
.device-icon {
|
|
@@ -1501,7 +1728,7 @@ button:disabled {
|
|
| 1501 |
.meter-track > span {
|
| 1502 |
display: block;
|
| 1503 |
height: 100%;
|
| 1504 |
-
background:
|
| 1505 |
}
|
| 1506 |
|
| 1507 |
.memory-grid {
|
|
@@ -1586,15 +1813,15 @@ button:disabled {
|
|
| 1586 |
width: 25px;
|
| 1587 |
height: 25px;
|
| 1588 |
border: 1px solid var(--line-light);
|
| 1589 |
-
border-radius:
|
| 1590 |
background: transparent;
|
| 1591 |
cursor: pointer;
|
| 1592 |
}
|
| 1593 |
|
| 1594 |
.artifact-empty {
|
| 1595 |
margin: 0;
|
| 1596 |
-
color:
|
| 1597 |
-
font: var(--type-
|
| 1598 |
}
|
| 1599 |
|
| 1600 |
.artifact-pane {
|
|
@@ -1711,7 +1938,6 @@ button:disabled {
|
|
| 1711 |
display: flex;
|
| 1712 |
justify-content: flex-end;
|
| 1713 |
background: rgba(8, 11, 9, 0.64);
|
| 1714 |
-
backdrop-filter: blur(4px);
|
| 1715 |
animation: station-in 160ms ease both;
|
| 1716 |
}
|
| 1717 |
|
|
@@ -1722,9 +1948,7 @@ button:disabled {
|
|
| 1722 |
padding: 30px 27px;
|
| 1723 |
color: var(--paper);
|
| 1724 |
border-left: 1px solid var(--phosphor);
|
| 1725 |
-
background:
|
| 1726 |
-
repeating-radial-gradient(ellipse at 104% 101%, transparent 0 13px, rgba(185, 255, 69, 0.055) 13.5px 14.5px),
|
| 1727 |
-
var(--ink);
|
| 1728 |
box-shadow: -24px 0 70px rgba(0, 0, 0, 0.32);
|
| 1729 |
animation: sheet-in 300ms cubic-bezier(.16, 1, .3, 1) both;
|
| 1730 |
}
|
|
@@ -1761,18 +1985,13 @@ button:disabled {
|
|
| 1761 |
|
| 1762 |
.setting-group {
|
| 1763 |
display: grid;
|
| 1764 |
-
grid-template-columns:
|
| 1765 |
align-items: center;
|
| 1766 |
gap: 12px;
|
| 1767 |
padding: 15px 0;
|
| 1768 |
border-bottom: 1px solid var(--line-light);
|
| 1769 |
}
|
| 1770 |
|
| 1771 |
-
.setting-number {
|
| 1772 |
-
color: var(--phosphor);
|
| 1773 |
-
font: var(--type-utility)/1 var(--mono);
|
| 1774 |
-
}
|
| 1775 |
-
|
| 1776 |
.setting-group > div,
|
| 1777 |
.range-setting > span:first-child {
|
| 1778 |
display: grid;
|
|
@@ -2012,28 +2231,37 @@ button:disabled {
|
|
| 2012 |
|
| 2013 |
@media (max-width: 1279px) {
|
| 2014 |
.station-header {
|
| 2015 |
-
grid-template-columns: minmax(
|
| 2016 |
}
|
| 2017 |
|
| 2018 |
-
.
|
| 2019 |
-
|
| 2020 |
}
|
| 2021 |
|
| 2022 |
-
.
|
| 2023 |
-
display:
|
| 2024 |
}
|
| 2025 |
|
| 2026 |
.workspace-grid {
|
| 2027 |
-
grid-template-columns:
|
| 2028 |
grid-template-areas:
|
| 2029 |
"models chat"
|
| 2030 |
"inspector inspector";
|
| 2031 |
}
|
| 2032 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2033 |
.model-rail,
|
| 2034 |
.chat-surface {
|
| 2035 |
-
min-height: calc(100dvh -
|
| 2036 |
-
height: calc(100dvh -
|
| 2037 |
}
|
| 2038 |
|
| 2039 |
.inspector {
|
|
@@ -2042,7 +2270,7 @@ button:disabled {
|
|
| 2042 |
display: grid;
|
| 2043 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 2044 |
overflow: visible;
|
| 2045 |
-
border-top: 1px solid var(--
|
| 2046 |
border-left: 0;
|
| 2047 |
}
|
| 2048 |
|
|
@@ -2054,36 +2282,37 @@ button:disabled {
|
|
| 2054 |
|
| 2055 |
@media (max-width: 820px) {
|
| 2056 |
.station-header {
|
| 2057 |
-
|
| 2058 |
-
|
|
|
|
| 2059 |
}
|
| 2060 |
|
| 2061 |
.brand-block {
|
| 2062 |
-
|
| 2063 |
-
padding
|
|
|
|
| 2064 |
}
|
| 2065 |
|
| 2066 |
-
.brand-
|
| 2067 |
-
width:
|
| 2068 |
-
height:
|
| 2069 |
-
flex-basis:
|
| 2070 |
}
|
| 2071 |
|
| 2072 |
-
.
|
| 2073 |
-
|
| 2074 |
-
|
| 2075 |
-
|
|
|
|
| 2076 |
border-top: 1px solid var(--line-light);
|
| 2077 |
}
|
| 2078 |
|
| 2079 |
.header-actions {
|
| 2080 |
-
|
| 2081 |
-
|
| 2082 |
-
|
| 2083 |
-
|
| 2084 |
-
|
| 2085 |
-
.station-clock {
|
| 2086 |
-
display: none;
|
| 2087 |
}
|
| 2088 |
|
| 2089 |
.workspace-grid {
|
|
@@ -2101,7 +2330,7 @@ button:disabled {
|
|
| 2101 |
display: block;
|
| 2102 |
padding: 18px 15px;
|
| 2103 |
border-right: 0;
|
| 2104 |
-
border-bottom: 1px solid var(--
|
| 2105 |
}
|
| 2106 |
|
| 2107 |
.model-list {
|
|
@@ -2123,7 +2352,9 @@ button:disabled {
|
|
| 2123 |
|
| 2124 |
.chat-surface {
|
| 2125 |
display: grid;
|
| 2126 |
-
|
|
|
|
|
|
|
| 2127 |
overflow: visible;
|
| 2128 |
}
|
| 2129 |
|
|
@@ -2156,7 +2387,7 @@ button:disabled {
|
|
| 2156 |
|
| 2157 |
.inspector {
|
| 2158 |
display: block;
|
| 2159 |
-
border-top: 1px solid var(--
|
| 2160 |
}
|
| 2161 |
|
| 2162 |
.inspector-section {
|
|
@@ -2167,12 +2398,21 @@ button:disabled {
|
|
| 2167 |
|
| 2168 |
@media (max-width: 520px) {
|
| 2169 |
.brand-block h1 {
|
| 2170 |
-
font-size:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2171 |
}
|
| 2172 |
|
| 2173 |
-
.
|
| 2174 |
-
.
|
| 2175 |
-
|
|
|
|
| 2176 |
}
|
| 2177 |
|
| 2178 |
.chat-header {
|
|
@@ -2180,8 +2420,7 @@ button:disabled {
|
|
| 2180 |
}
|
| 2181 |
|
| 2182 |
.botanical-subtitle {
|
| 2183 |
-
display:
|
| 2184 |
-
margin: 4px 0 0;
|
| 2185 |
}
|
| 2186 |
|
| 2187 |
.message-user {
|
|
@@ -2193,38 +2432,11 @@ button:disabled {
|
|
| 2193 |
}
|
| 2194 |
|
| 2195 |
.composer-activity {
|
| 2196 |
-
|
| 2197 |
-
grid-template-rows: 24px 30px 44px 36px;
|
| 2198 |
-
padding-inline: 9px;
|
| 2199 |
-
}
|
| 2200 |
-
|
| 2201 |
-
.activity-stage-line {
|
| 2202 |
-
gap: 3px;
|
| 2203 |
-
}
|
| 2204 |
-
|
| 2205 |
-
.activity-stage-line span {
|
| 2206 |
-
gap: 3px;
|
| 2207 |
-
letter-spacing: 0;
|
| 2208 |
-
}
|
| 2209 |
-
|
| 2210 |
-
.activity-stage-line i {
|
| 2211 |
-
display: none;
|
| 2212 |
-
}
|
| 2213 |
-
|
| 2214 |
-
.load-live-grid {
|
| 2215 |
-
gap: 7px;
|
| 2216 |
-
}
|
| 2217 |
-
|
| 2218 |
-
.activity-memory {
|
| 2219 |
-
min-width: 68px;
|
| 2220 |
-
}
|
| 2221 |
-
|
| 2222 |
-
.shard-strip {
|
| 2223 |
-
gap: 3px;
|
| 2224 |
}
|
| 2225 |
|
| 2226 |
-
.
|
| 2227 |
-
|
| 2228 |
}
|
| 2229 |
|
| 2230 |
.composer-field {
|
|
@@ -2249,8 +2461,11 @@ button:disabled {
|
|
| 2249 |
}
|
| 2250 |
|
| 2251 |
.gauge-ring::after {
|
| 2252 |
-
|
| 2253 |
-
|
|
|
|
|
|
|
|
|
|
| 2254 |
}
|
| 2255 |
}
|
| 2256 |
|
|
|
|
| 1 |
:root {
|
| 2 |
color-scheme: dark;
|
| 3 |
+
--ink: #141613;
|
| 4 |
+
--ink-raised: #1b1e1a;
|
| 5 |
+
--ink-soft: #252921;
|
| 6 |
+
--paper: #f1f2ed;
|
| 7 |
+
--paper-deep: #e4e6df;
|
| 8 |
+
--paper-ghost: rgba(241, 242, 237, 0.64);
|
| 9 |
+
--phosphor: #a8d574;
|
| 10 |
+
--phosphor-dim: rgba(168, 213, 116, 0.14);
|
| 11 |
+
--moss: #738066;
|
| 12 |
+
--rust: #c27a56;
|
| 13 |
+
--line-light: rgba(241, 242, 237, 0.13);
|
| 14 |
+
--line-dark: rgba(20, 22, 19, 0.15);
|
| 15 |
+
--display: "Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;
|
| 16 |
+
--body: "Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;
|
| 17 |
--mono: "Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;
|
| 18 |
+
--type-utility: clamp(0.75rem, 0.12vw + 0.72rem, 0.8125rem);
|
| 19 |
+
--type-caption: clamp(0.8125rem, 0.15vw + 0.77rem, 0.9375rem);
|
| 20 |
+
--type-body: clamp(1.0625rem, 0.18vw + 1.02rem, 1.1875rem);
|
| 21 |
+
--type-heading: clamp(1.55rem, 0.85vw + 1.35rem, 2.15rem);
|
| 22 |
font-family: var(--body);
|
| 23 |
font-synthesis: none;
|
| 24 |
text-rendering: optimizeLegibility;
|
|
|
|
| 36 |
margin: 0;
|
| 37 |
min-width: 320px;
|
| 38 |
min-height: 100vh;
|
| 39 |
+
background: var(--ink);
|
|
|
|
|
|
|
|
|
|
| 40 |
color: var(--paper);
|
| 41 |
font-size: var(--type-body);
|
| 42 |
}
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
.station-frame {
|
| 72 |
+
--canvas-bg: #f1f2ed;
|
| 73 |
+
--canvas-raised: #e4e6df;
|
| 74 |
+
--canvas-field: #f8f9f5;
|
| 75 |
+
--canvas-text: #141613;
|
| 76 |
+
--canvas-muted: rgba(20, 22, 19, 0.52);
|
| 77 |
+
--canvas-line: rgba(20, 22, 19, 0.15);
|
| 78 |
min-height: 100dvh;
|
| 79 |
+
color-scheme: light;
|
| 80 |
isolation: isolate;
|
| 81 |
animation: station-in 580ms cubic-bezier(.16, 1, .3, 1) both;
|
| 82 |
}
|
| 83 |
|
| 84 |
+
.station-frame[data-theme="dark"] {
|
| 85 |
+
--canvas-bg: #1a1c18;
|
| 86 |
+
--canvas-raised: #20231f;
|
| 87 |
+
--canvas-field: #252823;
|
| 88 |
+
--canvas-text: #edf0e8;
|
| 89 |
+
--canvas-muted: rgba(237, 240, 232, 0.58);
|
| 90 |
+
--canvas-line: rgba(237, 240, 232, 0.13);
|
| 91 |
+
color-scheme: dark;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
.station-frame::before {
|
| 95 |
+
display: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
.station-header {
|
| 99 |
+
min-height: 64px;
|
| 100 |
display: grid;
|
| 101 |
+
grid-template-columns: 260px minmax(320px, 1fr) auto;
|
| 102 |
align-items: stretch;
|
| 103 |
background: var(--ink);
|
| 104 |
+
border-bottom: 1px solid var(--line-light);
|
|
|
|
| 105 |
position: relative;
|
| 106 |
z-index: 10;
|
| 107 |
}
|
| 108 |
|
| 109 |
.brand-block {
|
| 110 |
display: flex;
|
| 111 |
+
gap: 10px;
|
| 112 |
align-items: center;
|
| 113 |
+
padding: 10px 18px;
|
| 114 |
+
border-right: 1px solid var(--line-light);
|
| 115 |
}
|
| 116 |
|
| 117 |
.brand-block h1 {
|
| 118 |
margin: 0;
|
| 119 |
+
font: 650 20px/1.1 var(--display);
|
| 120 |
+
letter-spacing: -0.02em;
|
| 121 |
}
|
| 122 |
|
| 123 |
+
.brand-block > div {
|
| 124 |
+
display: grid;
|
| 125 |
+
gap: 2px;
|
| 126 |
}
|
| 127 |
|
| 128 |
+
.brand-block > div > span {
|
| 129 |
+
color: var(--paper-ghost);
|
| 130 |
+
font-size: var(--type-caption);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.brand-glyph {
|
| 134 |
+
width: 34px;
|
| 135 |
+
height: 34px;
|
| 136 |
+
flex: 0 0 34px;
|
| 137 |
display: grid;
|
| 138 |
place-items: center;
|
| 139 |
+
color: var(--phosphor);
|
| 140 |
+
border: 1px solid rgba(168, 213, 116, 0.42);
|
| 141 |
+
border-radius: 8px;
|
| 142 |
+
font: 650 16px/1 var(--mono);
|
| 143 |
}
|
| 144 |
|
| 145 |
.brand-mark::before,
|
|
|
|
| 185 |
color: rgba(17, 21, 18, 0.52);
|
| 186 |
}
|
| 187 |
|
| 188 |
+
.browser-notice {
|
| 189 |
display: flex;
|
| 190 |
align-items: center;
|
| 191 |
+
gap: 10px;
|
| 192 |
+
min-width: 0;
|
| 193 |
+
padding: 0 20px;
|
|
|
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
+
.notice-mark {
|
| 197 |
+
width: 6px;
|
| 198 |
+
height: 6px;
|
| 199 |
+
flex: 0 0 6px;
|
| 200 |
+
border-radius: 2px;
|
|
|
|
|
|
|
| 201 |
background: var(--phosphor);
|
|
|
|
| 202 |
}
|
| 203 |
|
| 204 |
+
.browser-notice > span:last-child {
|
| 205 |
display: grid;
|
| 206 |
+
gap: 2px;
|
| 207 |
}
|
| 208 |
|
| 209 |
+
.browser-notice strong {
|
| 210 |
+
color: var(--paper);
|
| 211 |
+
font: 600 var(--type-caption)/1.2 var(--body);
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
+
.browser-notice > span > span {
|
| 215 |
color: var(--paper-ghost);
|
| 216 |
font: var(--type-caption)/1.25 var(--body);
|
| 217 |
}
|
| 218 |
|
| 219 |
+
.browser-notice.is-warning .notice-mark {
|
| 220 |
+
background: var(--rust);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.browser-notice.is-warning strong {
|
| 224 |
+
color: #efb08f;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
.header-actions {
|
| 228 |
justify-self: end;
|
| 229 |
display: flex;
|
| 230 |
align-items: center;
|
| 231 |
+
gap: 10px;
|
| 232 |
+
padding: 0 14px;
|
| 233 |
+
border-left: 1px solid var(--line-light);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.layout-controls {
|
| 237 |
+
display: flex;
|
| 238 |
+
gap: 4px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.layout-controls button,
|
| 242 |
+
.icon-button {
|
| 243 |
+
width: 36px;
|
| 244 |
+
height: 36px;
|
| 245 |
+
display: grid;
|
| 246 |
+
place-items: center;
|
| 247 |
+
color: var(--paper-ghost);
|
| 248 |
+
border: 1px solid var(--line-light);
|
| 249 |
+
border-radius: 7px;
|
| 250 |
+
background: transparent;
|
| 251 |
+
cursor: pointer;
|
| 252 |
+
transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.layout-controls button:hover,
|
| 256 |
+
.icon-button:hover {
|
| 257 |
+
color: var(--paper);
|
| 258 |
+
border-color: rgba(241, 242, 237, 0.3);
|
| 259 |
+
background: rgba(241, 242, 237, 0.05);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.layout-controls button[aria-pressed="true"] {
|
| 263 |
+
color: var(--phosphor);
|
| 264 |
+
border-color: rgba(168, 213, 116, 0.34);
|
| 265 |
+
background: var(--phosphor-dim);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.layout-controls svg,
|
| 269 |
+
.icon-button svg {
|
| 270 |
+
width: 18px;
|
| 271 |
+
fill: none;
|
| 272 |
+
stroke: currentColor;
|
| 273 |
+
stroke-width: 1.4;
|
| 274 |
}
|
| 275 |
|
| 276 |
.station-clock {
|
|
|
|
| 293 |
}
|
| 294 |
|
| 295 |
.icon-button {
|
| 296 |
+
flex: 0 0 36px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
}
|
| 298 |
|
| 299 |
.icon-button:hover {
|
| 300 |
+
color: var(--paper);
|
|
|
|
|
|
|
| 301 |
}
|
| 302 |
|
| 303 |
.icon-button svg {
|
|
|
|
| 308 |
}
|
| 309 |
|
| 310 |
.workspace-grid {
|
| 311 |
+
min-height: calc(100dvh - 64px);
|
| 312 |
display: grid;
|
| 313 |
+
grid-template-columns: 260px minmax(0, 1fr) 340px;
|
| 314 |
grid-template-areas: "models chat inspector";
|
| 315 |
+
background: #10120f;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.workspace-grid.is-models-collapsed {
|
| 319 |
+
grid-template-columns: 0 minmax(0, 1fr) 340px;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.workspace-grid.is-inspector-collapsed {
|
| 323 |
+
grid-template-columns: 260px minmax(0, 1fr) 0;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.workspace-grid.is-models-collapsed.is-inspector-collapsed {
|
| 327 |
+
grid-template-columns: 0 minmax(0, 1fr) 0;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.workspace-grid.layout-centered .chat-surface {
|
| 331 |
+
width: min(100%, 1120px);
|
| 332 |
+
justify-self: center;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.workspace-grid.layout-wide .chat-surface {
|
| 336 |
+
width: 100%;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.model-rail[hidden],
|
| 340 |
+
.inspector[hidden] {
|
| 341 |
+
display: none !important;
|
| 342 |
}
|
| 343 |
|
| 344 |
.model-rail,
|
|
|
|
| 354 |
display: flex;
|
| 355 |
flex-direction: column;
|
| 356 |
border-right: 1px solid var(--line-light);
|
| 357 |
+
padding: 20px 14px 14px;
|
|
|
|
| 358 |
}
|
| 359 |
|
| 360 |
.model-rail::before {
|
| 361 |
+
display: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
}
|
| 363 |
|
| 364 |
.rail-heading,
|
|
|
|
| 366 |
position: relative;
|
| 367 |
z-index: 1;
|
| 368 |
display: flex;
|
| 369 |
+
align-items: center;
|
| 370 |
+
gap: 10px;
|
| 371 |
}
|
| 372 |
|
| 373 |
.rail-heading > span,
|
|
|
|
| 381 |
.rail-heading h2,
|
| 382 |
.section-heading h2 {
|
| 383 |
margin: 0;
|
| 384 |
+
font: 600 20px/1.2 var(--display);
|
| 385 |
+
letter-spacing: -0.015em;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.rail-heading p {
|
| 389 |
+
margin: 4px 0 0;
|
| 390 |
+
color: var(--paper-ghost);
|
| 391 |
+
font-size: var(--type-caption);
|
| 392 |
+
line-height: 1.35;
|
| 393 |
}
|
| 394 |
|
| 395 |
.model-list {
|
|
|
|
| 401 |
}
|
| 402 |
|
| 403 |
.model-card {
|
| 404 |
+
border: 1px solid var(--line-light);
|
| 405 |
+
border-radius: 8px;
|
| 406 |
transition: border-color 180ms ease, background 180ms ease;
|
| 407 |
}
|
| 408 |
|
| 409 |
.model-card.is-active {
|
| 410 |
+
border-color: rgba(168, 213, 116, 0.58);
|
| 411 |
background: var(--phosphor-dim);
|
|
|
|
| 412 |
}
|
| 413 |
|
| 414 |
.model-select {
|
| 415 |
width: 100%;
|
| 416 |
display: grid;
|
| 417 |
+
grid-template-columns: 1fr auto;
|
| 418 |
gap: 8px;
|
| 419 |
+
min-height: 72px;
|
| 420 |
padding: 13px 9px 12px;
|
| 421 |
text-align: left;
|
| 422 |
color: var(--paper);
|
|
|
|
| 426 |
}
|
| 427 |
|
| 428 |
.model-card:not(.is-active) .model-select:hover {
|
| 429 |
+
background: rgba(241, 242, 237, 0.045);
|
| 430 |
}
|
| 431 |
|
| 432 |
.model-index {
|
|
|
|
| 449 |
}
|
| 450 |
|
| 451 |
.model-title-line strong {
|
| 452 |
+
font: 600 20px/1 var(--display);
|
| 453 |
}
|
| 454 |
|
| 455 |
.model-copy em {
|
|
|
|
| 469 |
.availability {
|
| 470 |
padding: 2px 4px;
|
| 471 |
font: 600 var(--type-utility)/1 var(--mono);
|
| 472 |
+
letter-spacing: 0;
|
|
|
|
| 473 |
border: 1px solid var(--line-light);
|
| 474 |
}
|
| 475 |
|
|
|
|
| 495 |
|
| 496 |
.model-limit-reason {
|
| 497 |
display: block;
|
| 498 |
+
margin: -4px 8px 9px;
|
| 499 |
color: rgba(232, 223, 202, 0.58);
|
| 500 |
font: var(--type-utility)/1.35 var(--mono);
|
| 501 |
}
|
|
|
|
| 515 |
.chat-surface {
|
| 516 |
grid-area: chat;
|
| 517 |
min-width: 0;
|
| 518 |
+
min-height: calc(100dvh - 64px);
|
| 519 |
+
height: calc(100dvh - 64px);
|
| 520 |
display: grid;
|
| 521 |
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
| 522 |
overflow: hidden;
|
| 523 |
+
color: var(--canvas-text);
|
| 524 |
+
background: var(--canvas-bg);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
position: relative;
|
| 526 |
}
|
| 527 |
|
|
|
|
| 530 |
align-items: center;
|
| 531 |
justify-content: space-between;
|
| 532 |
gap: 20px;
|
| 533 |
+
min-height: 72px;
|
| 534 |
+
padding: 14px clamp(20px, 3vw, 40px);
|
| 535 |
+
border-bottom: 1px solid var(--canvas-line);
|
| 536 |
}
|
| 537 |
|
| 538 |
.chat-header h2 {
|
| 539 |
display: inline;
|
| 540 |
margin: 0;
|
| 541 |
+
font: 650 var(--type-heading)/1.1 var(--display);
|
| 542 |
+
letter-spacing: -0.025em;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.chat-header-main {
|
| 546 |
+
min-width: 0;
|
| 547 |
+
display: flex;
|
| 548 |
+
align-items: center;
|
| 549 |
+
gap: 8px;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
.chat-list-button,
|
| 553 |
+
.chat-settings-button {
|
| 554 |
+
min-height: 36px;
|
| 555 |
+
display: inline-flex;
|
| 556 |
+
align-items: center;
|
| 557 |
+
gap: 5px;
|
| 558 |
+
padding: 0 9px;
|
| 559 |
+
color: var(--canvas-muted);
|
| 560 |
+
border: 1px solid var(--canvas-line);
|
| 561 |
+
border-radius: 7px;
|
| 562 |
+
background: transparent;
|
| 563 |
+
font: 600 var(--type-utility)/1 var(--body);
|
| 564 |
+
white-space: nowrap;
|
| 565 |
+
cursor: pointer;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
.chat-list-button:hover,
|
| 569 |
+
.chat-settings-button:hover {
|
| 570 |
+
color: var(--canvas-text);
|
| 571 |
+
border-color: var(--canvas-muted);
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.chat-list-button svg {
|
| 575 |
+
width: 16px;
|
| 576 |
+
fill: none;
|
| 577 |
+
stroke: currentColor;
|
| 578 |
+
stroke-width: 1.5;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.chat-title {
|
| 582 |
+
min-width: 0;
|
| 583 |
+
display: flex;
|
| 584 |
+
align-items: baseline;
|
| 585 |
+
gap: 10px;
|
| 586 |
}
|
| 587 |
|
| 588 |
.botanical-subtitle {
|
|
|
|
| 589 |
color: rgba(17, 21, 18, 0.46);
|
| 590 |
+
font: var(--type-caption)/1 var(--body);
|
| 591 |
}
|
| 592 |
|
| 593 |
.chat-header-meta {
|
|
|
|
| 596 |
gap: 5px 13px;
|
| 597 |
color: rgba(17, 21, 18, 0.55);
|
| 598 |
font: var(--type-utility)/1.2 var(--mono);
|
| 599 |
+
letter-spacing: 0;
|
|
|
|
| 600 |
}
|
| 601 |
|
| 602 |
.live-label {
|
|
|
|
| 605 |
display: flex;
|
| 606 |
align-items: center;
|
| 607 |
gap: 7px;
|
| 608 |
+
color: #46622f;
|
| 609 |
}
|
| 610 |
|
| 611 |
.live-label i {
|
| 612 |
width: 5px;
|
| 613 |
height: 5px;
|
| 614 |
+
box-shadow: none;
|
| 615 |
+
background: #668a45;
|
| 616 |
}
|
| 617 |
|
| 618 |
.live-label.is-dormant {
|
|
|
|
| 621 |
|
| 622 |
.live-label.is-dormant i {
|
| 623 |
background: rgba(17, 21, 18, 0.36);
|
| 624 |
+
box-shadow: none;
|
| 625 |
}
|
| 626 |
|
| 627 |
.runtime-error {
|
|
|
|
| 703 |
margin-bottom: 22px;
|
| 704 |
color: rgba(17, 21, 18, 0.45);
|
| 705 |
font: var(--type-utility)/1 var(--mono);
|
| 706 |
+
letter-spacing: 0;
|
|
|
|
| 707 |
}
|
| 708 |
|
| 709 |
.transcript-date::before,
|
| 710 |
.transcript-date::after {
|
| 711 |
+
display: none;
|
|
|
|
|
|
|
|
|
|
| 712 |
}
|
| 713 |
|
| 714 |
.transcript-empty {
|
|
|
|
| 718 |
color: rgba(17, 21, 18, 0.5);
|
| 719 |
}
|
| 720 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
.transcript-empty h3 {
|
| 722 |
+
margin: 0 0 8px;
|
| 723 |
color: var(--ink);
|
| 724 |
+
font: 650 22px/1.2 var(--display);
|
| 725 |
}
|
| 726 |
|
| 727 |
.transcript-empty p {
|
|
|
|
| 730 |
line-height: 1.5;
|
| 731 |
}
|
| 732 |
|
| 733 |
+
.system-instructions-button,
|
| 734 |
+
.conversation-list-empty button,
|
| 735 |
+
.new-chat-button {
|
| 736 |
+
min-height: 42px;
|
| 737 |
+
padding: 0 13px;
|
| 738 |
+
color: var(--ink);
|
| 739 |
+
border: 1px solid var(--ink);
|
| 740 |
+
border-radius: 7px;
|
| 741 |
+
background: var(--phosphor);
|
| 742 |
+
font: 650 var(--type-caption)/1 var(--body);
|
| 743 |
+
cursor: pointer;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
.system-instructions-button {
|
| 747 |
+
margin-top: 18px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.conversation-library {
|
| 751 |
+
grid-template-rows: auto minmax(0, 1fr);
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.conversation-list-header > div {
|
| 755 |
+
display: grid;
|
| 756 |
+
gap: 4px;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.conversation-list-header p {
|
| 760 |
+
margin: 0;
|
| 761 |
+
color: var(--canvas-muted);
|
| 762 |
+
font-size: var(--type-caption);
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
.conversation-list {
|
| 766 |
+
min-height: 0;
|
| 767 |
+
overflow-y: auto;
|
| 768 |
+
padding: clamp(20px, 4vw, 48px);
|
| 769 |
+
scrollbar-color: var(--canvas-muted) transparent;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.conversation-row {
|
| 773 |
+
max-width: 850px;
|
| 774 |
+
display: grid;
|
| 775 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 776 |
+
align-items: stretch;
|
| 777 |
+
margin: 0 auto 9px;
|
| 778 |
+
border: 1px solid var(--canvas-line);
|
| 779 |
+
border-radius: 9px;
|
| 780 |
+
background: var(--canvas-field);
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
.conversation-row.is-current {
|
| 784 |
+
border-color: rgba(115, 128, 102, 0.72);
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.conversation-open {
|
| 788 |
+
min-width: 0;
|
| 789 |
+
display: grid;
|
| 790 |
+
gap: 6px;
|
| 791 |
+
padding: 15px 16px;
|
| 792 |
+
color: var(--canvas-text);
|
| 793 |
+
border: 0;
|
| 794 |
+
background: transparent;
|
| 795 |
+
text-align: left;
|
| 796 |
+
cursor: pointer;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
.conversation-title-line {
|
| 800 |
+
min-width: 0;
|
| 801 |
+
display: flex;
|
| 802 |
+
align-items: center;
|
| 803 |
+
gap: 9px;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.conversation-title-line strong {
|
| 807 |
+
overflow: hidden;
|
| 808 |
+
font: 650 var(--type-body)/1.2 var(--display);
|
| 809 |
+
text-overflow: ellipsis;
|
| 810 |
+
white-space: nowrap;
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
.conversation-title-line small {
|
| 814 |
+
flex: 0 0 auto;
|
| 815 |
+
color: #52712f;
|
| 816 |
+
font: 600 var(--type-utility)/1 var(--body);
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
.conversation-open > span:nth-child(2) {
|
| 820 |
+
overflow: hidden;
|
| 821 |
+
color: var(--canvas-muted);
|
| 822 |
+
font-size: var(--type-caption);
|
| 823 |
+
line-height: 1.35;
|
| 824 |
+
text-overflow: ellipsis;
|
| 825 |
+
white-space: nowrap;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.conversation-open > small {
|
| 829 |
+
color: var(--canvas-muted);
|
| 830 |
+
font: var(--type-utility)/1.2 var(--mono);
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
.conversation-actions {
|
| 834 |
+
display: grid;
|
| 835 |
+
align-content: center;
|
| 836 |
+
gap: 4px;
|
| 837 |
+
padding: 7px;
|
| 838 |
+
border-left: 1px solid var(--canvas-line);
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
.conversation-actions button {
|
| 842 |
+
min-height: 34px;
|
| 843 |
+
padding: 0 8px;
|
| 844 |
+
color: var(--canvas-muted);
|
| 845 |
+
border: 0;
|
| 846 |
+
background: transparent;
|
| 847 |
+
font: 600 var(--type-utility)/1 var(--body);
|
| 848 |
+
cursor: pointer;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
.conversation-actions button:hover {
|
| 852 |
+
color: var(--canvas-text);
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
.conversation-actions .conversation-delete {
|
| 856 |
+
color: var(--rust);
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
.conversation-list-empty {
|
| 860 |
+
max-width: 430px;
|
| 861 |
+
display: grid;
|
| 862 |
+
justify-items: center;
|
| 863 |
+
gap: 10px;
|
| 864 |
+
margin: 15vh auto 0;
|
| 865 |
+
color: var(--canvas-muted);
|
| 866 |
+
text-align: center;
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
.conversation-list-empty h3,
|
| 870 |
+
.conversation-list-empty p {
|
| 871 |
+
margin: 0;
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
.conversation-list-empty h3 {
|
| 875 |
+
color: var(--canvas-text);
|
| 876 |
+
font: 650 22px/1.2 var(--display);
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
.conversation-list-empty button {
|
| 880 |
+
margin-top: 8px;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
.message {
|
| 884 |
+
max-width: 760px;
|
| 885 |
margin-bottom: 25px;
|
|
|
|
| 886 |
}
|
| 887 |
|
| 888 |
.message > header {
|
|
|
|
| 892 |
margin-bottom: 8px;
|
| 893 |
color: rgba(17, 21, 18, 0.47);
|
| 894 |
font: var(--type-utility)/1.2 var(--mono);
|
| 895 |
+
letter-spacing: 0;
|
|
|
|
| 896 |
}
|
| 897 |
|
| 898 |
.message > header strong {
|
|
|
|
| 989 |
margin: 0 0 11px;
|
| 990 |
padding: 8px 10px;
|
| 991 |
color: rgba(17, 21, 18, 0.62);
|
| 992 |
+
border: 1px solid rgba(17, 21, 18, 0.16);
|
| 993 |
+
border-radius: 8px;
|
| 994 |
+
background: rgba(17, 21, 18, 0.03);
|
| 995 |
font-size: var(--type-caption);
|
| 996 |
}
|
| 997 |
|
| 998 |
.reasoning-block summary {
|
| 999 |
color: #4d6728;
|
| 1000 |
+
font: 600 var(--type-caption)/1.2 var(--body);
|
|
|
|
|
|
|
| 1001 |
cursor: pointer;
|
| 1002 |
}
|
| 1003 |
|
|
|
|
| 1023 |
|
| 1024 |
.message-assistant {
|
| 1025 |
padding-left: 22px;
|
| 1026 |
+
border-left: 2px solid rgba(84, 111, 60, 0.45);
|
| 1027 |
}
|
| 1028 |
|
| 1029 |
.message-assistant .message-content > p::first-letter {
|
| 1030 |
+
float: none;
|
| 1031 |
+
margin: 0;
|
| 1032 |
+
color: inherit;
|
| 1033 |
+
font: inherit;
|
| 1034 |
}
|
| 1035 |
|
| 1036 |
.message-user {
|
|
|
|
| 1039 |
padding: 12px 15px 13px;
|
| 1040 |
color: var(--paper);
|
| 1041 |
background: var(--ink);
|
| 1042 |
+
border-radius: 10px;
|
|
|
|
| 1043 |
}
|
| 1044 |
|
| 1045 |
.message-user > header,
|
|
|
|
| 1057 |
margin-top: 9px;
|
| 1058 |
padding-top: 7px;
|
| 1059 |
color: rgba(17, 21, 18, 0.43);
|
| 1060 |
+
border-top: 1px solid rgba(17, 21, 18, 0.13);
|
| 1061 |
font: var(--type-utility)/1.2 var(--mono);
|
| 1062 |
}
|
| 1063 |
|
|
|
|
| 1125 |
background: var(--rust);
|
| 1126 |
}
|
| 1127 |
|
| 1128 |
+
.tool-call > div {
|
| 1129 |
+
padding: 9px 10px;
|
| 1130 |
+
border-top: 1px solid rgba(17, 21, 18, 0.13);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1131 |
}
|
| 1132 |
|
| 1133 |
+
.tool-call > div p {
|
| 1134 |
+
margin: 5px 0 0;
|
| 1135 |
+
color: rgba(17, 21, 18, 0.58);
|
| 1136 |
+
font-size: var(--type-caption);
|
| 1137 |
}
|
| 1138 |
|
| 1139 |
+
.composer {
|
| 1140 |
+
position: relative;
|
| 1141 |
+
padding: 12px clamp(21px, 3vw, 44px) 18px;
|
| 1142 |
+
border-top: 1px solid var(--canvas-line);
|
| 1143 |
+
background: var(--canvas-raised);
|
| 1144 |
}
|
| 1145 |
|
| 1146 |
+
.composer::before {
|
| 1147 |
+
display: none;
|
|
|
|
|
|
|
|
|
|
| 1148 |
}
|
| 1149 |
|
| 1150 |
+
.composer-activity {
|
|
|
|
| 1151 |
display: grid;
|
| 1152 |
+
gap: 10px;
|
| 1153 |
+
margin-bottom: 12px;
|
| 1154 |
+
padding: 12px 14px;
|
| 1155 |
+
color: var(--paper);
|
| 1156 |
+
border: 1px solid rgba(20, 22, 19, 0.9);
|
| 1157 |
+
border-radius: 8px;
|
| 1158 |
+
background: var(--ink);
|
| 1159 |
}
|
| 1160 |
|
| 1161 |
+
.activity-status,
|
| 1162 |
+
.activity-status > div {
|
| 1163 |
display: flex;
|
| 1164 |
align-items: center;
|
|
|
|
| 1165 |
}
|
| 1166 |
|
| 1167 |
+
.activity-status {
|
| 1168 |
+
min-width: 0;
|
| 1169 |
+
justify-content: space-between;
|
| 1170 |
+
gap: 12px;
|
| 1171 |
+
font: var(--type-caption)/1.2 var(--body);
|
| 1172 |
}
|
| 1173 |
|
| 1174 |
+
.activity-status > div {
|
| 1175 |
min-width: 0;
|
| 1176 |
+
gap: 10px;
|
|
|
|
|
|
|
| 1177 |
}
|
| 1178 |
|
| 1179 |
+
.activity-status > div > span:last-child {
|
| 1180 |
+
min-width: 0;
|
| 1181 |
+
display: grid;
|
| 1182 |
+
gap: 3px;
|
|
|
|
| 1183 |
}
|
| 1184 |
|
| 1185 |
+
.activity-status strong,
|
| 1186 |
+
.activity-status small {
|
| 1187 |
+
overflow: hidden;
|
| 1188 |
+
text-overflow: ellipsis;
|
| 1189 |
+
white-space: nowrap;
|
|
|
|
| 1190 |
}
|
| 1191 |
|
| 1192 |
+
.activity-status strong {
|
| 1193 |
color: var(--paper);
|
| 1194 |
font-weight: 600;
|
| 1195 |
}
|
| 1196 |
|
| 1197 |
+
.activity-status small {
|
| 1198 |
+
color: var(--paper-ghost);
|
| 1199 |
+
font-size: var(--type-utility);
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
.activity-status > b {
|
| 1203 |
+
flex: 0 0 auto;
|
| 1204 |
+
color: var(--phosphor);
|
| 1205 |
+
font: 600 var(--type-caption)/1 var(--mono);
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
.activity-pulse {
|
| 1209 |
+
width: 5px;
|
| 1210 |
+
height: 22px;
|
| 1211 |
+
flex: 0 0 5px;
|
| 1212 |
+
border-radius: 2px;
|
| 1213 |
+
background: var(--phosphor);
|
| 1214 |
+
}
|
| 1215 |
+
|
| 1216 |
+
.activity-track {
|
| 1217 |
+
height: 6px;
|
| 1218 |
overflow: hidden;
|
| 1219 |
+
border-radius: 3px;
|
| 1220 |
+
background: rgba(241, 242, 237, 0.12);
|
| 1221 |
+
}
|
| 1222 |
+
|
| 1223 |
+
.activity-track > span {
|
| 1224 |
+
display: block;
|
| 1225 |
+
height: 100%;
|
| 1226 |
+
border-radius: inherit;
|
| 1227 |
+
background: var(--phosphor);
|
| 1228 |
+
transition: width 120ms linear;
|
| 1229 |
}
|
| 1230 |
|
| 1231 |
.composer-meta {
|
|
|
|
| 1249 |
border: 0;
|
| 1250 |
background: transparent;
|
| 1251 |
font: 700 var(--type-utility)/1 var(--mono);
|
|
|
|
| 1252 |
cursor: pointer;
|
| 1253 |
}
|
| 1254 |
|
|
|
|
| 1264 |
border: 0;
|
| 1265 |
background: transparent;
|
| 1266 |
font: 700 var(--type-utility)/1 var(--mono);
|
|
|
|
| 1267 |
cursor: pointer;
|
| 1268 |
}
|
| 1269 |
|
|
|
|
| 1284 |
grid-template-columns: 1fr auto;
|
| 1285 |
position: relative;
|
| 1286 |
min-height: 84px;
|
| 1287 |
+
border: 1px solid var(--canvas-text);
|
| 1288 |
+
border-radius: 10px;
|
| 1289 |
+
background: var(--canvas-field);
|
| 1290 |
+
overflow: hidden;
|
| 1291 |
}
|
| 1292 |
|
| 1293 |
.composer-field label {
|
|
|
|
| 1296 |
left: 11px;
|
| 1297 |
color: rgba(17, 21, 18, 0.45);
|
| 1298 |
font: var(--type-utility)/1 var(--mono);
|
| 1299 |
+
letter-spacing: 0;
|
|
|
|
| 1300 |
}
|
| 1301 |
|
| 1302 |
.composer-field textarea {
|
| 1303 |
min-width: 0;
|
| 1304 |
resize: none;
|
| 1305 |
padding: 23px 13px 9px;
|
| 1306 |
+
color: var(--canvas-text);
|
| 1307 |
border: 0;
|
| 1308 |
outline: none;
|
| 1309 |
background: transparent;
|
|
|
|
| 1324 |
margin: 7px;
|
| 1325 |
color: var(--ink);
|
| 1326 |
border: 1px solid var(--ink);
|
| 1327 |
+
border-radius: 8px;
|
| 1328 |
background: var(--phosphor);
|
| 1329 |
font: 700 var(--type-caption)/1 var(--mono);
|
|
|
|
| 1330 |
cursor: pointer;
|
| 1331 |
+
transition: background 150ms ease, opacity 150ms ease;
|
| 1332 |
}
|
| 1333 |
|
| 1334 |
.send-button:hover:not(:disabled) {
|
| 1335 |
+
background: #b6df87;
|
|
|
|
| 1336 |
}
|
| 1337 |
|
| 1338 |
.send-button:disabled {
|
|
|
|
| 1361 |
background: var(--phosphor);
|
| 1362 |
}
|
| 1363 |
|
| 1364 |
+
.station-frame[data-theme="dark"] .botanical-subtitle,
|
| 1365 |
+
.station-frame[data-theme="dark"] .chat-header-meta,
|
| 1366 |
+
.station-frame[data-theme="dark"] .transcript-date,
|
| 1367 |
+
.station-frame[data-theme="dark"] .transcript-empty,
|
| 1368 |
+
.station-frame[data-theme="dark"] .message > header,
|
| 1369 |
+
.station-frame[data-theme="dark"] .message > footer,
|
| 1370 |
+
.station-frame[data-theme="dark"] .composer-meta,
|
| 1371 |
+
.station-frame[data-theme="dark"] .tool-toggle,
|
| 1372 |
+
.station-frame[data-theme="dark"] .regenerate-button,
|
| 1373 |
+
.station-frame[data-theme="dark"] .composer-field label {
|
| 1374 |
+
color: var(--canvas-muted);
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
.station-frame[data-theme="dark"] .transcript-empty h3,
|
| 1378 |
+
.station-frame[data-theme="dark"] .message > header strong {
|
| 1379 |
+
color: var(--canvas-text);
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
.station-frame[data-theme="dark"] .live-label {
|
| 1383 |
+
color: #a8d574;
|
| 1384 |
+
}
|
| 1385 |
+
|
| 1386 |
+
.station-frame[data-theme="dark"] .conversation-title-line small {
|
| 1387 |
+
color: var(--phosphor);
|
| 1388 |
+
}
|
| 1389 |
+
|
| 1390 |
+
.station-frame[data-theme="dark"] .live-label.is-dormant {
|
| 1391 |
+
color: var(--canvas-muted);
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
.station-frame[data-theme="dark"] .live-label.is-dormant i {
|
| 1395 |
+
background: var(--canvas-muted);
|
| 1396 |
+
}
|
| 1397 |
+
|
| 1398 |
+
.station-frame[data-theme="dark"] .transcript {
|
| 1399 |
+
scrollbar-color: rgba(237, 240, 232, 0.24) transparent;
|
| 1400 |
+
}
|
| 1401 |
+
|
| 1402 |
+
.station-frame[data-theme="dark"] .markdown-content blockquote,
|
| 1403 |
+
.station-frame[data-theme="dark"] .reasoning-block,
|
| 1404 |
+
.station-frame[data-theme="dark"] .tool-call > div p {
|
| 1405 |
+
color: rgba(237, 240, 232, 0.7);
|
| 1406 |
+
}
|
| 1407 |
+
|
| 1408 |
+
.station-frame[data-theme="dark"] .markdown-content code {
|
| 1409 |
+
background: rgba(237, 240, 232, 0.09);
|
| 1410 |
+
}
|
| 1411 |
+
|
| 1412 |
+
.station-frame[data-theme="dark"] .markdown-content pre {
|
| 1413 |
+
color: var(--paper);
|
| 1414 |
+
background: #0f110e;
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
.station-frame[data-theme="dark"] .markdown-content a,
|
| 1418 |
+
.station-frame[data-theme="dark"] .reasoning-block summary {
|
| 1419 |
+
color: var(--phosphor);
|
| 1420 |
+
}
|
| 1421 |
+
|
| 1422 |
+
.station-frame[data-theme="dark"] .reasoning-block,
|
| 1423 |
+
.station-frame[data-theme="dark"] .tool-call {
|
| 1424 |
+
border-color: var(--canvas-line);
|
| 1425 |
+
background: rgba(237, 240, 232, 0.035);
|
| 1426 |
+
}
|
| 1427 |
+
|
| 1428 |
+
.station-frame[data-theme="dark"] .generation-placeholder,
|
| 1429 |
+
.station-frame[data-theme="dark"] .tool-call summary > span:last-child,
|
| 1430 |
+
.station-frame[data-theme="dark"] .composer-field textarea::placeholder {
|
| 1431 |
+
color: rgba(237, 240, 232, 0.4);
|
| 1432 |
+
}
|
| 1433 |
+
|
| 1434 |
+
.station-frame[data-theme="dark"] .message-assistant {
|
| 1435 |
+
border-left-color: rgba(168, 213, 116, 0.44);
|
| 1436 |
+
}
|
| 1437 |
+
|
| 1438 |
+
.station-frame[data-theme="dark"] .message-user {
|
| 1439 |
+
color: var(--canvas-text);
|
| 1440 |
+
border: 1px solid var(--canvas-line);
|
| 1441 |
+
background: #11130f;
|
| 1442 |
+
}
|
| 1443 |
+
|
| 1444 |
+
.station-frame[data-theme="dark"] .message > footer,
|
| 1445 |
+
.station-frame[data-theme="dark"] .tool-call > div {
|
| 1446 |
+
border-color: var(--canvas-line);
|
| 1447 |
+
}
|
| 1448 |
+
|
| 1449 |
+
.station-frame[data-theme="dark"] .tool-toggle.is-on {
|
| 1450 |
+
color: var(--phosphor);
|
| 1451 |
+
}
|
| 1452 |
+
|
| 1453 |
+
.station-frame[data-theme="dark"] .composer-field textarea {
|
| 1454 |
+
color: var(--canvas-text);
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
.station-frame[data-theme="dark"] .composer-field {
|
| 1458 |
+
border-color: rgba(237, 240, 232, 0.3);
|
| 1459 |
+
}
|
| 1460 |
+
|
| 1461 |
+
.station-frame[data-theme="dark"] .send-button {
|
| 1462 |
+
color: var(--ink);
|
| 1463 |
+
border-color: #0f110e;
|
| 1464 |
+
}
|
| 1465 |
+
|
| 1466 |
+
.station-frame[data-theme="dark"] .send-button.stop {
|
| 1467 |
+
color: var(--canvas-text);
|
| 1468 |
+
border-color: var(--canvas-line);
|
| 1469 |
+
background: #11130f;
|
| 1470 |
+
}
|
| 1471 |
+
|
| 1472 |
.inspector {
|
| 1473 |
grid-area: inspector;
|
| 1474 |
+
min-height: calc(100dvh - 64px);
|
| 1475 |
+
height: calc(100dvh - 64px);
|
| 1476 |
overflow-y: auto;
|
| 1477 |
border-left: 1px solid var(--line-light);
|
| 1478 |
scrollbar-color: rgba(232, 223, 202, 0.18) transparent;
|
|
|
|
| 1479 |
}
|
| 1480 |
|
| 1481 |
.inspector-section {
|
|
|
|
| 1486 |
.section-heading {
|
| 1487 |
align-items: center;
|
| 1488 |
margin-bottom: 17px;
|
| 1489 |
+
justify-content: space-between;
|
| 1490 |
}
|
| 1491 |
|
| 1492 |
.section-heading.compact {
|
|
|
|
| 1498 |
}
|
| 1499 |
|
| 1500 |
.section-heading h2 {
|
| 1501 |
+
font-size: 20px;
|
| 1502 |
}
|
| 1503 |
|
| 1504 |
.sampling-label {
|
| 1505 |
+
padding: 3px 6px;
|
| 1506 |
+
color: var(--paper-ghost);
|
| 1507 |
+
border: 1px solid var(--line-light);
|
| 1508 |
+
border-radius: 6px;
|
| 1509 |
+
font: var(--type-utility)/1 var(--body);
|
|
|
|
|
|
|
| 1510 |
}
|
| 1511 |
|
| 1512 |
+
.sampling-label.phase-prefill,
|
| 1513 |
+
.sampling-label.phase-decode {
|
| 1514 |
+
color: var(--phosphor);
|
| 1515 |
+
border-color: rgba(168, 213, 116, 0.35);
|
| 1516 |
}
|
| 1517 |
|
| 1518 |
.speed-gauge {
|
|
|
|
| 1527 |
height: 116px;
|
| 1528 |
display: grid;
|
| 1529 |
place-items: center;
|
| 1530 |
+
border-radius: 50%;
|
| 1531 |
background:
|
| 1532 |
+
radial-gradient(circle, var(--ink) 0 56%, transparent 57%),
|
| 1533 |
+
conic-gradient(var(--phosphor) 0 var(--gauge), rgba(241, 242, 237, 0.11) var(--gauge) 100%);
|
| 1534 |
+
box-shadow: inset 0 0 0 1px rgba(241, 242, 237, 0.08);
|
|
|
|
| 1535 |
}
|
| 1536 |
|
| 1537 |
.gauge-ring::after {
|
| 1538 |
+
display: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1539 |
}
|
| 1540 |
|
| 1541 |
.gauge-ring > div {
|
|
|
|
| 1543 |
z-index: 1;
|
| 1544 |
display: grid;
|
| 1545 |
justify-items: center;
|
| 1546 |
+
min-width: 0;
|
| 1547 |
}
|
| 1548 |
|
| 1549 |
.gauge-ring strong {
|
| 1550 |
color: var(--phosphor);
|
| 1551 |
+
font: 600 clamp(24px, 2vw, 29px)/1 var(--display);
|
| 1552 |
+
white-space: nowrap;
|
| 1553 |
}
|
| 1554 |
|
| 1555 |
.gauge-ring small {
|
| 1556 |
+
display: grid;
|
| 1557 |
+
justify-items: center;
|
| 1558 |
+
gap: 2px;
|
| 1559 |
margin-top: 3px;
|
| 1560 |
color: var(--paper-ghost);
|
| 1561 |
+
font: 600 10px/1 var(--mono);
|
| 1562 |
+
letter-spacing: 0.02em;
|
| 1563 |
text-transform: uppercase;
|
| 1564 |
}
|
| 1565 |
|
| 1566 |
+
.gauge-ring small span {
|
| 1567 |
+
white-space: nowrap;
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
.speed-gauge dl {
|
| 1571 |
margin: 0;
|
| 1572 |
}
|
|
|
|
| 1588 |
color: var(--paper);
|
| 1589 |
}
|
| 1590 |
|
| 1591 |
+
.telemetry-live-grid {
|
| 1592 |
+
display: grid;
|
| 1593 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1594 |
+
gap: 8px;
|
| 1595 |
+
margin-top: 16px;
|
| 1596 |
+
}
|
| 1597 |
+
|
| 1598 |
+
.telemetry-metric {
|
| 1599 |
+
min-width: 0;
|
| 1600 |
+
display: grid;
|
| 1601 |
+
gap: 6px;
|
| 1602 |
+
padding: 10px;
|
| 1603 |
+
border: 1px solid var(--line-light);
|
| 1604 |
+
border-radius: 8px;
|
| 1605 |
+
background: var(--ink-raised);
|
| 1606 |
+
}
|
| 1607 |
+
|
| 1608 |
+
.telemetry-metric.is-active {
|
| 1609 |
+
border-color: rgba(168, 213, 116, 0.4);
|
| 1610 |
+
}
|
| 1611 |
+
|
| 1612 |
+
.telemetry-metric > div:first-child {
|
| 1613 |
+
display: flex;
|
| 1614 |
+
align-items: baseline;
|
| 1615 |
+
justify-content: space-between;
|
| 1616 |
+
gap: 8px;
|
| 1617 |
+
}
|
| 1618 |
+
|
| 1619 |
+
.telemetry-metric span,
|
| 1620 |
+
.telemetry-metric small {
|
| 1621 |
+
color: var(--paper-ghost);
|
| 1622 |
+
font-size: var(--type-utility);
|
| 1623 |
+
}
|
| 1624 |
+
|
| 1625 |
+
.telemetry-metric strong {
|
| 1626 |
+
color: var(--paper);
|
| 1627 |
+
font: 600 var(--type-caption)/1 var(--mono);
|
| 1628 |
+
white-space: nowrap;
|
| 1629 |
+
}
|
| 1630 |
+
|
| 1631 |
+
.telemetry-metric.is-active strong {
|
| 1632 |
+
color: var(--phosphor);
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
.telemetry-meter {
|
| 1636 |
+
height: 4px;
|
| 1637 |
+
overflow: hidden;
|
| 1638 |
+
border-radius: 2px;
|
| 1639 |
+
background: rgba(241, 242, 237, 0.1);
|
| 1640 |
+
}
|
| 1641 |
+
|
| 1642 |
+
.telemetry-meter > span {
|
| 1643 |
+
display: block;
|
| 1644 |
+
height: 100%;
|
| 1645 |
+
border-radius: inherit;
|
| 1646 |
+
background: var(--phosphor);
|
| 1647 |
+
transition: width 120ms linear;
|
| 1648 |
+
}
|
| 1649 |
+
|
| 1650 |
.device-card {
|
| 1651 |
display: grid;
|
| 1652 |
grid-template-columns: auto 1fr auto;
|
|
|
|
| 1654 |
gap: 10px;
|
| 1655 |
margin: 19px 0 14px;
|
| 1656 |
padding: 10px;
|
| 1657 |
+
border: 1px solid var(--line-light);
|
| 1658 |
+
border-radius: 8px;
|
| 1659 |
+
background: var(--ink-raised);
|
| 1660 |
}
|
| 1661 |
|
| 1662 |
.device-icon {
|
|
|
|
| 1728 |
.meter-track > span {
|
| 1729 |
display: block;
|
| 1730 |
height: 100%;
|
| 1731 |
+
background: var(--phosphor);
|
| 1732 |
}
|
| 1733 |
|
| 1734 |
.memory-grid {
|
|
|
|
| 1813 |
width: 25px;
|
| 1814 |
height: 25px;
|
| 1815 |
border: 1px solid var(--line-light);
|
| 1816 |
+
border-radius: 6px;
|
| 1817 |
background: transparent;
|
| 1818 |
cursor: pointer;
|
| 1819 |
}
|
| 1820 |
|
| 1821 |
.artifact-empty {
|
| 1822 |
margin: 0;
|
| 1823 |
+
color: var(--paper-ghost);
|
| 1824 |
+
font: var(--type-caption)/1.5 var(--body);
|
| 1825 |
}
|
| 1826 |
|
| 1827 |
.artifact-pane {
|
|
|
|
| 1938 |
display: flex;
|
| 1939 |
justify-content: flex-end;
|
| 1940 |
background: rgba(8, 11, 9, 0.64);
|
|
|
|
| 1941 |
animation: station-in 160ms ease both;
|
| 1942 |
}
|
| 1943 |
|
|
|
|
| 1948 |
padding: 30px 27px;
|
| 1949 |
color: var(--paper);
|
| 1950 |
border-left: 1px solid var(--phosphor);
|
| 1951 |
+
background: var(--ink);
|
|
|
|
|
|
|
| 1952 |
box-shadow: -24px 0 70px rgba(0, 0, 0, 0.32);
|
| 1953 |
animation: sheet-in 300ms cubic-bezier(.16, 1, .3, 1) both;
|
| 1954 |
}
|
|
|
|
| 1985 |
|
| 1986 |
.setting-group {
|
| 1987 |
display: grid;
|
| 1988 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 1989 |
align-items: center;
|
| 1990 |
gap: 12px;
|
| 1991 |
padding: 15px 0;
|
| 1992 |
border-bottom: 1px solid var(--line-light);
|
| 1993 |
}
|
| 1994 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1995 |
.setting-group > div,
|
| 1996 |
.range-setting > span:first-child {
|
| 1997 |
display: grid;
|
|
|
|
| 2231 |
|
| 2232 |
@media (max-width: 1279px) {
|
| 2233 |
.station-header {
|
| 2234 |
+
grid-template-columns: 240px minmax(260px, 1fr) auto;
|
| 2235 |
}
|
| 2236 |
|
| 2237 |
+
.browser-notice > span > span {
|
| 2238 |
+
display: none;
|
| 2239 |
}
|
| 2240 |
|
| 2241 |
+
.browser-notice.is-warning > span > span {
|
| 2242 |
+
display: block;
|
| 2243 |
}
|
| 2244 |
|
| 2245 |
.workspace-grid {
|
| 2246 |
+
grid-template-columns: 240px minmax(0, 1fr);
|
| 2247 |
grid-template-areas:
|
| 2248 |
"models chat"
|
| 2249 |
"inspector inspector";
|
| 2250 |
}
|
| 2251 |
|
| 2252 |
+
.workspace-grid.is-models-collapsed,
|
| 2253 |
+
.workspace-grid.is-models-collapsed.is-inspector-collapsed {
|
| 2254 |
+
grid-template-columns: 0 minmax(0, 1fr);
|
| 2255 |
+
}
|
| 2256 |
+
|
| 2257 |
+
.workspace-grid.is-inspector-collapsed:not(.is-models-collapsed) {
|
| 2258 |
+
grid-template-columns: 240px minmax(0, 1fr);
|
| 2259 |
+
}
|
| 2260 |
+
|
| 2261 |
.model-rail,
|
| 2262 |
.chat-surface {
|
| 2263 |
+
min-height: calc(100dvh - 64px);
|
| 2264 |
+
height: calc(100dvh - 64px);
|
| 2265 |
}
|
| 2266 |
|
| 2267 |
.inspector {
|
|
|
|
| 2270 |
display: grid;
|
| 2271 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 2272 |
overflow: visible;
|
| 2273 |
+
border-top: 1px solid var(--line-light);
|
| 2274 |
border-left: 0;
|
| 2275 |
}
|
| 2276 |
|
|
|
|
| 2282 |
|
| 2283 |
@media (max-width: 820px) {
|
| 2284 |
.station-header {
|
| 2285 |
+
min-height: 58px;
|
| 2286 |
+
display: grid;
|
| 2287 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 2288 |
}
|
| 2289 |
|
| 2290 |
.brand-block {
|
| 2291 |
+
min-width: 0;
|
| 2292 |
+
padding: 10px 14px;
|
| 2293 |
+
border-right: 0;
|
| 2294 |
}
|
| 2295 |
|
| 2296 |
+
.brand-glyph {
|
| 2297 |
+
width: 32px;
|
| 2298 |
+
height: 32px;
|
| 2299 |
+
flex-basis: 32px;
|
| 2300 |
}
|
| 2301 |
|
| 2302 |
+
.browser-notice {
|
| 2303 |
+
grid-column: 1 / -1;
|
| 2304 |
+
grid-row: 2;
|
| 2305 |
+
min-height: 38px;
|
| 2306 |
+
padding: 7px 14px;
|
| 2307 |
border-top: 1px solid var(--line-light);
|
| 2308 |
}
|
| 2309 |
|
| 2310 |
.header-actions {
|
| 2311 |
+
grid-column: 2;
|
| 2312 |
+
grid-row: 1;
|
| 2313 |
+
position: static;
|
| 2314 |
+
padding-inline: 10px;
|
| 2315 |
+
border-left: 0;
|
|
|
|
|
|
|
| 2316 |
}
|
| 2317 |
|
| 2318 |
.workspace-grid {
|
|
|
|
| 2330 |
display: block;
|
| 2331 |
padding: 18px 15px;
|
| 2332 |
border-right: 0;
|
| 2333 |
+
border-bottom: 1px solid var(--line-light);
|
| 2334 |
}
|
| 2335 |
|
| 2336 |
.model-list {
|
|
|
|
| 2352 |
|
| 2353 |
.chat-surface {
|
| 2354 |
display: grid;
|
| 2355 |
+
width: 100% !important;
|
| 2356 |
+
min-height: calc(100dvh - 96px);
|
| 2357 |
+
grid-template-rows: auto minmax(240px, auto) auto;
|
| 2358 |
overflow: visible;
|
| 2359 |
}
|
| 2360 |
|
|
|
|
| 2387 |
|
| 2388 |
.inspector {
|
| 2389 |
display: block;
|
| 2390 |
+
border-top: 1px solid var(--line-light);
|
| 2391 |
}
|
| 2392 |
|
| 2393 |
.inspector-section {
|
|
|
|
| 2398 |
|
| 2399 |
@media (max-width: 520px) {
|
| 2400 |
.brand-block h1 {
|
| 2401 |
+
font-size: 18px;
|
| 2402 |
+
}
|
| 2403 |
+
|
| 2404 |
+
.brand-block > div > span {
|
| 2405 |
+
display: none;
|
| 2406 |
+
}
|
| 2407 |
+
|
| 2408 |
+
.layout-controls {
|
| 2409 |
+
gap: 2px;
|
| 2410 |
}
|
| 2411 |
|
| 2412 |
+
.layout-controls button,
|
| 2413 |
+
.icon-button {
|
| 2414 |
+
width: 32px;
|
| 2415 |
+
height: 32px;
|
| 2416 |
}
|
| 2417 |
|
| 2418 |
.chat-header {
|
|
|
|
| 2420 |
}
|
| 2421 |
|
| 2422 |
.botanical-subtitle {
|
| 2423 |
+
display: none;
|
|
|
|
| 2424 |
}
|
| 2425 |
|
| 2426 |
.message-user {
|
|
|
|
| 2432 |
}
|
| 2433 |
|
| 2434 |
.composer-activity {
|
| 2435 |
+
padding-inline: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2436 |
}
|
| 2437 |
|
| 2438 |
+
.activity-status small {
|
| 2439 |
+
max-width: 210px;
|
| 2440 |
}
|
| 2441 |
|
| 2442 |
.composer-field {
|
|
|
|
| 2461 |
}
|
| 2462 |
|
| 2463 |
.gauge-ring::after {
|
| 2464 |
+
display: none;
|
| 2465 |
+
}
|
| 2466 |
+
|
| 2467 |
+
.telemetry-live-grid {
|
| 2468 |
+
grid-template-columns: 1fr;
|
| 2469 |
}
|
| 2470 |
}
|
| 2471 |
|
src/app/theme.test.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from 'vitest';
|
| 2 |
+
import { normalizeThemeMode, resolveThemeMode } from './theme';
|
| 3 |
+
|
| 4 |
+
describe('workspace theme', () => {
|
| 5 |
+
it('falls back to the system theme for missing or invalid persisted values', () => {
|
| 6 |
+
expect(normalizeThemeMode(null)).toBe('system');
|
| 7 |
+
expect(normalizeThemeMode('sepia')).toBe('system');
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
it('resolves system mode without overriding explicit light and dark choices', () => {
|
| 11 |
+
expect(resolveThemeMode('system', true)).toBe('dark');
|
| 12 |
+
expect(resolveThemeMode('system', false)).toBe('light');
|
| 13 |
+
expect(resolveThemeMode('light', true)).toBe('light');
|
| 14 |
+
expect(resolveThemeMode('dark', false)).toBe('dark');
|
| 15 |
+
});
|
| 16 |
+
});
|
src/app/theme.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type ThemeMode = 'system' | 'light' | 'dark';
|
| 2 |
+
export type ResolvedTheme = Exclude<ThemeMode, 'system'>;
|
| 3 |
+
|
| 4 |
+
export const THEME_STORAGE_KEY = 'bonsai-theme';
|
| 5 |
+
|
| 6 |
+
export function normalizeThemeMode(value: string | null | undefined): ThemeMode {
|
| 7 |
+
return value === 'light' || value === 'dark' || value === 'system' ? value : 'system';
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function resolveThemeMode(mode: ThemeMode, systemDark: boolean): ResolvedTheme {
|
| 11 |
+
return mode === 'system' ? (systemDark ? 'dark' : 'light') : mode;
|
| 12 |
+
}
|
src/lib/contracts.ts
CHANGED
|
@@ -50,6 +50,15 @@ export interface ChatMessage {
|
|
| 50 |
error?: string;
|
| 51 |
}
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
export interface TelemetrySnapshot {
|
| 54 |
backend: 'WebGPU' | 'WASM' | 'Unavailable';
|
| 55 |
device: string;
|
|
@@ -140,6 +149,9 @@ export interface BonsaiShellProps {
|
|
| 140 |
activeModelId: ModelId;
|
| 141 |
loadedModelId: ModelId | null;
|
| 142 |
messages: ChatMessage[];
|
|
|
|
|
|
|
|
|
|
| 143 |
streamState: StreamState;
|
| 144 |
modelLoadProgress: ModelLoadProgress;
|
| 145 |
telemetry: TelemetrySnapshot;
|
|
@@ -164,6 +176,11 @@ export interface BonsaiShellProps {
|
|
| 164 |
onPersistStorage: () => void;
|
| 165 |
onClearStorage: () => void;
|
| 166 |
onClearConversation: () => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
onDismissError: () => void;
|
| 168 |
onRetryShard: (modelId: ModelId) => void;
|
| 169 |
onCloseArtifact: () => void;
|
|
|
|
| 50 |
error?: string;
|
| 51 |
}
|
| 52 |
|
| 53 |
+
export interface ChatSession {
|
| 54 |
+
id: string;
|
| 55 |
+
title: string;
|
| 56 |
+
messages: ChatMessage[];
|
| 57 |
+
modelId: ModelId;
|
| 58 |
+
systemPrompt: string;
|
| 59 |
+
updatedAt: number;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
export interface TelemetrySnapshot {
|
| 63 |
backend: 'WebGPU' | 'WASM' | 'Unavailable';
|
| 64 |
device: string;
|
|
|
|
| 149 |
activeModelId: ModelId;
|
| 150 |
loadedModelId: ModelId | null;
|
| 151 |
messages: ChatMessage[];
|
| 152 |
+
conversations: ChatSession[];
|
| 153 |
+
activeConversationId: string;
|
| 154 |
+
conversationListOpen: boolean;
|
| 155 |
streamState: StreamState;
|
| 156 |
modelLoadProgress: ModelLoadProgress;
|
| 157 |
telemetry: TelemetrySnapshot;
|
|
|
|
| 176 |
onPersistStorage: () => void;
|
| 177 |
onClearStorage: () => void;
|
| 178 |
onClearConversation: () => void;
|
| 179 |
+
onOpenConversationList: () => void;
|
| 180 |
+
onNewConversation: () => void;
|
| 181 |
+
onOpenConversation: (conversationId: string) => void;
|
| 182 |
+
onOpenConversationSettings: (conversationId: string) => void;
|
| 183 |
+
onDeleteConversation: (conversationId: string) => void;
|
| 184 |
onDismissError: () => void;
|
| 185 |
onRetryShard: (modelId: ModelId) => void;
|
| 186 |
onCloseArtifact: () => void;
|
src/lib/conversations.test.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from 'vitest';
|
| 2 |
+
import type { ChatMessage, ChatSession } from './contracts';
|
| 3 |
+
import { conversationPreview, conversationTitle, normalizeConversationModelId, upsertConversation } from './conversations';
|
| 4 |
+
|
| 5 |
+
const message = (content: string, role: ChatMessage['role'] = 'user'): ChatMessage => ({
|
| 6 |
+
id: content,
|
| 7 |
+
role,
|
| 8 |
+
content,
|
| 9 |
+
timestamp: '12:00:00',
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
describe('local conversation sessions', () => {
|
| 13 |
+
it('derives a compact title from the first user prompt and a preview from the latest message', () => {
|
| 14 |
+
const messages = [message(' Explain recurrent models '), message('A compact answer', 'assistant')];
|
| 15 |
+
expect(conversationTitle(messages)).toBe('Explain recurrent models');
|
| 16 |
+
expect(conversationPreview(messages)).toBe('A compact answer');
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
it('falls back to the flagship model for stale persisted model ids', () => {
|
| 20 |
+
expect(normalizeConversationModelId('bonsai-8b')).toBe('bonsai-8b');
|
| 21 |
+
expect(normalizeConversationModelId('removed-model')).toBe('bonsai-27b');
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
it('moves an updated session to the front without duplicating it', () => {
|
| 25 |
+
const older: ChatSession = { id: 'a', title: 'A', messages: [], modelId: 'bonsai-27b', systemPrompt: 'A', updatedAt: 1 };
|
| 26 |
+
const other: ChatSession = { id: 'b', title: 'B', messages: [], modelId: 'bonsai-8b', systemPrompt: 'B', updatedAt: 2 };
|
| 27 |
+
const updated = { ...older, title: 'Updated', updatedAt: 3 };
|
| 28 |
+
expect(upsertConversation([other, older], updated).map(({ id, title }) => ({ id, title }))).toEqual([
|
| 29 |
+
{ id: 'a', title: 'Updated' },
|
| 30 |
+
{ id: 'b', title: 'B' },
|
| 31 |
+
]);
|
| 32 |
+
});
|
| 33 |
+
});
|
src/lib/conversations.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ChatMessage, ChatSession, ModelId } from './contracts';
|
| 2 |
+
|
| 3 |
+
const MODEL_IDS: readonly ModelId[] = ['bonsai-1.7b', 'bonsai-4b', 'bonsai-8b', 'bonsai-27b'];
|
| 4 |
+
|
| 5 |
+
export function conversationTitle(messages: ChatMessage[]): string {
|
| 6 |
+
const firstPrompt = messages.find((message) => message.role === 'user' && message.content.trim())?.content.trim();
|
| 7 |
+
return firstPrompt ? firstPrompt.replace(/\s+/g, ' ').slice(0, 80) : 'New chat';
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function conversationPreview(messages: ChatMessage[]): string {
|
| 11 |
+
const latest = [...messages].reverse().find((message) => message.content.trim())?.content.trim();
|
| 12 |
+
return latest ? latest.replace(/\s+/g, ' ').slice(0, 140) : 'No messages yet';
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function normalizeConversationModelId(value: string): ModelId {
|
| 16 |
+
return MODEL_IDS.includes(value as ModelId) ? value as ModelId : 'bonsai-27b';
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function upsertConversation(sessions: ChatSession[], next: ChatSession): ChatSession[] {
|
| 20 |
+
return [next, ...sessions.filter((session) => session.id !== next.id)]
|
| 21 |
+
.sort((left, right) => right.updatedAt - left.updatedAt);
|
| 22 |
+
}
|
src/lib/idb.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ConversationRecord<TMessage = unknown> {
|
|
| 14 |
title: string;
|
| 15 |
messages: TMessage[];
|
| 16 |
modelId: string;
|
|
|
|
| 17 |
updatedAt: number;
|
| 18 |
}
|
| 19 |
|
|
@@ -116,3 +117,11 @@ export async function listConversations<TMessage>(): Promise<ConversationRecord<
|
|
| 116 |
database.close();
|
| 117 |
return result.sort((left, right) => right.updatedAt - left.updatedAt);
|
| 118 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
title: string;
|
| 15 |
messages: TMessage[];
|
| 16 |
modelId: string;
|
| 17 |
+
systemPrompt?: string;
|
| 18 |
updatedAt: number;
|
| 19 |
}
|
| 20 |
|
|
|
|
| 117 |
database.close();
|
| 118 |
return result.sort((left, right) => right.updatedAt - left.updatedAt);
|
| 119 |
}
|
| 120 |
+
|
| 121 |
+
export async function deleteConversation(id: string): Promise<void> {
|
| 122 |
+
const database = await openDatabase();
|
| 123 |
+
const transaction = database.transaction(CONVERSATION_STORE, 'readwrite');
|
| 124 |
+
transaction.objectStore(CONVERSATION_STORE).delete(validateIdentifier(id, 'conversation id'));
|
| 125 |
+
await transactionDone(transaction);
|
| 126 |
+
database.close();
|
| 127 |
+
}
|