mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-13 00:10:37 +02:00
* chore: `npm audit fix --force` * feat: Update sidebar toggle to use Logo * refactor: Clean up favicon SVG * feat: Refactor logo component and implement theme-aware favicon generation * feat: Add configurable padding to generated PWA assets * test: Add unit tests for writeThemeFavicons * refactor: Componentization * feat: WIP * feat: WIP * feat: WIP * feat: Mobile UI * feat: add SEARCH route constant * feat: create SidebarNavigationSearchResults component * refactor: use SidebarNavigationSearchResults in conversation list * feat: enable mobile search navigation in sidebar actions * feat: add mobile search route and page * fix: prevent sidebar overflow on mobile viewports * fix: Mobile sidebar * feat: Mobile Search WIP * feat: Mobile WIP * feat: Add PWA standalone detection and refine mobile UI * feat: Improve mobile layout, sidebar handling, and chat scrolling * feat: Improve mobile sidebar visibility and iOS Safari chat spacing * fix: Disable auto-scroll on mobile * chore: Linting * fix: Wrong condition * feat: Mobile chat scroll * refactor: WIP * fix: Desktop initial scroll always working again * fix: Partial fix for mobile auto-scroll / initial scroll * fix: Desktop auto-scroll on initial load and during streaming * fix: Mobile scrolling logic * refactor: Clean up * feat: Improve start UI * feat: Add `delay` to `fadeInView` * feat: Auto-scroll button * refactor: Cleanup * refactor: Extract chat dialogs and alerts into dedicated component * refactor: Reorganize ChatScreen component structure and initialization * feat: Improve auto-scroll after sending message * feat: UI improvements * fix: Settings link * feat: UI improvements * fix: better UI spacing * fix: Remove unneeded logic * fix: Chat Processing Info UI rendering * feat: Improve mobile UI * feat: UI improvement * fix: Conditional transition delay for Chat Messages based on route from * fix: Delay mobile sidebar collapse for smoother transitions * fix: Mobile scroll down button + sidebar pointer events * fix: Mobile UI * fix: Auto scrolling * fix: Implement dynamic height calculations for chat auto-scroll positioning and UI elements * fix: Retrieve `autofocus` for Chat Form textarea * fix: Use proper class Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * refactor: extract scroll-to-bottom logic and fix message send flow * fix: update viewport store usage and remove conflicting autofocus * feat: add accessibility labels to scroll down button * fix: correct HTML structure in sidebar empty states * fix: dynamically toggle processing info visibility * chore: remove commented exports and fix formatting * fix * fix: Mobile Chat Form Add Action Sheet interactions --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
274 lines
6.4 KiB
TypeScript
274 lines
6.4 KiB
TypeScript
import { onMount } from 'svelte';
|
|
import {
|
|
modelsStore,
|
|
modelOptions,
|
|
modelsLoading,
|
|
modelsUpdating,
|
|
selectedModelId,
|
|
singleModelName
|
|
} from '$lib/stores/models.svelte';
|
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
|
import { filterModelOptions, groupModelOptions } from '$lib/components/app/models/utils';
|
|
import type { ModelOption } from '$lib/types/models';
|
|
|
|
export interface UseModelsSelectorOptions {
|
|
currentModel: () => string | null;
|
|
useGlobalSelection?: () => boolean;
|
|
onModelChange?: () =>
|
|
| ((modelId: string, modelName: string) => Promise<boolean> | boolean | void)
|
|
| undefined;
|
|
onOpenChange?: (open: boolean) => void;
|
|
}
|
|
|
|
export interface UseModelsSelectorReturn {
|
|
readonly options: ModelOption[];
|
|
readonly loading: boolean;
|
|
readonly updating: boolean;
|
|
readonly activeId: string | null;
|
|
readonly isRouter: boolean;
|
|
readonly serverModel: string | null;
|
|
readonly isHighlightedCurrentModelActive: boolean;
|
|
readonly isCurrentModelInCache: boolean;
|
|
readonly filteredOptions: ModelOption[];
|
|
readonly groupedFilteredOptions: ReturnType<typeof groupModelOptions>;
|
|
readonly isLoadingModel: boolean;
|
|
readonly searchTerm: string;
|
|
readonly showModelDialog: boolean;
|
|
readonly infoModelId: string | null;
|
|
setSearchTerm(value: string): void;
|
|
setShowModelDialog(value: boolean): void;
|
|
handleInfoClick(modelName: string): void;
|
|
handleSelect(modelId: string): Promise<void>;
|
|
handleOpenChange(open: boolean): void;
|
|
isFavorite(model: string): boolean;
|
|
getDisplayOption(): ModelOption | undefined;
|
|
}
|
|
|
|
/**
|
|
* Shared reactive state and logic for model selection.
|
|
*
|
|
* Used by both the desktop dropdown (`ModelsSelectorDropdown`)
|
|
* and the mobile sheet (`ModelsSelectorSheet`) to avoid
|
|
* duplicating store derivations, selection handling, and model loading.
|
|
*/
|
|
export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSelectorReturn {
|
|
const options = $derived(
|
|
modelOptions().filter((option) => {
|
|
const modelProps = modelsStore.getModelProps(option.model);
|
|
|
|
return modelProps?.ui !== false;
|
|
})
|
|
);
|
|
const loading = $derived(modelsLoading());
|
|
const updating = $derived(modelsUpdating());
|
|
const activeId = $derived(selectedModelId());
|
|
const isRouter = $derived(isRouterMode());
|
|
const serverModel = $derived(singleModelName());
|
|
|
|
const currentModel = $derived(opts.currentModel());
|
|
const onModelChange = $derived(opts.onModelChange?.());
|
|
|
|
const isHighlightedCurrentModelActive = $derived.by(() => {
|
|
if (!isRouter || !currentModel) return false;
|
|
const currentOption = options.find((option) => option.model === currentModel);
|
|
return currentOption ? currentOption.id === activeId : false;
|
|
});
|
|
|
|
const isCurrentModelInCache = $derived.by(() => {
|
|
if (!isRouter || !currentModel) return true;
|
|
return options.some((option) => option.model === currentModel);
|
|
});
|
|
|
|
let isLoadingModel = $state(false);
|
|
let searchTerm = $state('');
|
|
let showModelDialog = $state(false);
|
|
let infoModelId = $state<string | null>(null);
|
|
const filteredOptions = $derived(filterModelOptions(options, searchTerm));
|
|
const groupedFilteredOptions = $derived(
|
|
groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) =>
|
|
modelsStore.isModelLoaded(m)
|
|
)
|
|
);
|
|
|
|
function handleInfoClick(modelName: string) {
|
|
infoModelId = modelName;
|
|
showModelDialog = true;
|
|
}
|
|
|
|
onMount(() => {
|
|
modelsStore.fetch().catch((error) => {
|
|
console.error('Unable to load models:', error);
|
|
});
|
|
});
|
|
|
|
function handleOpenChange(open: boolean) {
|
|
if (loading || updating) return;
|
|
|
|
if (isRouter) {
|
|
searchTerm = '';
|
|
|
|
if (open) {
|
|
modelsStore.fetchRouterModels().then(() => {
|
|
modelsStore.fetchModalitiesForLoadedModels();
|
|
});
|
|
}
|
|
|
|
opts.onOpenChange?.(open);
|
|
} else {
|
|
showModelDialog = open;
|
|
}
|
|
}
|
|
|
|
async function handleSelect(modelId: string) {
|
|
const option = options.find((opt) => opt.id === modelId);
|
|
if (!option) return;
|
|
|
|
let shouldCloseMenu = true;
|
|
|
|
if (onModelChange) {
|
|
const result = await onModelChange(option.id, option.model);
|
|
|
|
if (result === false) {
|
|
shouldCloseMenu = false;
|
|
}
|
|
} else {
|
|
await modelsStore.selectModelById(option.id);
|
|
}
|
|
|
|
if (shouldCloseMenu) {
|
|
handleOpenChange(false);
|
|
|
|
requestAnimationFrame(() => {
|
|
const textarea = document.querySelector<HTMLTextAreaElement>(
|
|
'[data-slot="chat-form"] textarea'
|
|
);
|
|
|
|
textarea?.focus({ preventScroll: true });
|
|
});
|
|
}
|
|
|
|
if (!onModelChange && isRouter && !modelsStore.isModelLoaded(option.model)) {
|
|
isLoadingModel = true;
|
|
|
|
modelsStore
|
|
.loadModel(option.model)
|
|
.catch((error) => console.error('Failed to load model:', error))
|
|
.finally(() => (isLoadingModel = false));
|
|
}
|
|
}
|
|
|
|
function getDisplayOption(): ModelOption | undefined {
|
|
if (!isRouter) {
|
|
const displayModel = serverModel || currentModel;
|
|
|
|
if (displayModel) {
|
|
return {
|
|
id: serverModel ? 'current' : 'offline-current',
|
|
model: displayModel,
|
|
name: displayModel.split('/').pop() || displayModel,
|
|
capabilities: []
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
if (currentModel) {
|
|
if (!isCurrentModelInCache) {
|
|
return {
|
|
id: 'not-in-cache',
|
|
model: currentModel,
|
|
name: currentModel.split('/').pop() || currentModel,
|
|
capabilities: []
|
|
};
|
|
}
|
|
|
|
return options.find((option) => option.model === currentModel);
|
|
}
|
|
|
|
if (activeId) {
|
|
return options.find((option) => option.id === activeId);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
get options() {
|
|
return options;
|
|
},
|
|
|
|
get loading() {
|
|
return loading;
|
|
},
|
|
|
|
get updating() {
|
|
return updating;
|
|
},
|
|
|
|
get activeId() {
|
|
return activeId;
|
|
},
|
|
|
|
get isRouter() {
|
|
return isRouter;
|
|
},
|
|
|
|
get serverModel() {
|
|
return serverModel;
|
|
},
|
|
|
|
get isHighlightedCurrentModelActive() {
|
|
return isHighlightedCurrentModelActive;
|
|
},
|
|
|
|
get isCurrentModelInCache() {
|
|
return isCurrentModelInCache;
|
|
},
|
|
|
|
get filteredOptions() {
|
|
return filteredOptions;
|
|
},
|
|
|
|
get groupedFilteredOptions() {
|
|
return groupedFilteredOptions;
|
|
},
|
|
|
|
get isLoadingModel() {
|
|
return isLoadingModel;
|
|
},
|
|
|
|
get searchTerm() {
|
|
return searchTerm;
|
|
},
|
|
|
|
get showModelDialog() {
|
|
return showModelDialog;
|
|
},
|
|
|
|
get infoModelId() {
|
|
return infoModelId;
|
|
},
|
|
|
|
setSearchTerm(value: string) {
|
|
searchTerm = value;
|
|
},
|
|
|
|
setShowModelDialog(value: boolean) {
|
|
showModelDialog = value;
|
|
},
|
|
|
|
handleInfoClick,
|
|
|
|
handleSelect,
|
|
|
|
handleOpenChange,
|
|
|
|
isFavorite(model: string) {
|
|
return modelsStore.favoriteModelIds.has(model);
|
|
},
|
|
|
|
getDisplayOption
|
|
};
|
|
}
|