|
2 | 2 | import { onMount, tick, untrack } from 'svelte'; |
3 | 3 | import { listen } from '@tauri-apps/api/event'; |
4 | 4 | import { getCurrentWebview } from '@tauri-apps/api/webview'; |
5 | | - import { X, Check, PanelRight, ChevronDown, ChevronUp, Search } from 'lucide-svelte'; |
| 5 | + import { X, Check, PanelRight, ChevronDown, ChevronUp, Search, LoaderCircle } from 'lucide-svelte'; |
6 | 6 | import { open, ask, message } from '@tauri-apps/plugin-dialog'; |
7 | 7 | import { cycleTheme } from '$lib/theme.svelte'; |
8 | 8 | import { |
|
361 | 361 | return true; |
362 | 362 | } |
363 | 363 |
|
| 364 | + // Effort switch is debounced: reflect the pick immediately on the slider |
| 365 | + // (optimistic chat.effort) so the handle stays put, but only send the actual |
| 366 | + // `/model` command once the user settles — rapid drags/clicks don't race a |
| 367 | + // half-dozen switches through the engine. |
| 368 | + let effortTimer: ReturnType<typeof setTimeout> | undefined; |
364 | 369 | function chooseEffort(ef: string) { |
365 | | - if (chat) send({ op: 'command', input: `/model ${chat.model} ${ef}` }); |
| 370 | + if (!chat) return; |
| 371 | + chat.effort = ef; |
| 372 | + const model = chat.model; |
| 373 | + clearTimeout(effortTimer); |
| 374 | + effortTimer = setTimeout(() => { |
| 375 | + if (chat) send({ op: 'command', input: `/model ${model} ${ef}` }); |
| 376 | + }, 350); |
| 377 | + } |
| 378 | +
|
| 379 | + // Open the model picker as a popover. If we already have a cached catalog, |
| 380 | + // show it instantly and refresh in the background; otherwise fetch first. |
| 381 | + function openModelPicker() { |
| 382 | + if (!chat) return; |
| 383 | + if (chat.modelCatalog.length) { |
| 384 | + chat.picker = { |
| 385 | + kind: 'model', |
| 386 | + models: chat.modelCatalog, |
| 387 | + activeEffort: chat.modelCatalogEffort || chat.effort |
| 388 | + }; |
| 389 | + const act = chat.modelCatalog.findIndex((m) => m.active); |
| 390 | + selIdx = act >= 0 ? act : 0; |
| 391 | + } |
| 392 | + nav('/model'); |
366 | 393 | } |
367 | 394 |
|
368 | 395 | // The assistant message that's still streaming: render it as plain text and |
|
1119 | 1146 | <div bind:this={contentEl}> |
1120 | 1147 | <MessageList messages={chat.messages} {streamingMsg} {streamingReasoning} phase={chat.phase} compactionTokens={chat.compactionTokens} {findActive} {scroller} onEdit={editMessage} onRewind={rewindToMessage} /> |
1121 | 1148 | </div> |
1122 | | - {#if chat.messages.length === 0 && !chat.busy} |
| 1149 | + {#if chat.booting && chat.engineState !== 'exited'} |
| 1150 | + <div class="welcome spawning"> |
| 1151 | + <span class="spawn-spin"><LoaderCircle size={26} /></span> |
| 1152 | + <p class="welcome-tip">{t('shell.spawning')}</p> |
| 1153 | + </div> |
| 1154 | + {:else if chat.messages.length === 0 && !chat.busy} |
1123 | 1155 | <div class="welcome"> |
1124 | 1156 | <span class="welcome-mark">JuCode</span> |
1125 | 1157 | <p class="welcome-tip">{t('shell.welcomeTip')}</p> |
|
1167 | 1199 | onPick={pickFiles} |
1168 | 1200 | onScreenshot={screenshot} |
1169 | 1201 | onRecord={toggleRecord} |
1170 | | - onModel={() => nav('/model')} |
| 1202 | + onModel={openModelPicker} |
1171 | 1203 | onModelSelect={selectRow} |
1172 | 1204 | onModelEffort={setEffort} |
1173 | 1205 | onModelClose={() => chat?.closePicker()} |
|
1476 | 1508 | text-align: center; |
1477 | 1509 | animation: rise 0.3s ease both; |
1478 | 1510 | } |
| 1511 | + .spawn-spin { |
| 1512 | + display: inline-flex; |
| 1513 | + color: var(--accent); |
| 1514 | + animation: spawn-spin 0.8s linear infinite; |
| 1515 | + } |
| 1516 | + @keyframes spawn-spin { |
| 1517 | + to { |
| 1518 | + transform: rotate(360deg); |
| 1519 | + } |
| 1520 | + } |
1479 | 1521 | .welcome-mark { |
1480 | 1522 | font-family: var(--font-display); |
1481 | 1523 | font-weight: 800; |
|
0 commit comments