|
173 | 173 | let isOpen = $state(false); |
174 | 174 | let showModelDialog = $state(false); |
175 | 175 |
|
176 | | - onMount(async () => { |
177 | | - try { |
178 | | - await modelsStore.fetch(); |
179 | | - } catch (error) { |
| 176 | + onMount(() => { |
| 177 | + modelsStore.fetch().catch((error) => { |
180 | 178 | console.error('Unable to load models:', error); |
181 | | - } |
| 179 | + }); |
182 | 180 | }); |
183 | 181 |
|
184 | 182 | function handleOpenChange(open: boolean) { |
|
394 | 392 |
|
395 | 393 | <Popover.Content |
396 | 394 | class="w-96 max-w-[calc(100vw-2rem)] p-0" |
397 | | - side="top" |
398 | 395 | align="end" |
399 | 396 | sideOffset={8} |
| 397 | + collisionPadding={16} |
400 | 398 | > |
401 | | - <div class="p-4"> |
402 | | - <SearchInput |
403 | | - id="model-search" |
404 | | - placeholder="Search models..." |
405 | | - bind:value={searchTerm} |
406 | | - bind:ref={searchInputRef} |
407 | | - onClose={closeMenu} |
408 | | - onKeyDown={handleSearchKeyDown} |
409 | | - /> |
410 | | - </div> |
411 | | - <div class="max-h-80 overflow-y-auto"> |
412 | | - {#if !isCurrentModelInCache() && currentModel} |
413 | | - <!-- Show unavailable model as first option (disabled) --> |
414 | | - <button |
415 | | - type="button" |
416 | | - class="flex w-full cursor-not-allowed items-center bg-red-400/10 px-4 py-2 text-left text-sm text-red-400" |
417 | | - role="option" |
418 | | - aria-selected="true" |
419 | | - aria-disabled="true" |
420 | | - disabled |
421 | | - > |
422 | | - <span class="truncate">{selectedOption?.name || currentModel}</span> |
423 | | - <span class="ml-2 text-xs whitespace-nowrap opacity-70">(not available)</span> |
424 | | - </button> |
425 | | - <div class="my-1 h-px bg-border"></div> |
426 | | - {/if} |
427 | | - {#if filteredOptions.length === 0} |
428 | | - <p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p> |
429 | | - {/if} |
430 | | - {#each filteredOptions as option, index (option.id)} |
431 | | - {@const status = getModelStatus(option.model)} |
432 | | - {@const isLoaded = status === ServerModelStatus.LOADED} |
433 | | - {@const isLoading = status === ServerModelStatus.LOADING} |
434 | | - {@const isSelected = currentModel === option.model || activeId === option.id} |
435 | | - {@const isCompatible = isModelCompatible(option)} |
436 | | - {@const isHighlighted = index === highlightedIndex} |
437 | | - {@const missingModalities = getMissingModalities(option)} |
438 | | - |
439 | | - <div |
440 | | - class={cn( |
441 | | - 'group flex w-full items-center gap-2 px-4 py-2 text-left text-sm transition focus:outline-none', |
442 | | - isCompatible |
443 | | - ? 'cursor-pointer hover:bg-muted focus:bg-muted' |
444 | | - : 'cursor-not-allowed opacity-50', |
445 | | - isSelected || isHighlighted |
446 | | - ? 'bg-accent text-accent-foreground' |
447 | | - : isCompatible |
448 | | - ? 'hover:bg-accent hover:text-accent-foreground' |
449 | | - : '', |
450 | | - isLoaded ? 'text-popover-foreground' : 'text-muted-foreground' |
451 | | - )} |
452 | | - role="option" |
453 | | - aria-selected={isSelected || isHighlighted} |
454 | | - aria-disabled={!isCompatible} |
455 | | - tabindex={isCompatible ? 0 : -1} |
456 | | - onclick={() => isCompatible && handleSelect(option.id)} |
457 | | - onmouseenter={() => (highlightedIndex = index)} |
458 | | - onkeydown={(e) => { |
459 | | - if (isCompatible && (e.key === 'Enter' || e.key === ' ')) { |
460 | | - e.preventDefault(); |
461 | | - handleSelect(option.id); |
462 | | - } |
463 | | - }} |
464 | | - > |
465 | | - <span class="min-w-0 flex-1 truncate">{option.model}</span> |
466 | | - |
467 | | - {#if missingModalities} |
468 | | - <span class="flex shrink-0 items-center gap-1 text-muted-foreground/70"> |
469 | | - {#if missingModalities.vision} |
470 | | - <Tooltip.Root> |
471 | | - <Tooltip.Trigger> |
472 | | - <EyeOff class="h-3.5 w-3.5" /> |
473 | | - </Tooltip.Trigger> |
474 | | - <Tooltip.Content class="z-[9999]"> |
475 | | - <p>No vision support</p> |
476 | | - </Tooltip.Content> |
477 | | - </Tooltip.Root> |
478 | | - {/if} |
479 | | - {#if missingModalities.audio} |
480 | | - <Tooltip.Root> |
481 | | - <Tooltip.Trigger> |
482 | | - <MicOff class="h-3.5 w-3.5" /> |
483 | | - </Tooltip.Trigger> |
484 | | - <Tooltip.Content class="z-[9999]"> |
485 | | - <p>No audio support</p> |
486 | | - </Tooltip.Content> |
487 | | - </Tooltip.Root> |
488 | | - {/if} |
489 | | - </span> |
490 | | - {/if} |
491 | | - |
492 | | - {#if isLoading} |
493 | | - <Tooltip.Root> |
494 | | - <Tooltip.Trigger> |
495 | | - <Loader2 class="h-4 w-4 shrink-0 animate-spin text-muted-foreground" /> |
496 | | - </Tooltip.Trigger> |
497 | | - <Tooltip.Content class="z-[9999]"> |
498 | | - <p>Loading model...</p> |
499 | | - </Tooltip.Content> |
500 | | - </Tooltip.Root> |
501 | | - {:else if isLoaded} |
502 | | - <Tooltip.Root> |
503 | | - <Tooltip.Trigger> |
504 | | - <button |
505 | | - type="button" |
506 | | - class="relative ml-2 flex h-4 w-4 shrink-0 items-center justify-center" |
507 | | - onclick={(e) => { |
508 | | - e.stopPropagation(); |
509 | | - modelsStore.unloadModel(option.model); |
510 | | - }} |
511 | | - > |
512 | | - <span |
513 | | - class="mr-2 h-2 w-2 rounded-full bg-green-500 transition-opacity group-hover:opacity-0" |
514 | | - ></span> |
515 | | - <Power |
516 | | - class="absolute mr-2 h-4 w-4 text-red-500 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600" |
517 | | - /> |
518 | | - </button> |
519 | | - </Tooltip.Trigger> |
520 | | - <Tooltip.Content class="z-[9999]"> |
521 | | - <p>Unload model</p> |
522 | | - </Tooltip.Content> |
523 | | - </Tooltip.Root> |
524 | | - {:else} |
525 | | - <span class="mx-2 h-2 w-2 rounded-full bg-muted-foreground/50"></span> |
526 | | - {/if} |
527 | | - </div> |
528 | | - {/each} |
| 399 | + <div class="flex max-h-[50dvh] flex-col overflow-hidden"> |
| 400 | + <div class="shrink-0 p-4"> |
| 401 | + <SearchInput |
| 402 | + id="model-search" |
| 403 | + placeholder="Search models..." |
| 404 | + bind:value={searchTerm} |
| 405 | + bind:ref={searchInputRef} |
| 406 | + onClose={closeMenu} |
| 407 | + onKeyDown={handleSearchKeyDown} |
| 408 | + /> |
| 409 | + </div> |
| 410 | + <div class="min-h-0 flex-1 overflow-y-auto"> |
| 411 | + {#if !isCurrentModelInCache() && currentModel} |
| 412 | + <!-- Show unavailable model as first option (disabled) --> |
| 413 | + <button |
| 414 | + type="button" |
| 415 | + class="flex w-full cursor-not-allowed items-center bg-red-400/10 px-4 py-2 text-left text-sm text-red-400" |
| 416 | + role="option" |
| 417 | + aria-selected="true" |
| 418 | + aria-disabled="true" |
| 419 | + disabled |
| 420 | + > |
| 421 | + <span class="truncate">{selectedOption?.name || currentModel}</span> |
| 422 | + <span class="ml-2 text-xs whitespace-nowrap opacity-70">(not available)</span> |
| 423 | + </button> |
| 424 | + <div class="my-1 h-px bg-border"></div> |
| 425 | + {/if} |
| 426 | + {#if filteredOptions.length === 0} |
| 427 | + <p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p> |
| 428 | + {/if} |
| 429 | + {#each filteredOptions as option, index (option.id)} |
| 430 | + {@const status = getModelStatus(option.model)} |
| 431 | + {@const isLoaded = status === ServerModelStatus.LOADED} |
| 432 | + {@const isLoading = status === ServerModelStatus.LOADING} |
| 433 | + {@const isSelected = currentModel === option.model || activeId === option.id} |
| 434 | + {@const isCompatible = isModelCompatible(option)} |
| 435 | + {@const isHighlighted = index === highlightedIndex} |
| 436 | + {@const missingModalities = getMissingModalities(option)} |
| 437 | + |
| 438 | + <div |
| 439 | + class={cn( |
| 440 | + 'group flex w-full items-center gap-2 px-4 py-2 text-left text-sm transition focus:outline-none', |
| 441 | + isCompatible |
| 442 | + ? 'cursor-pointer hover:bg-muted focus:bg-muted' |
| 443 | + : 'cursor-not-allowed opacity-50', |
| 444 | + isSelected || isHighlighted |
| 445 | + ? 'bg-accent text-accent-foreground' |
| 446 | + : isCompatible |
| 447 | + ? 'hover:bg-accent hover:text-accent-foreground' |
| 448 | + : '', |
| 449 | + isLoaded ? 'text-popover-foreground' : 'text-muted-foreground' |
| 450 | + )} |
| 451 | + role="option" |
| 452 | + aria-selected={isSelected || isHighlighted} |
| 453 | + aria-disabled={!isCompatible} |
| 454 | + tabindex={isCompatible ? 0 : -1} |
| 455 | + onclick={() => isCompatible && handleSelect(option.id)} |
| 456 | + onmouseenter={() => (highlightedIndex = index)} |
| 457 | + onkeydown={(e) => { |
| 458 | + if (isCompatible && (e.key === 'Enter' || e.key === ' ')) { |
| 459 | + e.preventDefault(); |
| 460 | + handleSelect(option.id); |
| 461 | + } |
| 462 | + }} |
| 463 | + > |
| 464 | + <span class="min-w-0 flex-1 truncate">{option.model}</span> |
| 465 | + |
| 466 | + {#if missingModalities} |
| 467 | + <span class="flex shrink-0 items-center gap-1 text-muted-foreground/70"> |
| 468 | + {#if missingModalities.vision} |
| 469 | + <Tooltip.Root> |
| 470 | + <Tooltip.Trigger> |
| 471 | + <EyeOff class="h-3.5 w-3.5" /> |
| 472 | + </Tooltip.Trigger> |
| 473 | + <Tooltip.Content class="z-[9999]"> |
| 474 | + <p>No vision support</p> |
| 475 | + </Tooltip.Content> |
| 476 | + </Tooltip.Root> |
| 477 | + {/if} |
| 478 | + {#if missingModalities.audio} |
| 479 | + <Tooltip.Root> |
| 480 | + <Tooltip.Trigger> |
| 481 | + <MicOff class="h-3.5 w-3.5" /> |
| 482 | + </Tooltip.Trigger> |
| 483 | + <Tooltip.Content class="z-[9999]"> |
| 484 | + <p>No audio support</p> |
| 485 | + </Tooltip.Content> |
| 486 | + </Tooltip.Root> |
| 487 | + {/if} |
| 488 | + </span> |
| 489 | + {/if} |
| 490 | + |
| 491 | + {#if isLoading} |
| 492 | + <Tooltip.Root> |
| 493 | + <Tooltip.Trigger> |
| 494 | + <Loader2 class="h-4 w-4 shrink-0 animate-spin text-muted-foreground" /> |
| 495 | + </Tooltip.Trigger> |
| 496 | + <Tooltip.Content class="z-[9999]"> |
| 497 | + <p>Loading model...</p> |
| 498 | + </Tooltip.Content> |
| 499 | + </Tooltip.Root> |
| 500 | + {:else if isLoaded} |
| 501 | + <Tooltip.Root> |
| 502 | + <Tooltip.Trigger> |
| 503 | + <button |
| 504 | + type="button" |
| 505 | + class="relative ml-2 flex h-4 w-4 shrink-0 items-center justify-center" |
| 506 | + onclick={(e) => { |
| 507 | + e.stopPropagation(); |
| 508 | + modelsStore.unloadModel(option.model); |
| 509 | + }} |
| 510 | + > |
| 511 | + <span |
| 512 | + class="mr-2 h-2 w-2 rounded-full bg-green-500 transition-opacity group-hover:opacity-0" |
| 513 | + ></span> |
| 514 | + <Power |
| 515 | + class="absolute mr-2 h-4 w-4 text-red-500 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600" |
| 516 | + /> |
| 517 | + </button> |
| 518 | + </Tooltip.Trigger> |
| 519 | + <Tooltip.Content class="z-[9999]"> |
| 520 | + <p>Unload model</p> |
| 521 | + </Tooltip.Content> |
| 522 | + </Tooltip.Root> |
| 523 | + {:else} |
| 524 | + <span class="mx-2 h-2 w-2 rounded-full bg-muted-foreground/50"></span> |
| 525 | + {/if} |
| 526 | + </div> |
| 527 | + {/each} |
| 528 | + </div> |
529 | 529 | </div> |
530 | 530 | </Popover.Content> |
531 | 531 | </Popover.Root> |
|
0 commit comments