Thomas G. Lopes
commited on
Commit
·
2087f3e
1
Parent(s):
9b4caaa
improve model selector experience
Browse files
src/lib/components/inference-playground/model-selector-modal.svelte
CHANGED
@@ -1,83 +1,87 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import type { Conversation } from "$lib/types.js";
|
3 |
|
4 |
-
import {
|
5 |
|
|
|
6 |
import { models } from "$lib/state/models.svelte.js";
|
7 |
import fuzzysearch from "$lib/utils/search.js";
|
|
|
8 |
import IconSearch from "~icons/carbon/search";
|
9 |
import IconStar from "~icons/carbon/star";
|
10 |
|
11 |
interface Props {
|
|
|
|
|
12 |
conversation: Conversation;
|
13 |
}
|
14 |
|
15 |
-
let { conversation }: Props = $props();
|
16 |
|
17 |
let backdropEl = $state<HTMLDivElement>();
|
18 |
-
let highlightIdx = $state(
|
19 |
let ignoreCursorHighlight = $state(false);
|
20 |
let containerEl = $state<HTMLDivElement>();
|
21 |
let query = $state("");
|
22 |
|
23 |
-
const
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
27 |
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
}
|
34 |
-
|
35 |
|
36 |
-
|
|
|
|
|
|
|
37 |
|
38 |
-
function handleKeydown(
|
39 |
-
|
40 |
-
|
41 |
-
if (key === "
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
event.preventDefault();
|
46 |
-
const highlightedEl = document.querySelector(".highlighted");
|
47 |
-
if (highlightedEl) {
|
48 |
-
(highlightedEl as HTMLButtonElement).click();
|
49 |
-
}
|
50 |
-
} else if (key === "ArrowUp") {
|
51 |
-
event.preventDefault();
|
52 |
-
highlightIdx--;
|
53 |
-
scrollLogicalPosition = "start";
|
54 |
ignoreCursorHighlight = true;
|
55 |
-
} else if (key === "ArrowDown") {
|
56 |
-
|
57 |
-
highlightIdx++;
|
58 |
ignoreCursorHighlight = true;
|
|
|
|
|
59 |
}
|
60 |
-
|
61 |
-
|
62 |
-
scrollToResult(
|
63 |
}
|
64 |
|
65 |
-
async function scrollToResult(
|
66 |
await tick();
|
67 |
-
const highlightedEl = document.querySelector("
|
68 |
-
|
69 |
-
const { bottom: containerBottom, top: containerTop } = containerEl.getBoundingClientRect();
|
70 |
-
const { bottom: highlightedBottom, top: highlightedTop } = highlightedEl.getBoundingClientRect();
|
71 |
-
if (highlightedBottom > containerBottom || containerTop > highlightedTop) {
|
72 |
-
highlightedEl.scrollIntoView({ block });
|
73 |
-
}
|
74 |
-
}
|
75 |
}
|
76 |
|
77 |
function highlightRow(idx: number) {
|
78 |
-
if (
|
79 |
-
|
80 |
-
}
|
81 |
}
|
82 |
|
83 |
function handleBackdropClick(event: MouseEvent) {
|
@@ -86,7 +90,7 @@
|
|
86 |
return;
|
87 |
}
|
88 |
if (event.target === backdropEl) {
|
89 |
-
|
90 |
}
|
91 |
}
|
92 |
</script>
|
@@ -109,70 +113,51 @@
|
|
109 |
<div class="mr-2 text-sm">
|
110 |
<IconSearch />
|
111 |
</div>
|
112 |
-
<!-- svelte-ignore a11y_autofocus -->
|
113 |
<input
|
114 |
-
autofocus
|
115 |
class="flex h-10 w-full rounded-md bg-transparent py-3 text-sm placeholder-gray-400 outline-hidden"
|
116 |
placeholder="Search models ..."
|
117 |
bind:value={query}
|
118 |
/>
|
119 |
</div>
|
120 |
<div class="max-h-[300px] overflow-x-hidden overflow-y-auto">
|
121 |
-
{#
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
</div>
|
|
|
|
|
|
|
149 |
{/if}
|
150 |
-
{#if
|
151 |
-
<div>
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
{@const [nameSpace, modelName] = model.id.split("/")}
|
156 |
-
{@const idx = featuredModels.length + _idx}
|
157 |
-
<button
|
158 |
-
class="flex w-full cursor-pointer items-center px-2 py-1.5 text-sm {highlightIdx === idx
|
159 |
-
? 'highlighted bg-gray-100 dark:bg-gray-800'
|
160 |
-
: ''}"
|
161 |
-
onmouseenter={() => highlightRow(idx)}
|
162 |
-
onclick={() => {
|
163 |
-
dispatch("modelSelected", model.id);
|
164 |
-
dispatch("close");
|
165 |
-
}}
|
166 |
-
>
|
167 |
-
<span class="inline-flex items-center"
|
168 |
-
><span class="text-gray-500 dark:text-gray-400">{nameSpace}</span><span
|
169 |
-
class="mx-1 text-gray-300 dark:text-gray-700">/</span
|
170 |
-
><span class="text-black dark:text-white">{modelName}</span></span
|
171 |
-
>
|
172 |
-
</button>
|
173 |
-
{/each}
|
174 |
-
</div>
|
175 |
-
</div>
|
176 |
{/if}
|
177 |
</div>
|
178 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import type { Conversation, ModelWithTokenizer } from "$lib/types.js";
|
3 |
|
4 |
+
import { tick } from "svelte";
|
5 |
|
6 |
+
import { autofocus } from "$lib/actions/autofocus.js";
|
7 |
import { models } from "$lib/state/models.svelte.js";
|
8 |
import fuzzysearch from "$lib/utils/search.js";
|
9 |
+
import { watch } from "runed";
|
10 |
import IconSearch from "~icons/carbon/search";
|
11 |
import IconStar from "~icons/carbon/star";
|
12 |
|
13 |
interface Props {
|
14 |
+
onModelSelect?: (model: string) => void;
|
15 |
+
onClose?: () => void;
|
16 |
conversation: Conversation;
|
17 |
}
|
18 |
|
19 |
+
let { onModelSelect, onClose, conversation }: Props = $props();
|
20 |
|
21 |
let backdropEl = $state<HTMLDivElement>();
|
22 |
+
let highlightIdx = $state(-1);
|
23 |
let ignoreCursorHighlight = $state(false);
|
24 |
let containerEl = $state<HTMLDivElement>();
|
25 |
let query = $state("");
|
26 |
|
27 |
+
const trending = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
|
28 |
+
const other = $derived(fuzzysearch({ needle: query, haystack: models.nonTrending, property: "id" }));
|
29 |
+
const queried = $derived(trending.concat(other));
|
30 |
+
function getModelIdx(model: ModelWithTokenizer) {
|
31 |
+
return queried.findIndex(m => m.id === model.id);
|
32 |
+
}
|
33 |
+
const highlighted = $derived(queried[highlightIdx]);
|
34 |
|
35 |
+
watch(
|
36 |
+
() => queried,
|
37 |
+
(curr, prev) => {
|
38 |
+
const prevModel = prev?.[highlightIdx];
|
39 |
+
if (prevModel) {
|
40 |
+
// maintain model selection
|
41 |
+
highlightIdx = Math.max(
|
42 |
+
0,
|
43 |
+
curr.findIndex(model => model.id === prevModel?.id)
|
44 |
+
);
|
45 |
+
} else {
|
46 |
+
highlightIdx = curr.findIndex(model => model.id === conversation.model.id);
|
47 |
+
}
|
48 |
+
scrollToResult();
|
49 |
}
|
50 |
+
);
|
51 |
|
52 |
+
function selectModel(model: ModelWithTokenizer) {
|
53 |
+
onModelSelect?.(model.id);
|
54 |
+
onClose?.();
|
55 |
+
}
|
56 |
|
57 |
+
function handleKeydown(e: KeyboardEvent) {
|
58 |
+
if (e.key === "Escape") {
|
59 |
+
onClose?.();
|
60 |
+
} else if (e.key === "Enter") {
|
61 |
+
if (highlighted) selectModel(highlighted);
|
62 |
+
} else if (e.key === "ArrowUp") {
|
63 |
+
if (highlightIdx > 0) highlightIdx--;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
ignoreCursorHighlight = true;
|
65 |
+
} else if (e.key === "ArrowDown") {
|
66 |
+
if (highlightIdx < queried.length - 1) highlightIdx++;
|
|
|
67 |
ignoreCursorHighlight = true;
|
68 |
+
} else {
|
69 |
+
return;
|
70 |
}
|
71 |
+
e.preventDefault();
|
72 |
+
|
73 |
+
scrollToResult();
|
74 |
}
|
75 |
|
76 |
+
async function scrollToResult() {
|
77 |
await tick();
|
78 |
+
const highlightedEl = document.querySelector("[data-model][data-highlighted]");
|
79 |
+
highlightedEl?.scrollIntoView({ block: "nearest" });
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
}
|
81 |
|
82 |
function highlightRow(idx: number) {
|
83 |
+
if (ignoreCursorHighlight) return;
|
84 |
+
highlightIdx = idx;
|
|
|
85 |
}
|
86 |
|
87 |
function handleBackdropClick(event: MouseEvent) {
|
|
|
90 |
return;
|
91 |
}
|
92 |
if (event.target === backdropEl) {
|
93 |
+
onClose?.();
|
94 |
}
|
95 |
}
|
96 |
</script>
|
|
|
113 |
<div class="mr-2 text-sm">
|
114 |
<IconSearch />
|
115 |
</div>
|
|
|
116 |
<input
|
117 |
+
use:autofocus
|
118 |
class="flex h-10 w-full rounded-md bg-transparent py-3 text-sm placeholder-gray-400 outline-hidden"
|
119 |
placeholder="Search models ..."
|
120 |
bind:value={query}
|
121 |
/>
|
122 |
</div>
|
123 |
<div class="max-h-[300px] overflow-x-hidden overflow-y-auto">
|
124 |
+
{#snippet modelEntry(model: ModelWithTokenizer, trending?: boolean)}
|
125 |
+
{@const idx = getModelIdx(model)}
|
126 |
+
{@const [nameSpace, modelName] = model.id.split("/")}
|
127 |
+
<button
|
128 |
+
class="flex w-full cursor-pointer items-center px-2 py-1.5 text-sm
|
129 |
+
data-[highlighted]:bg-gray-100 data-[highlighted]:dark:bg-gray-800"
|
130 |
+
data-highlighted={highlightIdx === idx ? true : undefined}
|
131 |
+
data-model
|
132 |
+
onmouseenter={() => highlightRow(idx)}
|
133 |
+
onclick={() => {
|
134 |
+
onModelSelect?.(model.id);
|
135 |
+
onClose?.();
|
136 |
+
}}
|
137 |
+
>
|
138 |
+
{#if trending}
|
139 |
+
<div class="lucide lucide-star mr-1.5 size-4 text-yellow-400">
|
140 |
+
<IconStar />
|
141 |
+
</div>
|
142 |
+
{/if}
|
143 |
+
<span class="inline-flex items-center"
|
144 |
+
><span class="text-gray-500 dark:text-gray-400">{nameSpace}</span><span
|
145 |
+
class="mx-1 text-gray-300 dark:text-gray-700">/</span
|
146 |
+
><span class="text-black dark:text-white">{modelName}</span></span
|
147 |
+
>
|
148 |
+
</button>
|
149 |
+
{/snippet}
|
150 |
+
{#if trending.length > 0}
|
151 |
+
<div class="px-2 py-1.5 text-xs font-medium text-gray-500">Trending</div>
|
152 |
+
{#each trending as model}
|
153 |
+
{@render modelEntry(model, true)}
|
154 |
+
{/each}
|
155 |
{/if}
|
156 |
+
{#if other.length > 0}
|
157 |
+
<div class="px-2 py-1.5 text-xs font-medium text-gray-500">Other models</div>
|
158 |
+
{#each other as model}
|
159 |
+
{@render modelEntry(model, false)}
|
160 |
+
{/each}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
{/if}
|
162 |
</div>
|
163 |
</div>
|
src/lib/components/inference-playground/model-selector.svelte
CHANGED
@@ -58,11 +58,7 @@
|
|
58 |
</div>
|
59 |
|
60 |
{#if showModelPickerModal}
|
61 |
-
<ModelSelectorModal
|
62 |
-
{conversation}
|
63 |
-
on:modelSelected={e => changeModel(e.detail)}
|
64 |
-
on:close={() => (showModelPickerModal = false)}
|
65 |
-
/>
|
66 |
{/if}
|
67 |
|
68 |
<ProviderSelect bind:conversation />
|
|
|
58 |
</div>
|
59 |
|
60 |
{#if showModelPickerModal}
|
61 |
+
<ModelSelectorModal {conversation} onModelSelect={changeModel} onClose={() => (showModelPickerModal = false)} />
|
|
|
|
|
|
|
|
|
62 |
{/if}
|
63 |
|
64 |
<ProviderSelect bind:conversation />
|
src/lib/components/inference-playground/playground.svelte
CHANGED
@@ -42,10 +42,10 @@
|
|
42 |
| [GenerationStatistics, GenerationStatistics]
|
43 |
);
|
44 |
|
45 |
-
|
46 |
session.project.conversations.some(conversation => isSystemPromptSupported(conversation.model))
|
47 |
);
|
48 |
-
|
49 |
|
50 |
function reset() {
|
51 |
session.project.conversations = session.project.conversations.map(conversation => {
|
@@ -412,7 +412,7 @@
|
|
412 |
{#if selectCompareModelOpen}
|
413 |
<ModelSelectorModal
|
414 |
conversation={session.project.conversations[0]!}
|
415 |
-
|
416 |
-
|
417 |
/>
|
418 |
{/if}
|
|
|
42 |
| [GenerationStatistics, GenerationStatistics]
|
43 |
);
|
44 |
|
45 |
+
const systemPromptSupported = $derived(
|
46 |
session.project.conversations.some(conversation => isSystemPromptSupported(conversation.model))
|
47 |
);
|
48 |
+
const compareActive = $derived(session.project.conversations.length === 2);
|
49 |
|
50 |
function reset() {
|
51 |
session.project.conversations = session.project.conversations.map(conversation => {
|
|
|
412 |
{#if selectCompareModelOpen}
|
413 |
<ModelSelectorModal
|
414 |
conversation={session.project.conversations[0]!}
|
415 |
+
onModelSelect={addCompareModel}
|
416 |
+
onClose={() => (selectCompareModelOpen = false)}
|
417 |
/>
|
418 |
{/if}
|
src/lib/state/models.svelte.ts
CHANGED
@@ -4,6 +4,7 @@ import type { ModelWithTokenizer } from "$lib/types.js";
|
|
4 |
class Models {
|
5 |
all = $derived(page.data.models as ModelWithTokenizer[]);
|
6 |
trending = $derived(this.all.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, 5));
|
|
|
7 |
}
|
8 |
|
9 |
export const models = new Models();
|
|
|
4 |
class Models {
|
5 |
all = $derived(page.data.models as ModelWithTokenizer[]);
|
6 |
trending = $derived(this.all.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, 5));
|
7 |
+
nonTrending = $derived(this.all.filter(m => !this.trending.includes(m)));
|
8 |
}
|
9 |
|
10 |
export const models = new Models();
|