feat(ai): use custom select model
This commit is contained in:
120
src/lib/components/Ai/ChangeModelBox.svelte
Normal file
120
src/lib/components/Ai/ChangeModelBox.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import type { Model } from './types';
|
||||
|
||||
interface Props {
|
||||
models: Model[];
|
||||
model: Model;
|
||||
onSelect?: (model: Model) => void;
|
||||
}
|
||||
|
||||
let { models, model = $bindable(), onSelect }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ul role="listbox">
|
||||
{#each models as m (m.name + m.brand)}
|
||||
{@const isSelected =
|
||||
m.brand === model.brand && m.name === model.name && m.endpoint === model.endpoint}
|
||||
<li role="option" aria-selected={isSelected}>
|
||||
<button
|
||||
title={[m.brand, m.name].filter(Boolean).join(' • ')}
|
||||
type="button"
|
||||
onclick={() => ((model = m), onSelect?.(m))}
|
||||
>
|
||||
<span class="name">{m.name}</span>
|
||||
<span class="description">
|
||||
<span class="tag">{m.brand}</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
background-color: hsl(0, 0%, 10%);
|
||||
border: 1px solid hsl(0, 0%, 15%);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul[role='listbox'] {
|
||||
padding: 4px;
|
||||
width: 265px;
|
||||
max-height: 265px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
li[role='option'] {
|
||||
width: 100%;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
& > button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: start;
|
||||
padding: 6px;
|
||||
color: hsl(0, 0%, 80%);
|
||||
border-radius: 4px;
|
||||
|
||||
& > span {
|
||||
display: block;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:first-of-type {
|
||||
font-family: monospace;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
color: hsl(0, 0%, 75%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-within) > button:not(:disabled) {
|
||||
background-color: hsl(0deg 0% 15%);
|
||||
color: hsl(0deg 0% 90%);
|
||||
}
|
||||
|
||||
&:is([aria-selected='true']) > button:not(:disabled) {
|
||||
background-color: hsl(0deg 0% 20%);
|
||||
color: hsl(0deg 0% 90%);
|
||||
}
|
||||
}
|
||||
|
||||
span.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
background-color: hsl(0deg 0% 17%);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,12 +2,13 @@
|
||||
import { autoresize } from '$lib/actions/autoresize.svelte';
|
||||
import { scroll_to_bottom } from '$lib/actions/scrollToBottom.svelte';
|
||||
import Select from '$lib/components/Select.svelte';
|
||||
import ChevronDown from '$lib/icons/ChevronDown.svelte';
|
||||
import CircleStack from '$lib/icons/CircleStack.svelte';
|
||||
import CircleStopSolid from '$lib/icons/CircleStopSolid.svelte';
|
||||
import Plus from '$lib/icons/Plus.svelte';
|
||||
import { getTextFromElement, transform } from '$lib/markdown';
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
import { deserializeModel, serializeModel } from '.';
|
||||
import ChangeModelBox from './ChangeModelBox.svelte';
|
||||
import DatasetsBox from './DatasetsBox.svelte';
|
||||
import Loader from './Loader.svelte';
|
||||
import type { ChatInput, ChatOutput, Model } from './types';
|
||||
@@ -37,11 +38,11 @@
|
||||
let loading = $state(false);
|
||||
let submitter = $state<HTMLButtonElement>();
|
||||
let message = $state('');
|
||||
let select = $state<ReturnType<typeof Select>>();
|
||||
let datasetSelectbox = $state<ReturnType<typeof Select>>();
|
||||
let textarea = $state<HTMLTextAreaElement>();
|
||||
let abortController: AbortController | undefined;
|
||||
let chatMessages = $derived(messages.filter((m) => m.role === 'user' || m.role === 'assistant'));
|
||||
let modelsOptions = $derived(Object.entries(Object.groupBy(models, (item) => item.brand)));
|
||||
let modelSelectbox = $state<ReturnType<typeof Select>>();
|
||||
|
||||
function getContextFromTable(table: Table): string {
|
||||
const columns = table.columns.map((col) => `- ${col.name} (${col.type})`).join('\n');
|
||||
@@ -195,38 +196,47 @@
|
||||
</section>
|
||||
|
||||
<div class="submitter">
|
||||
<button type="button" title="Add context" onclick={(e) => select?.open(e.currentTarget)}>
|
||||
<button
|
||||
type="button"
|
||||
title="Add context"
|
||||
onclick={(e) => datasetSelectbox?.open(e.currentTarget)}
|
||||
>
|
||||
<Plus size="12" />
|
||||
</button>
|
||||
<span class="separator"></span>
|
||||
<Select bind:this={select} placement="top-start">
|
||||
<Select bind:this={datasetSelectbox} placement="top-start">
|
||||
<DatasetsBox
|
||||
{datasets}
|
||||
onSelect={() => (
|
||||
select?.close(), abortController?.abort('Context changed'), onClearConversation?.()
|
||||
datasetSelectbox?.close(),
|
||||
abortController?.abort('Context changed'),
|
||||
onClearConversation?.()
|
||||
)}
|
||||
bind:dataset
|
||||
/>
|
||||
</Select>
|
||||
<select
|
||||
onchange={(e) => {
|
||||
const next = deserializeModel(e.currentTarget.value);
|
||||
if (next) onModelChange(next);
|
||||
}}
|
||||
<span class="separator"></span>
|
||||
<button
|
||||
type="button"
|
||||
title="Change model"
|
||||
onclick={(e) => modelSelectbox?.open(e.currentTarget)}
|
||||
disabled={models.length === 1}
|
||||
class="select-trigger"
|
||||
>
|
||||
{#each modelsOptions as [brand, models]}
|
||||
<optgroup label={brand}>
|
||||
{#each models ?? [] as model}
|
||||
{@const isSelected =
|
||||
model.brand === selectedModel.brand &&
|
||||
model.name === selectedModel.name &&
|
||||
model.endpoint === selectedModel.endpoint}
|
||||
<option selected={isSelected} value={serializeModel(model)}>{model.name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
{/each}
|
||||
</select>
|
||||
<span>{selectedModel.name}</span>
|
||||
{#if models.length > 1}
|
||||
<ChevronDown size="12" />
|
||||
{/if}
|
||||
</button>
|
||||
<Select bind:this={modelSelectbox} placement="top-start" id="change-model">
|
||||
<ChangeModelBox
|
||||
{models}
|
||||
bind:model={selectedModel}
|
||||
onSelect={() => {
|
||||
modelSelectbox?.close();
|
||||
abortController?.abort('Model changed');
|
||||
}}
|
||||
/>
|
||||
</Select>
|
||||
<span class="spacer"></span>
|
||||
{#if loading}
|
||||
<button
|
||||
@@ -340,25 +350,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.submitter > select {
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
color: hsl(0deg 0% 65%);
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
padding: 2px 0;
|
||||
|
||||
&:disabled {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
cursor: pointer;
|
||||
background-color: hsl(0deg 0% 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.submitter > button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -389,6 +380,18 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.select-trigger {
|
||||
aspect-ratio: initial;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(body:has(#change-model)) .select-trigger :global(> svg) {
|
||||
transform-origin: center;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
children?: Snippet;
|
||||
onClose?: () => void;
|
||||
anchor_size?: boolean;
|
||||
id?: HTMLElement['id'];
|
||||
}
|
||||
|
||||
let { placement = 'bottom-start', children, onClose, anchor_size = false }: Props = $props();
|
||||
let { placement = 'bottom-start', children, onClose, anchor_size = false, id }: Props = $props();
|
||||
|
||||
let opened = $state(false);
|
||||
let dropdown = $state<HTMLElement>();
|
||||
@@ -65,6 +66,7 @@
|
||||
transition:fade={{ duration: 150 }}
|
||||
role="presentation"
|
||||
onclick={(e) => e.target === e.currentTarget && close()}
|
||||
{id}
|
||||
>
|
||||
<div role="dialog" bind:this={dropdown} transition:fly={{ duration: 150, y: -10 }}>
|
||||
{@render children?.()}
|
||||
|
||||
15
src/lib/icons/ChevronDown.svelte
Normal file
15
src/lib/icons/ChevronDown.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
interface Props extends Omit<SvelteHTMLElements['svg'], 'width' | 'height'> {
|
||||
size?: string | number | null;
|
||||
}
|
||||
|
||||
let { size = 24, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width={size} height={size} fill="currentColor" viewBox="0 0 24 24" {...rest}>
|
||||
<path
|
||||
d="M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z"
|
||||
/>
|
||||
</svg>
|
||||
Reference in New Issue
Block a user