refactor: Icons
This commit is contained in:
parent
b1cf8bb814
commit
23a91cd257
|
|
@ -4,7 +4,7 @@
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
||||||
import { FILE_TYPE_ICONS } from '$lib/constants/modality-icons';
|
import { FILE_TYPE_ICONS } from '$lib/constants/icons';
|
||||||
import { FileTypeCategory } from '$lib/enums';
|
import { FileTypeCategory } from '$lib/enums';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Eye, Mic } from '@lucide/svelte';
|
|
||||||
import { ModelModality } from '$lib/enums';
|
import { ModelModality } from '$lib/enums';
|
||||||
|
import { MODALITY_ICONS, MODALITY_LABELS } from '$lib/constants/icons';
|
||||||
import { cn } from '$lib/components/ui/utils';
|
import { cn } from '$lib/components/ui/utils';
|
||||||
|
|
||||||
|
type DisplayableModality = ModelModality.VISION | ModelModality.AUDIO;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modalities: ModelModality[];
|
modalities: ModelModality[];
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
@ -10,31 +12,17 @@
|
||||||
|
|
||||||
let { modalities, class: className = '' }: Props = $props();
|
let { modalities, class: className = '' }: Props = $props();
|
||||||
|
|
||||||
function getModalityIcon(modality: ModelModality) {
|
// Filter to only modalities that have icons (VISION, AUDIO)
|
||||||
switch (modality) {
|
const displayableModalities = $derived(
|
||||||
case ModelModality.VISION:
|
modalities.filter(
|
||||||
return Eye;
|
(m): m is DisplayableModality => m === ModelModality.VISION || m === ModelModality.AUDIO
|
||||||
case ModelModality.AUDIO:
|
)
|
||||||
return Mic;
|
);
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModalityLabel(modality: ModelModality): string {
|
|
||||||
switch (modality) {
|
|
||||||
case ModelModality.VISION:
|
|
||||||
return 'Vision';
|
|
||||||
case ModelModality.AUDIO:
|
|
||||||
return 'Audio';
|
|
||||||
default:
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each modalities as modality, index (index)}
|
{#each displayableModalities as modality, index (index)}
|
||||||
{@const IconComponent = getModalityIcon(modality)}
|
{@const IconComponent = MODALITY_ICONS[modality]}
|
||||||
|
{@const label = MODALITY_LABELS[modality]}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={cn(
|
||||||
|
|
@ -46,6 +34,6 @@
|
||||||
<IconComponent class="h-3 w-3" />
|
<IconComponent class="h-3 w-3" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{getModalityLabel(modality)}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@
|
||||||
modelsUpdating,
|
modelsUpdating,
|
||||||
selectModel,
|
selectModel,
|
||||||
selectedModelId,
|
selectedModelId,
|
||||||
modelsStore,
|
unloadModel,
|
||||||
unloadModel
|
routerModels,
|
||||||
|
loadingModelIds,
|
||||||
|
loadModel
|
||||||
} from '$lib/stores/models.svelte';
|
} from '$lib/stores/models.svelte';
|
||||||
|
import { ServerModelStatus } from '$lib/enums';
|
||||||
import { isRouterMode, propsStore } from '$lib/stores/props.svelte';
|
import { isRouterMode, propsStore } from '$lib/stores/props.svelte';
|
||||||
import { DialogModelInformation } from '$lib/components/app';
|
import { DialogModelInformation } from '$lib/components/app';
|
||||||
import { MODALITY_ICONS } from '$lib/constants/modality-icons';
|
|
||||||
import type { ModelOption } from '$lib/types/models';
|
import type { ModelOption } from '$lib/types/models';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -44,6 +46,22 @@
|
||||||
let isRouter = $derived(isRouterMode());
|
let isRouter = $derived(isRouterMode());
|
||||||
let serverModel = $derived(propsStore.modelName);
|
let serverModel = $derived(propsStore.modelName);
|
||||||
|
|
||||||
|
// Reactive router models state - needed for proper reactivity of isModelLoaded checks
|
||||||
|
let currentRouterModels = $derived(routerModels());
|
||||||
|
let currentLoadingModelIds = $derived(loadingModelIds());
|
||||||
|
|
||||||
|
// Helper functions that create reactive dependencies
|
||||||
|
function checkIsModelLoaded(modelId: string): boolean {
|
||||||
|
// Access currentRouterModels to establish reactive dependency
|
||||||
|
const model = currentRouterModels.find((m) => m.name === modelId);
|
||||||
|
return model?.status?.value === ServerModelStatus.LOADED || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIsModelOperationInProgress(modelId: string): boolean {
|
||||||
|
// Access currentLoadingModelIds to establish reactive dependency
|
||||||
|
return currentLoadingModelIds.includes(modelId);
|
||||||
|
}
|
||||||
|
|
||||||
let isHighlightedCurrentModelActive = $derived(
|
let isHighlightedCurrentModelActive = $derived(
|
||||||
!isRouter || !currentModel
|
!isRouter || !currentModel
|
||||||
? false
|
? false
|
||||||
|
|
@ -240,16 +258,28 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelect(modelId: string) {
|
async function handleSelect(modelId: string) {
|
||||||
const option = options.find((opt) => opt.id === modelId);
|
const option = options.find((opt) => opt.id === modelId);
|
||||||
if (option && onModelChange) {
|
if (!option) return;
|
||||||
|
|
||||||
|
closeMenu();
|
||||||
|
|
||||||
|
if (onModelChange) {
|
||||||
// If callback provided, use it (for regenerate functionality)
|
// If callback provided, use it (for regenerate functionality)
|
||||||
onModelChange(option.id, option.model);
|
onModelChange(option.id, option.model);
|
||||||
} else if (option) {
|
} else {
|
||||||
// Otherwise, just update the global selection (for form selector)
|
// Update global selection
|
||||||
selectModel(option.id).catch(console.error);
|
await selectModel(option.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the model if not already loaded (router mode)
|
||||||
|
if (isRouter && !checkIsModelLoaded(option.model)) {
|
||||||
|
try {
|
||||||
|
await loadModel(option.model);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load model:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
closeMenu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayOption(): ModelOption | undefined {
|
function getDisplayOption(): ModelOption | undefined {
|
||||||
|
|
@ -382,10 +412,8 @@
|
||||||
<div class="my-1 h-px bg-border"></div>
|
<div class="my-1 h-px bg-border"></div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each options as option (option.id)}
|
{#each options as option (option.id)}
|
||||||
{@const isLoaded = modelsStore.isModelLoaded(option.model)}
|
{@const isLoaded = checkIsModelLoaded(option.model)}
|
||||||
{@const isUnloading = modelsStore.isModelOperationInProgress(option.model)}
|
{@const isUnloading = checkIsModelOperationInProgress(option.model)}
|
||||||
{@const hasVision = option.modalities?.vision ?? false}
|
|
||||||
{@const hasAudio = option.modalities?.audio ?? false}
|
|
||||||
{@const isSelected = currentModel === option.model || activeId === option.id}
|
{@const isSelected = currentModel === option.model || activeId === option.id}
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
|
|
@ -408,28 +436,13 @@
|
||||||
>
|
>
|
||||||
<span class="min-w-0 flex-1 truncate">{option.model}</span>
|
<span class="min-w-0 flex-1 truncate">{option.model}</span>
|
||||||
|
|
||||||
<!-- <div class="flex shrink-0 items-center gap-2"> -->
|
|
||||||
<MODALITY_ICONS.vision
|
|
||||||
class={cn(
|
|
||||||
'h-3.5 w-3.5',
|
|
||||||
hasVision ? 'text-foreground' : 'text-muted-foreground/40'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<MODALITY_ICONS.audio
|
|
||||||
class={cn(
|
|
||||||
'h-3.5 w-3.5',
|
|
||||||
hasAudio ? 'text-foreground' : 'text-muted-foreground/40'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<!-- </div> -->
|
|
||||||
|
|
||||||
{#if isUnloading}
|
{#if isUnloading}
|
||||||
<Loader2 class="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
|
<Loader2 class="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
|
||||||
{:else if isLoaded}
|
{:else if isLoaded}
|
||||||
<!-- Green dot, on hover show red unload button -->
|
<!-- Green dot, on hover show red unload button -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="relative flex h-4 w-4 shrink-0 items-center justify-center"
|
class="relative ml-2 flex h-4 w-4 shrink-0 items-center justify-center"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
unloadModel(option.model);
|
unloadModel(option.model);
|
||||||
|
|
@ -437,14 +450,15 @@
|
||||||
title="Unload model"
|
title="Unload model"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="h-2 w-2 rounded-full bg-green-500 transition-opacity group-hover:opacity-0"
|
class="mr-2 h-2 w-2 rounded-full bg-green-500 transition-opacity group-hover:opacity-0"
|
||||||
></span>
|
></span>
|
||||||
|
|
||||||
<Power
|
<Power
|
||||||
class="absolute h-4 w-4 text-red-500 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600"
|
class="absolute h-4 w-4 text-red-500 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="mr-1 h-2 w-2 shrink-0 rounded-full bg-muted-foreground/50"></span>
|
<span class="mx-2 h-2 w-2 shrink-0 rounded-full bg-muted-foreground/50"></span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ import {
|
||||||
File as FileIcon,
|
File as FileIcon,
|
||||||
FileText as FileTextIcon,
|
FileText as FileTextIcon,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
Volume2 as AudioIcon
|
Eye as VisionIcon,
|
||||||
|
Mic as AudioIcon
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { FileTypeCategory } from '$lib/enums';
|
import { FileTypeCategory, ModelModality } from '$lib/enums';
|
||||||
|
|
||||||
export const FILE_TYPE_ICONS = {
|
export const FILE_TYPE_ICONS = {
|
||||||
[FileTypeCategory.IMAGE]: ImageIcon,
|
[FileTypeCategory.IMAGE]: ImageIcon,
|
||||||
|
|
@ -20,9 +21,12 @@ export const FILE_TYPE_ICONS = {
|
||||||
|
|
||||||
export const DEFAULT_FILE_ICON = FileIcon;
|
export const DEFAULT_FILE_ICON = FileIcon;
|
||||||
|
|
||||||
export type ModelModality = 'vision' | 'audio';
|
|
||||||
|
|
||||||
export const MODALITY_ICONS = {
|
export const MODALITY_ICONS = {
|
||||||
vision: ImageIcon,
|
[ModelModality.VISION]: VisionIcon,
|
||||||
audio: AudioIcon
|
[ModelModality.AUDIO]: AudioIcon
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const MODALITY_LABELS = {
|
||||||
|
[ModelModality.VISION]: 'Vision',
|
||||||
|
[ModelModality.AUDIO]: 'Audio'
|
||||||
} as const;
|
} as const;
|
||||||
Loading…
Reference in New Issue