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 { createEventDispatcher, onMount, tick } from "svelte";
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(0);
19
  let ignoreCursorHighlight = $state(false);
20
  let containerEl = $state<HTMLDivElement>();
21
  let query = $state("");
22
 
23
- const dispatch = createEventDispatcher<{ modelSelected: string; close: void }>();
24
-
25
- let featuredModels = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
26
- let otherModels = $derived(fuzzysearch({ needle: query, haystack: models.all, property: "id" }));
 
 
 
27
 
28
- onMount(() => {
29
- if (featuredModels.findIndex(model => model.id === conversation.model.id) !== -1) {
30
- highlightIdx = featuredModels.findIndex(model => model.id === conversation.model.id);
31
- } else {
32
- highlightIdx = featuredModels.length + otherModels.findIndex(model => model.id === conversation.model.id);
 
 
 
 
 
 
 
 
 
33
  }
34
- });
35
 
36
- type ScrollLogicalPosition = "center" | "end" | "nearest" | "start";
 
 
 
37
 
38
- function handleKeydown(event: KeyboardEvent) {
39
- const { key } = event;
40
- let scrollLogicalPosition: ScrollLogicalPosition = "end";
41
- if (key === "Escape") {
42
- event.preventDefault();
43
- dispatch("close");
44
- } else if (key === "Enter") {
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
- event.preventDefault();
57
- highlightIdx++;
58
  ignoreCursorHighlight = true;
 
 
59
  }
60
- const n = featuredModels.length + otherModels.length;
61
- highlightIdx = ((highlightIdx % n) + n) % n;
62
- scrollToResult(scrollLogicalPosition);
63
  }
64
 
65
- async function scrollToResult(block: ScrollLogicalPosition) {
66
  await tick();
67
- const highlightedEl = document.querySelector(".highlighted");
68
- if (containerEl && highlightedEl) {
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 (!ignoreCursorHighlight) {
79
- highlightIdx = idx;
80
- }
81
  }
82
 
83
  function handleBackdropClick(event: MouseEvent) {
@@ -86,7 +90,7 @@
86
  return;
87
  }
88
  if (event.target === backdropEl) {
89
- dispatch("close");
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
- {#if featuredModels.length}
122
- <div>
123
- <div class="px-2 py-1.5 text-xs font-medium text-gray-500">Trending</div>
124
- <div>
125
- {#each featuredModels as model, idx}
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 {highlightIdx === idx
129
- ? 'highlighted bg-gray-100 dark:bg-gray-800'
130
- : ''}"
131
- onmouseenter={() => highlightRow(idx)}
132
- onclick={() => {
133
- dispatch("modelSelected", model.id);
134
- dispatch("close");
135
- }}
136
- >
137
- <div class="lucide lucide-star mr-1.5 size-4 text-yellow-400">
138
- <IconStar />
139
- </div>
140
- <span class="inline-flex items-center"
141
- ><span class="text-gray-500 dark:text-gray-400">{nameSpace}</span><span
142
- class="mx-1 text-gray-300 dark:text-gray-700">/</span
143
- ><span class="text-black dark:text-white">{modelName}</span></span
144
- >
145
- </button>
146
- {/each}
147
- </div>
148
- </div>
 
 
 
149
  {/if}
150
- {#if otherModels.length}
151
- <div>
152
- <div class="px-2 py-1.5 text-xs font-medium text-gray-500">Other Models</div>
153
- <div>
154
- {#each otherModels as model, _idx}
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
- let systemPromptSupported = $derived(
46
  session.project.conversations.some(conversation => isSystemPromptSupported(conversation.model))
47
  );
48
- let compareActive = $derived(session.project.conversations.length === 2);
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
- on:modelSelected={e => addCompareModel(e.detail)}
416
- on:close={() => (selectCompareModelOpen = false)}
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();