]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
04b85b24b235c993b61c3755ee0650b136ad5420
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts" generics="T">
2 import type { Snippet } from 'svelte';
3 import { SearchInput } from '$lib/components/app';
4 import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
5 import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
6 import { CHAT_FORM_POPOVER_MAX_HEIGHT } from '$lib/constants';
7
8 interface Props {
9 items: T[];
10 isLoading: boolean;
11 selectedIndex: number;
12 searchQuery: string;
13 showSearchInput: boolean;
14 searchPlaceholder?: string;
15 // Omit to distinguish "haven't searched yet" from "search returned nothing".
16 emptyMessage?: string;
17 autofocus?: boolean;
18 inputRef?: HTMLInputElement | null;
19 onSearchClose?: () => void;
20 itemKey: (item: T, index: number) => string;
21 item: Snippet<[T, number, boolean]>;
22 skeleton?: Snippet;
23 skeletonCount?: number;
24 footer?: Snippet;
25 // Counter bumped by the picker on keyboard nav; scrolls the selected
26 // row into view without scrolling on hover or result replacement.
27 scrollTrigger?: number;
28 }
29
30 let {
31 items,
32 isLoading,
33 selectedIndex,
34 searchQuery = $bindable(),
35 showSearchInput,
36 searchPlaceholder = 'Search...',
37 emptyMessage,
38 autofocus = false,
39 inputRef = $bindable(null),
40 onSearchClose,
41 itemKey,
42 item,
43 skeleton,
44 skeletonCount = 6,
45 footer,
46 scrollTrigger
47 }: Props = $props();
48
49 let listContainer = $state<HTMLDivElement | null>(null);
50
51 let listPaddingTop = $derived(
52 showSearchInput ? (isLoading || items.length > 0 ? 'pt-13' : 'pt-10') : ''
53 );
54
55 // selectedIndex/items.length are untracked so hover and result replacement
56 // never re-fire the scroll; keyboard nav is the only path that bumps the trigger.
57 useScrollActiveRow({
58 getTrigger: () => scrollTrigger,
59 getContainer: () => listContainer,
60 getIndex: () => selectedIndex,
61 getCount: () => items.length,
62 dataIndex: 'picker'
63 });
64 </script>
65
66 <ScrollArea>
67 {#if showSearchInput}
68 <div class="absolute top-0 right-0 left-0 z-10 p-2 pb-0">
69 <SearchInput
70 {autofocus}
71 placeholder={searchPlaceholder}
72 bind:value={searchQuery}
73 bind:ref={inputRef}
74 onClose={onSearchClose}
75 />
76 </div>
77 {/if}
78
79 <div bind:this={listContainer} class={[`${CHAT_FORM_POPOVER_MAX_HEIGHT} p-2`, listPaddingTop]}>
80 {#if isLoading}
81 {#if skeleton}
82 {@render skeleton()}
83 {:else}
84 <div aria-busy="true" aria-live="polite" class="flex flex-col">
85 {#each { length: skeletonCount } as _, rowIndex (rowIndex)}
86 <div class="flex items-start gap-3 rounded-lg px-3 py-2">
87 <div class="mt-0.5 size-4 shrink-0 animate-pulse rounded-md bg-muted/60"></div>
88 <div class="flex min-w-0 flex-1 flex-col">
89 <div class="h-5 w-2/5 animate-pulse rounded-sm bg-muted/60"></div>
90 <div class="h-4 w-1/3 animate-pulse rounded-sm bg-muted/40"></div>
91 </div>
92 </div>
93 {/each}
94 </div>
95 {/if}
96 {:else if items && items.length === 0}
97 {#if emptyMessage}
98 <div class="py-6 text-center text-sm text-muted-foreground">{emptyMessage}</div>
99 {/if}
100 {:else}
101 {#each items as itemData, index (itemKey(itemData, index))}
102 {@render item(itemData, index, index === selectedIndex)}
103 {/each}
104 {/if}
105 </div>
106
107 {#if footer}
108 {@render footer()}
109 {/if}
110 </ScrollArea>