This commit is contained in:
Timothy Jaeryang Baek
2026-07-15 03:39:45 -04:00
parent fee4c280f8
commit fa889837e9
10 changed files with 278 additions and 100 deletions
@@ -57,7 +57,7 @@
<div
class={layout === 'grid'
? 'grid grid-cols-1 gap-x-5 gap-y-1 pb-safe-bottom text-xs sm:grid-cols-2 xl:grid-cols-3'
? 'grid grid-cols-1 gap-x-5 gap-y-1 pb-safe-bottom text-xs sm:grid-cols-2 lg:grid-cols-3'
: 'space-y-1 text-xs pb-safe-bottom'}
>
<div>
+7 -5
View File
@@ -912,7 +912,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
<PaneGroup direction="horizontal" class="w-full h-full">
<Pane defaultSize={70} minSize={30} class="h-full flex flex-col w-full relative">
<div class="relative flex-1 w-full h-full flex justify-center pt-[11px]" id="note-editor">
<div class="relative flex-1 w-full h-full flex justify-center pt-2" id="note-editor">
{#if loading}
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
<div class="m-auto">
@@ -921,7 +921,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
</div>
{:else}
<div class=" w-full flex flex-col {loading ? 'opacity-20' : ''}">
<div class="shrink-0 w-full flex justify-between items-center px-3.5 mb-1.5">
<div class="shrink-0 w-full flex justify-between items-center px-2.5 mb-1.5">
<div class="w-full min-w-0 flex items-center">
{#if $mobile}
<Tooltip
@@ -944,7 +944,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
{/if}
<input
class="w-full text-2xl font-normal bg-transparent outline-hidden"
class="w-full text-sm font-normal bg-transparent outline-hidden {$mobile
? 'ml-1'
: ''}"
type="text"
bind:value={note.title}
placeholder={titleGenerating ? $i18n.t('Generating...') : $i18n.t('Title')}
@@ -1124,7 +1126,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
</div>
</div>
<div class=" px-2.5">
<div class=" px-1.5">
<div
class=" flex w-full bg-transparent overflow-x-auto scrollbar-none"
on:wheel={(e) => {
@@ -1180,7 +1182,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
</div>
<div
class=" flex-1 w-full h-full overflow-auto px-3.5 relative"
class=" flex-1 w-full h-full overflow-auto px-2.5 relative"
id="note-content-container"
>
{#if editing}
@@ -68,7 +68,7 @@
<div>
<div class="mb-1.5 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Builtin Tools')}</div>
<div class="grid grid-cols-1 gap-x-5 gap-y-1 sm:grid-cols-2 xl:grid-cols-3">
<div class="grid grid-cols-1 gap-x-5 gap-y-1 sm:grid-cols-2 lg:grid-cols-3">
{#each allTools as tool}
<div class="flex min-h-6 items-center justify-between gap-2.5">
<div class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
@@ -84,7 +84,7 @@
<div>
<div class="mb-1.5 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Capabilities')}</div>
<div class="grid grid-cols-1 gap-x-5 gap-y-1 sm:grid-cols-2 xl:grid-cols-3">
<div class="grid grid-cols-1 gap-x-5 gap-y-1 sm:grid-cols-2 lg:grid-cols-3">
{#each visibleCapabilities as capability}
<div class="flex min-h-6 items-center justify-between gap-2.5">
<div class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
@@ -27,7 +27,7 @@
<div>
<div class="mb-1.5 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Default Features')}</div>
<div class="grid grid-cols-1 gap-x-5 gap-y-1 sm:grid-cols-2 xl:grid-cols-3">
<div class="grid grid-cols-1 gap-x-5 gap-y-1 sm:grid-cols-2 lg:grid-cols-3">
{#each availableFeatures as feature}
<div class="flex min-h-6 items-center justify-between gap-2.5">
<div class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
@@ -3,7 +3,14 @@
import { config, knowledge, settings, user } from '$lib/stores';
import KnowledgeSelector from './Knowledge/KnowledgeSelector.svelte';
import FileItem from '$lib/components/common/FileItem.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
import Database from '$lib/components/icons/Database.svelte';
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
import Folder from '$lib/components/icons/Folder.svelte';
import PageEdit from '$lib/components/icons/PageEdit.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import { uploadFile } from '$lib/apis/files';
@@ -155,79 +162,118 @@
/>
<div>
<slot name="label">
<div class="mb-2">
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-xs text-gray-500">
{$i18n.t('Knowledge')}
</div>
<div class="mb-2">
<div class="flex w-full items-center gap-2 mb-1">
<div class="min-w-0">
<slot name="label">
<div class=" self-center text-xs text-gray-500">
{$i18n.t('Knowledge')}
</div>
</slot>
</div>
{#if loaded}
<div class="flex shrink-0 items-center gap-2">
<div class="min-w-0">
<KnowledgeSelector
on:select={(e) => {
const item = e.detail;
if (!selectedItems.find((k) => k.id === item.id)) {
selectedItems = [
...selectedItems,
{
...item
}
];
}
}}
>
<div
class="flex min-w-0 items-center bg-transparent text-xs text-gray-500 outline-hidden hover:underline dark:text-gray-400"
>
<span class="truncate">{$i18n.t('Select Knowledge')}</span>
</div>
</KnowledgeSelector>
</div>
{#if $user?.role === 'admin' || $user?.permissions?.chat?.file_upload}
<button
class="self-center bg-transparent text-xs text-gray-500 hover:underline dark:text-gray-400"
type="button"
aria-label={$i18n.t('Upload Files')}
on:click={() => {
filesInputElement.click();
}}
>
{$i18n.t('Upload')}
</button>
{/if}
</div>
{/if}
</div>
</slot>
</div>
<div class="flex flex-col mb-1">
{#if selectedItems?.length > 0}
<div class=" flex flex-wrap items-center gap-2 mb-2.5">
<div class=" flex flex-wrap items-center gap-1.5 mb-2.5">
{#each selectedItems as file, fileIdx}
<FileItem
{file}
small={true}
item={file}
name={file.name}
modal={true}
edit={true}
loading={file.status === 'uploading'}
type={file?.legacy
? `Legacy${file.type ? ` ${file.type}` : ''}`
: (file?.type ?? 'collection')}
dismissible
on:dismiss={(e) => {
selectedItems = selectedItems.filter((_, idx) => idx !== fileIdx);
}}
/>
{/each}
</div>
{/if}
{#if loaded}
<div class="flex items-center gap-2">
<div class="min-w-0">
<KnowledgeSelector
on:select={(e) => {
const item = e.detail;
if (!selectedItems.find((k) => k.id === item.id)) {
selectedItems = [
...selectedItems,
{
...item
}
];
}
}}
>
<Tooltip content={file.description || file.name || file.id}>
<div
class="flex min-w-0 items-center bg-transparent text-xs text-gray-500 outline-hidden hover:underline dark:text-gray-400"
class="flex max-w-56 items-center gap-1.5 py-0.5 pr-2 text-xs text-gray-700 dark:text-gray-200"
>
<span class="truncate">{$i18n.t('Select Knowledge')}</span>
</div>
</KnowledgeSelector>
</div>
<div class="shrink-0 text-gray-500 dark:text-gray-400">
{#if file.status === 'uploading'}
<Spinner className="size-3.5" />
{:else if file.type === 'collection'}
<Database className="size-3.5" />
{:else if file.type === 'note'}
<PageEdit className="size-3.5" />
{:else if file.type === 'chat'}
<ChatBubble className="size-3.5" />
{:else if file.type === 'folder'}
<Folder className="size-3.5" />
{:else}
<DocumentPage className="size-3.5" />
{/if}
</div>
{#if $user?.role === 'admin' || $user?.permissions?.chat?.file_upload}
<button
class="self-center bg-transparent text-xs text-gray-500 hover:underline dark:text-gray-400"
type="button"
aria-label={$i18n.t('Upload Files')}
on:click={() => {
filesInputElement.click();
}}
>
{$i18n.t('Upload')}
</button>
{/if}
<div class="min-w-0 truncate">
{file.name || file.id}
</div>
{#if file.status === 'uploading'}
<div class="shrink-0 text-gray-400 dark:text-gray-500">
{$i18n.t('Uploading')}
</div>
{/if}
<button
type="button"
class="flex size-4 shrink-0 items-center justify-center text-gray-400 dark:text-gray-500"
aria-label={$i18n.t('Remove File')}
on:click={() => {
selectedItems = selectedItems.filter((_, idx) => idx !== fileIdx);
}}
>
<XMark className="size-3" />
</button>
</div>
</Tooltip>
{/each}
<button
type="button"
class="py-0.5 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
on:click={() => {
selectedItems = [];
}}
>
{$i18n.t('Disable all')}
</button>
</div>
{/if}
<!-- {knowledge} -->
</div>
@@ -155,7 +155,7 @@
{/if}
<div
class="h-[1.6875rem] px-2 rounded-xl w-full text-left flex justify-between items-center text-[13px] bg-transparent hover:text-gray-900 dark:hover:text-gray-100 selected-command-option-button"
class="h-[1.6875rem] px-2 rounded-xl w-full text-left flex justify-between items-center text-[13px] bg-transparent transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100 selected-command-option-button"
>
<button
class="w-full flex-1"
@@ -27,18 +27,18 @@
</script>
<div>
<div class="flex w-full justify-between mb-1">
<div class="flex w-full items-center gap-2 mb-1">
<div class=" self-center text-xs text-gray-500">{$i18n.t('Skills')}</div>
</div>
<div class="flex flex-col mb-1">
{#if activeSkills.length > 0}
<TypeaheadSelector
id="model-skills-selector"
items={activeSkills}
selectedIds={selectedSkillIds}
className="w-48 max-w-full"
placeholder={$i18n.t('Search skills')}
triggerLabel={$i18n.t('Select Skill')}
emptyLabel={$i18n.t('No skills found')}
variant="dropdown"
on:select={(e) => {
toggleSkill(e.detail);
}}
@@ -48,8 +48,12 @@
];
}}
/>
{/if}
</div>
<div class=" flex items-center flex-wrap">
<div class="flex flex-col mb-1">
{#if activeSkills.length > 0}
<div class=" flex items-center flex-wrap mt-1">
{#each selectedSkills as skill, skillIdx}
<div class=" flex items-center gap-2 mr-3">
<div class="self-center flex items-center">
@@ -79,7 +83,7 @@
selectedSkillIds = [];
}}
>
Disable all
{$i18n.t('Disable all')}
</button>
{/if}
</div>
@@ -27,18 +27,18 @@
</script>
<div>
<div class="flex w-full justify-between mb-1">
<div class="flex w-full items-center gap-2 mb-1">
<div class=" self-center text-xs text-gray-500">{$i18n.t('Tools')}</div>
</div>
<div class="flex flex-col mb-1">
{#if tools.length > 0}
<TypeaheadSelector
id="model-tools-selector"
items={tools}
selectedIds={selectedToolIds}
className="w-48 max-w-full"
placeholder={$i18n.t('Search tools')}
triggerLabel={$i18n.t('Select Tool')}
emptyLabel={$i18n.t('No tools found')}
variant="dropdown"
on:select={(e) => {
toggleTool(e.detail);
}}
@@ -46,8 +46,12 @@
selectedToolIds = [...new Set([...selectedToolIds, ...e.detail.map((tool) => tool.id)])];
}}
/>
{/if}
</div>
<div class=" flex items-center flex-wrap">
<div class="flex flex-col mb-1">
{#if tools.length > 0}
<div class=" flex items-center flex-wrap mt-1">
{#each selectedTools as tool, toolIdx}
<div class=" flex items-center gap-2 mr-3">
<div class="self-center flex items-center">
@@ -77,7 +81,7 @@
selectedToolIds = [];
}}
>
Disable all
{$i18n.t('Disable all')}
</button>
{/if}
</div>
@@ -1,5 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, tick } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Search from '$lib/components/icons/Search.svelte';
import TTSVoiceInput from './TTSVoiceInput.svelte';
type Item = {
@@ -13,29 +15,149 @@
export let items: Item[] = [];
export let placeholder = '';
export let triggerLabel = '';
export let emptyLabel = '';
export let id = 'typeahead-selector';
export let className = 'w-full';
export let selectedIds: string[] | null = null;
export let variant: 'inline' | 'dropdown' = 'inline';
const dispatch = createEventDispatcher<{
select: Item;
enableall: Item[];
}>();
let value = '';
let show = false;
let inputElement: HTMLInputElement | null = null;
$: query = value.trim().toLowerCase();
$: matchedItems = (items ?? []).filter((item) => {
const id = item.id.toLowerCase();
const name = (item.name ?? '').toLowerCase();
const description = (item.description ?? item.meta?.description ?? '').toLowerCase();
return (
query === '' || id.includes(query) || name.includes(query) || description.includes(query)
);
});
$: if (variant === 'dropdown' && show) {
tick().then(() => inputElement?.focus());
}
const selectItem = (item: Item) => {
dispatch('select', item);
show = false;
value = '';
};
const enableItems = () => {
dispatch('enableall', matchedItems);
show = false;
value = '';
};
</script>
<div class="mb-1 block">
<TTSVoiceInput
{id}
voices={items}
{placeholder}
{className}
{selectedIds}
bind:value
on:select={(e) => {
dispatch('select', e.detail);
if (selectedIds === null) value = '';
{#if variant === 'dropdown'}
<Dropdown
bind:show
onOpenChange={(state) => {
if (!state) {
value = '';
}
}}
on:enableall={(e) => dispatch('enableall', e.detail)}
/>
</div>
>
<div
class="flex min-w-0 items-center bg-transparent text-xs text-gray-500 outline-hidden hover:underline dark:text-gray-400"
>
<span class="truncate">{triggerLabel || placeholder}</span>
</div>
<div slot="content">
<div
class="z-[10000] flex w-64 flex-col rounded-xl border border-gray-200 bg-white p-0.5 text-black shadow-lg dark:border-gray-800 dark:bg-gray-850 dark:text-white"
>
<div class="flex w-full space-x-1.5 px-1.5 pb-0.5">
<div class="flex flex-1">
<div class="self-center mr-1.5">
<Search className="size-3.5" />
</div>
<input
bind:this={inputElement}
bind:value
id={`${id}-input`}
class="w-full bg-transparent py-0.5 text-[13px] outline-hidden"
type="text"
{placeholder}
autocomplete="off"
/>
</div>
</div>
<div class="flex max-h-56 flex-col gap-0.5 overflow-y-scroll">
{#if selectedIds !== null && matchedItems.length > 0}
<button
type="button"
class="h-[1.6875rem] w-full rounded-xl px-2 text-left text-[13px] text-gray-700 transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-gray-800/40 dark:hover:text-gray-100"
on:click={enableItems}
>
<span class="truncate">Enable all ({matchedItems.length})</span>
</button>
{/if}
{#if matchedItems.length === 0}
<div class="pt-4 pb-6 text-center text-xs text-gray-500 dark:text-gray-400">
{emptyLabel || placeholder}
</div>
{:else}
{#each matchedItems as item (item.id)}
<button
type="button"
class="flex h-[1.6875rem] w-full items-center justify-between gap-2 rounded-xl px-2 text-left text-[13px] transition-colors hover:bg-gray-50/40 hover:text-gray-900 dark:hover:bg-gray-800/40 dark:hover:text-gray-100 selected-command-option-button"
aria-pressed={selectedIds?.includes(item.id) ?? false}
on:click={() => {
selectItem(item);
}}
>
<span class="min-w-0 flex-1 truncate">{item.name || item.id}</span>
{#if selectedIds !== null && selectedIds.includes(item.id)}
<svg
class="size-3.5 shrink-0 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="m5 12 4.7 4.5 9.3-9"
/>
</svg>
{/if}
</button>
{/each}
{/if}
</div>
</div>
</div>
</Dropdown>
{:else}
<div class="mb-1 block">
<TTSVoiceInput
{id}
voices={items}
{placeholder}
{className}
{selectedIds}
bind:value
on:select={(e) => {
dispatch('select', e.detail);
if (selectedIds === null) value = '';
}}
on:enableall={(e) => dispatch('enableall', e.detail)}
/>
</div>
{/if}