refac
This commit is contained in:
1 parent
815446d5bb
commit
61cee42ded
10 files changed
+275
-141
No files matched your search
@@ -127,14 +127,14 @@
|
|||||||
<div
|
<div
|
||||||
use:portal
|
use:portal
|
||||||
bind:this={popupElement}
|
bind:this={popupElement}
|
||||||
class="fixed w-48 max-h-48 overflow-y-auto rounded-lg border border-gray-200 bg-white p-0.5 shadow-lg dark:border-gray-800 dark:bg-gray-850"
|
class="fixed w-48 max-h-48 overflow-y-auto rounded-2xl border border-gray-200 bg-white p-0.5 shadow-lg dark:border-gray-800 dark:bg-gray-850"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
style="z-index: 9999; top: 0; left: 0;"
|
style="z-index: 9999; top: 0; left: 0;"
|
||||||
>
|
>
|
||||||
{#each filteredSuggestionTags as tag (tag)}
|
{#each filteredSuggestionTags as tag (tag)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center rounded-md px-2 py-1 text-left text-xs text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800"
|
class="flex w-full items-center rounded-xl px-2 py-[5px] text-left text-xs text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800"
|
||||||
role="option"
|
role="option"
|
||||||
on:mousedown={(event) => {
|
on:mousedown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -1,57 +1,70 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import TypeaheadSelector from './TypeaheadSelector.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
type Action = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
is_global?: boolean;
|
||||||
|
meta?: {
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export let actions = [];
|
const i18n = getContext('i18n') as any;
|
||||||
export let selectedActionIds = [];
|
|
||||||
|
|
||||||
let _actions = {};
|
export let actions: Action[] = [];
|
||||||
|
export let selectedActionIds: string[] = [];
|
||||||
|
|
||||||
onMount(() => {
|
$: selectedActions = actions.filter(
|
||||||
_actions = actions.reduce((acc, action) => {
|
(action) => action.is_global || selectedActionIds.includes(action.id)
|
||||||
acc[action.id] = {
|
);
|
||||||
...action,
|
$: availableActions = actions.filter(
|
||||||
selected: selectedActionIds.includes(action.id)
|
(action) => !action.is_global && !selectedActionIds.includes(action.id)
|
||||||
};
|
);
|
||||||
|
|
||||||
return acc;
|
const selectAction = (action: Action) => {
|
||||||
}, {});
|
selectedActionIds = [...selectedActionIds, action.id];
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if actions.length > 0}
|
{#if actions.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex w-full justify-between mb-1">
|
<div class="flex w-full justify-between mb-1">
|
||||||
<div class=" self-center text-xs font-medium text-gray-500">{$i18n.t('Actions')}</div>
|
<div class=" self-center text-xs text-gray-500">{$i18n.t('Actions')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
<TypeaheadSelector
|
||||||
|
id="model-actions-selector"
|
||||||
|
items={availableActions}
|
||||||
|
className="w-48 max-w-full"
|
||||||
|
placeholder={$i18n.t('Search actions')}
|
||||||
|
on:select={(e) => {
|
||||||
|
selectAction(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class=" flex items-center flex-wrap">
|
<div class=" flex items-center flex-wrap">
|
||||||
{#each Object.keys(_actions) as action, actionIdx}
|
{#each selectedActions as action, actionIdx}
|
||||||
<div class=" flex items-center gap-2 mr-3">
|
<div class=" flex items-center gap-2 mr-3">
|
||||||
<div class="self-center flex items-center">
|
<div class="self-center flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
state={_actions[action].is_global
|
state="checked"
|
||||||
? 'checked'
|
disabled={action.is_global}
|
||||||
: _actions[action].selected
|
|
||||||
? 'checked'
|
|
||||||
: 'unchecked'}
|
|
||||||
disabled={_actions[action].is_global}
|
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
if (!_actions[action].is_global) {
|
if (!action.is_global && e.detail === 'unchecked') {
|
||||||
_actions[action].selected = e.detail === 'checked';
|
selectedActionIds = selectedActionIds.filter((id) => id !== action.id);
|
||||||
selectedActionIds = Object.keys(_actions).filter((t) => _actions[t].selected);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
<div class=" py-0.5 text-xs capitalize">
|
||||||
<Tooltip content={_actions[action].meta.description}>
|
<Tooltip content={action.meta?.description ?? action.id}>
|
||||||
{_actions[action].name}
|
{action.name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,22 +2,51 @@
|
|||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import TypeaheadSelector from './TypeaheadSelector.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
type Filter = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
meta?: {
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export let filters = [];
|
const i18n = getContext('i18n') as any;
|
||||||
export let selectedFilterIds = [];
|
|
||||||
|
export let filters: Filter[] = [];
|
||||||
|
export let selectedFilterIds: string[] = [];
|
||||||
|
|
||||||
|
$: selectedFilters = filters.filter((filter) => selectedFilterIds.includes(filter.id));
|
||||||
|
$: availableFilters = filters.filter((filter) => !selectedFilterIds.includes(filter.id));
|
||||||
|
|
||||||
|
const selectFilter = (filter: Filter) => {
|
||||||
|
selectedFilterIds = [...selectedFilterIds, filter.id];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex w-full justify-between mb-1">
|
<div class="flex w-full justify-between mb-1">
|
||||||
<div class=" self-center text-xs text-gray-500 font-medium">{$i18n.t('Default Filters')}</div>
|
<div class=" self-center text-xs text-gray-500">{$i18n.t('Default Filters')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if filters.length > 0}
|
{#if filters.length > 0}
|
||||||
|
<TypeaheadSelector
|
||||||
|
id="model-default-filters-selector"
|
||||||
|
items={availableFilters.map((filter) => ({
|
||||||
|
...filter,
|
||||||
|
description: filter.meta?.description
|
||||||
|
}))}
|
||||||
|
className="w-48 max-w-full"
|
||||||
|
placeholder={$i18n.t('Search filters')}
|
||||||
|
on:select={(e) => {
|
||||||
|
selectFilter(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class=" flex items-center flex-wrap">
|
<div class=" flex items-center flex-wrap">
|
||||||
{#each filters as filter}
|
{#each selectedFilters as filter}
|
||||||
{@const isSelected = selectedFilterIds.includes(filter.id)}
|
{@const isSelected = selectedFilterIds.includes(filter.id)}
|
||||||
<div class=" flex items-center gap-2 mr-3">
|
<div class=" flex items-center gap-2 mr-3">
|
||||||
<div class="self-center flex items-center">
|
<div class="self-center flex items-center">
|
||||||
@@ -35,8 +64,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
<div class=" py-0.5 text-xs capitalize">
|
||||||
<Tooltip content={filter.meta.description}>
|
<Tooltip content={filter.meta?.description ?? filter.id}>
|
||||||
{filter.name}
|
{filter.name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,23 +2,57 @@
|
|||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import TypeaheadSelector from './TypeaheadSelector.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
type Filter = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
is_global?: boolean;
|
||||||
|
meta?: {
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export let filters = [];
|
const i18n = getContext('i18n') as any;
|
||||||
export let selectedFilterIds = [];
|
|
||||||
|
export let filters: Filter[] = [];
|
||||||
|
export let selectedFilterIds: string[] = [];
|
||||||
|
|
||||||
|
$: selectedFilters = filters.filter(
|
||||||
|
(filter) => filter.is_global || selectedFilterIds.includes(filter.id)
|
||||||
|
);
|
||||||
|
$: availableFilters = filters.filter(
|
||||||
|
(filter) => !filter.is_global && !selectedFilterIds.includes(filter.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectFilter = (filter: Filter) => {
|
||||||
|
selectedFilterIds = [...selectedFilterIds, filter.id];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filters.length > 0}
|
{#if filters.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex w-full justify-between mb-1">
|
<div class="flex w-full justify-between mb-1">
|
||||||
<div class=" self-center text-xs font-medium text-gray-500">{$i18n.t('Filters')}</div>
|
<div class=" self-center text-xs text-gray-500">{$i18n.t('Filters')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: Filter order matters -->
|
<!-- TODO: Filter order matters -->
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
<TypeaheadSelector
|
||||||
|
id="model-filters-selector"
|
||||||
|
items={availableFilters.map((filter) => ({
|
||||||
|
...filter,
|
||||||
|
description: filter.meta?.description
|
||||||
|
}))}
|
||||||
|
className="w-48 max-w-full"
|
||||||
|
placeholder={$i18n.t('Search filters')}
|
||||||
|
on:select={(e) => {
|
||||||
|
selectFilter(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class=" flex items-center flex-wrap">
|
<div class=" flex items-center flex-wrap">
|
||||||
{#each filters as filter}
|
{#each selectedFilters as filter}
|
||||||
{@const isSelected = filter.is_global || selectedFilterIds.includes(filter.id)}
|
{@const isSelected = filter.is_global || selectedFilterIds.includes(filter.id)}
|
||||||
<div class=" flex items-center gap-2 mr-3">
|
<div class=" flex items-center gap-2 mr-3">
|
||||||
<div class="self-center flex items-center">
|
<div class="self-center flex items-center">
|
||||||
@@ -39,8 +73,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
<div class=" py-0.5 text-xs capitalize">
|
||||||
<Tooltip content={filter.meta.description}>
|
<Tooltip content={filter.meta?.description ?? filter.id}>
|
||||||
{filter.name}
|
{filter.name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
<slot name="label">
|
<slot name="label">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div class="flex w-full justify-between mb-1">
|
<div class="flex w-full justify-between mb-1">
|
||||||
<div class=" self-center text-xs font-medium text-gray-500">
|
<div class=" self-center text-xs text-gray-500">
|
||||||
{$i18n.t('Knowledge')}
|
{$i18n.t('Knowledge')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,36 +190,41 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="flex flex-wrap flex-row text-sm gap-1">
|
<div class="flex items-center gap-2">
|
||||||
<KnowledgeSelector
|
<div class="min-w-0">
|
||||||
on:select={(e) => {
|
<KnowledgeSelector
|
||||||
const item = e.detail;
|
on:select={(e) => {
|
||||||
|
const item = e.detail;
|
||||||
|
|
||||||
if (!selectedItems.find((k) => k.id === item.id)) {
|
if (!selectedItems.find((k) => k.id === item.id)) {
|
||||||
selectedItems = [
|
selectedItems = [
|
||||||
...selectedItems,
|
...selectedItems,
|
||||||
{
|
{
|
||||||
...item
|
...item
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl"
|
|
||||||
>
|
>
|
||||||
{$i18n.t('Select Knowledge')}
|
<div
|
||||||
</div>
|
class="flex min-w-0 items-center bg-transparent text-xs text-gray-500 outline-hidden hover:underline dark:text-gray-400"
|
||||||
</KnowledgeSelector>
|
>
|
||||||
|
<span class="truncate">{$i18n.t('Select Knowledge')}</span>
|
||||||
|
</div>
|
||||||
|
</KnowledgeSelector>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if $user?.role === 'admin' || $user?.permissions?.chat?.file_upload}
|
{#if $user?.role === 'admin' || $user?.permissions?.chat?.file_upload}
|
||||||
<button
|
<button
|
||||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl"
|
class="self-center bg-transparent text-xs text-gray-500 hover:underline dark:text-gray-400"
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={$i18n.t('Upload Files')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
filesInputElement.click();
|
filesInputElement.click();
|
||||||
}}>{$i18n.t('Upload Files')}</button
|
}}
|
||||||
>
|
>
|
||||||
|
{$i18n.t('Upload')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -119,15 +119,15 @@
|
|||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<div
|
<div
|
||||||
class="z-[10000] text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-70 p-1.5"
|
class="z-[10000] text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-64 p-1"
|
||||||
>
|
>
|
||||||
<div class=" flex w-full space-x-2 px-2 pb-0.5">
|
<div class=" flex w-full space-x-1.5 px-1.5 pb-0.5">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-1.5">
|
||||||
<Search className="size-3.5" />
|
<Search className="size-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
class=" w-full text-sm py-0.5 outline-hidden bg-transparent"
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
on:input={handleSearchInput}
|
on:input={handleSearchInput}
|
||||||
placeholder={$i18n.t('Search')}
|
placeholder={$i18n.t('Search')}
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#each items as item, i}
|
{#each items as item, i}
|
||||||
{#if i === 0 || item?.type !== items[i - 1]?.type}
|
{#if i === 0 || item?.type !== items[i - 1]?.type}
|
||||||
<div class="px-2 text-xs text-gray-500 py-1">
|
<div class="px-1.5 text-xs text-gray-500 py-0.5">
|
||||||
{#if item?.type === 'note'}
|
{#if item?.type === 'note'}
|
||||||
{$i18n.t('Notes')}
|
{$i18n.t('Notes')}
|
||||||
{:else if item?.type === 'collection'}
|
{:else if item?.type === 'collection'}
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm hover:bg-gray-50 hover:dark:bg-gray-800 hover:dark:text-gray-100 selected-command-option-button"
|
class=" px-1.5 py-0.5 rounded-xl w-full text-left flex justify-between items-center text-sm hover:bg-gray-50 hover:dark:bg-gray-800 hover:dark:text-gray-100 selected-command-option-button"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="w-full flex-1"
|
class="w-full flex-1"
|
||||||
|
|||||||
@@ -1,69 +1,62 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import { getContext, onMount } from 'svelte';
|
import TypeaheadSelector from './TypeaheadSelector.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
export let skills = [];
|
type Skill = {
|
||||||
|
id: string;
|
||||||
let _skills = {};
|
name?: string;
|
||||||
let searchQuery = '';
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let skills: Skill[] = [];
|
||||||
export let selectedSkillIds: string[] = [];
|
export let selectedSkillIds: string[] = [];
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n') as any;
|
||||||
|
|
||||||
$: filteredSkillKeys = Object.keys(_skills).filter((id) => {
|
$: selectedSkills = skills.filter((skill) => selectedSkillIds.includes(skill.id));
|
||||||
if (!searchQuery.trim()) return true;
|
$: availableSkills = skills.filter((skill) => !selectedSkillIds.includes(skill.id));
|
||||||
const q = searchQuery.toLowerCase();
|
|
||||||
return _skills[id].name?.toLowerCase().includes(q) || _skills[id].id?.toLowerCase().includes(q);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
const selectSkill = (skill: Skill) => {
|
||||||
_skills = skills.reduce((acc, skill) => {
|
selectedSkillIds = [...selectedSkillIds, skill.id];
|
||||||
acc[skill.id] = {
|
};
|
||||||
...skill,
|
|
||||||
selected: selectedSkillIds.includes(skill.id)
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex w-full justify-between mb-1">
|
<div class="flex w-full justify-between mb-1">
|
||||||
<div class=" self-center text-xs font-medium text-gray-500">{$i18n.t('Skills')}</div>
|
<div class=" self-center text-xs text-gray-500">{$i18n.t('Skills')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if Object.keys(_skills).length > 10}
|
|
||||||
<div class="mb-2">
|
|
||||||
<input
|
|
||||||
class="w-full text-sm bg-transparent outline-none border border-gray-100 dark:border-gray-800 rounded-lg px-3 py-1.5 placeholder-gray-400"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Search skills...')}
|
|
||||||
bind:value={searchQuery}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-1">
|
<div class="flex flex-col mb-1">
|
||||||
{#if skills.length > 0}
|
{#if skills.length > 0}
|
||||||
|
<TypeaheadSelector
|
||||||
|
id="model-skills-selector"
|
||||||
|
items={availableSkills}
|
||||||
|
className="w-48 max-w-full"
|
||||||
|
placeholder={$i18n.t('Search skills')}
|
||||||
|
on:select={(e) => {
|
||||||
|
selectSkill(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class=" flex items-center flex-wrap">
|
<div class=" flex items-center flex-wrap">
|
||||||
{#each filteredSkillKeys as skill, skillIdx}
|
{#each selectedSkills as skill, skillIdx}
|
||||||
<div class=" flex items-center gap-2 mr-3">
|
<div class=" flex items-center gap-2 mr-3">
|
||||||
<div class="self-center flex items-center">
|
<div class="self-center flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
state={_skills[skill].selected ? 'checked' : 'unchecked'}
|
state="checked"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
_skills[skill].selected = e.detail === 'checked';
|
if (e.detail === 'unchecked') {
|
||||||
selectedSkillIds = Object.keys(_skills).filter((s) => _skills[s].selected);
|
selectedSkillIds = selectedSkillIds.filter((id) => id !== skill.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip content={_skills[skill]?.description ?? _skills[skill].id}>
|
<Tooltip content={skill.description ?? skill.id}>
|
||||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
<div class=" py-0.5 text-xs capitalize">
|
||||||
{_skills[skill].name}
|
{skill.name}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { createEventDispatcher, tick } from 'svelte';
|
||||||
|
|
||||||
type Voice = {
|
type Voice = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
meta?: {
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export let id = 'tts-voice';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
export let voices: Voice[] = [];
|
export let voices: Voice[] = [];
|
||||||
export let placeholder = '';
|
export let placeholder = '';
|
||||||
|
export let className = 'w-full';
|
||||||
|
|
||||||
const listboxId = 'tts-voice-options';
|
const dispatch = createEventDispatcher<{ select: Voice }>();
|
||||||
|
const listboxId = `${id}-options`;
|
||||||
|
|
||||||
let inputElement: HTMLInputElement | null = null;
|
let inputElement: HTMLInputElement | null = null;
|
||||||
let popupElement: HTMLDivElement | null = null;
|
let popupElement: HTMLDivElement | null = null;
|
||||||
@@ -21,8 +28,11 @@
|
|||||||
.filter((voice) => {
|
.filter((voice) => {
|
||||||
const id = voice.id.toLowerCase();
|
const id = voice.id.toLowerCase();
|
||||||
const name = (voice.name ?? '').toLowerCase();
|
const name = (voice.name ?? '').toLowerCase();
|
||||||
|
const description = (voice.description ?? voice.meta?.description ?? '').toLowerCase();
|
||||||
|
|
||||||
return query === '' || id.includes(query) || name.includes(query);
|
return (
|
||||||
|
query === '' || id.includes(query) || name.includes(query) || description.includes(query)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.slice(0, 8);
|
.slice(0, 8);
|
||||||
$: if (suggestionsOpen && filteredVoices.length > 0) {
|
$: if (suggestionsOpen && filteredVoices.length > 0) {
|
||||||
@@ -52,6 +62,7 @@
|
|||||||
const selectVoice = (voice: Voice) => {
|
const selectVoice = (voice: Voice) => {
|
||||||
value = voice.id;
|
value = voice.id;
|
||||||
suggestionsOpen = false;
|
suggestionsOpen = false;
|
||||||
|
dispatch('select', voice);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerDown = (event: PointerEvent) => {
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
@@ -75,7 +86,8 @@
|
|||||||
<input
|
<input
|
||||||
bind:this={inputElement}
|
bind:this={inputElement}
|
||||||
bind:value
|
bind:value
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
id={`${id}-input`}
|
||||||
|
class="{className} text-sm bg-transparent outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
{placeholder}
|
{placeholder}
|
||||||
role="combobox"
|
role="combobox"
|
||||||
@@ -110,14 +122,14 @@
|
|||||||
use:portal
|
use:portal
|
||||||
bind:this={popupElement}
|
bind:this={popupElement}
|
||||||
id={listboxId}
|
id={listboxId}
|
||||||
class="fixed max-h-48 overflow-y-auto rounded-lg border border-gray-200 bg-white p-0.5 shadow-lg dark:border-gray-800 dark:bg-gray-850"
|
class="fixed max-h-48 overflow-y-auto rounded-2xl border border-gray-200 bg-white p-0.5 shadow-lg dark:border-gray-800 dark:bg-gray-850"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
style="z-index: 9999; top: 0; left: 0;"
|
style="z-index: 9999; top: 0; left: 0;"
|
||||||
>
|
>
|
||||||
{#each filteredVoices as voice (voice.id)}
|
{#each filteredVoices as voice (voice.id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-xs text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800"
|
class="flex w-full items-center justify-between gap-3 rounded-xl px-2 py-[5px] text-left text-xs text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={value === voice.id}
|
aria-selected={value === voice.id}
|
||||||
on:mousedown={(event) => {
|
on:mousedown={(event) => {
|
||||||
|
|||||||
@@ -1,51 +1,64 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import { getContext, onMount } from 'svelte';
|
import TypeaheadSelector from './TypeaheadSelector.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
export let tools = [];
|
type Tool = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
meta?: {
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
let _tools = {};
|
export let tools: Tool[] = [];
|
||||||
|
export let selectedToolIds: string[] = [];
|
||||||
|
|
||||||
export let selectedToolIds = [];
|
const i18n = getContext('i18n') as any;
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
$: selectedTools = tools.filter((tool) => selectedToolIds.includes(tool.id));
|
||||||
|
$: availableTools = tools.filter((tool) => !selectedToolIds.includes(tool.id));
|
||||||
|
|
||||||
onMount(() => {
|
const selectTool = (tool: Tool) => {
|
||||||
_tools = tools.reduce((acc, tool) => {
|
selectedToolIds = [...selectedToolIds, tool.id];
|
||||||
acc[tool.id] = {
|
};
|
||||||
...tool,
|
|
||||||
selected: selectedToolIds.includes(tool.id)
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex w-full justify-between mb-1">
|
<div class="flex w-full justify-between mb-1">
|
||||||
<div class=" self-center text-xs font-medium text-gray-500">{$i18n.t('Tools')}</div>
|
<div class=" self-center text-xs text-gray-500">{$i18n.t('Tools')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-1">
|
<div class="flex flex-col mb-1">
|
||||||
{#if tools.length > 0}
|
{#if tools.length > 0}
|
||||||
|
<TypeaheadSelector
|
||||||
|
id="model-tools-selector"
|
||||||
|
items={availableTools}
|
||||||
|
className="w-48 max-w-full"
|
||||||
|
placeholder={$i18n.t('Search tools')}
|
||||||
|
on:select={(e) => {
|
||||||
|
selectTool(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class=" flex items-center flex-wrap">
|
<div class=" flex items-center flex-wrap">
|
||||||
{#each Object.keys(_tools) as tool, toolIdx}
|
{#each selectedTools as tool, toolIdx}
|
||||||
<div class=" flex items-center gap-2 mr-3">
|
<div class=" flex items-center gap-2 mr-3">
|
||||||
<div class="self-center flex items-center">
|
<div class="self-center flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
state={_tools[tool].selected ? 'checked' : 'unchecked'}
|
state="checked"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
_tools[tool].selected = e.detail === 'checked';
|
if (e.detail === 'unchecked') {
|
||||||
selectedToolIds = Object.keys(_tools).filter((t) => _tools[t].selected);
|
selectedToolIds = selectedToolIds.filter((id) => id !== tool.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip content={_tools[tool]?.meta?.description ?? _tools[tool].id}>
|
<Tooltip content={tool.meta?.description ?? tool.id}>
|
||||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
<div class=" py-0.5 text-xs capitalize">
|
||||||
{_tools[tool].name}
|
{tool.name}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import TTSVoiceInput from './TTSVoiceInput.svelte';
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
meta?: {
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export let items: Item[] = [];
|
||||||
|
export let placeholder = '';
|
||||||
|
export let id = 'typeahead-selector';
|
||||||
|
export let className = 'w-full';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ select: Item }>();
|
||||||
|
let value = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-1 block">
|
||||||
|
<TTSVoiceInput
|
||||||
|
{id}
|
||||||
|
voices={items}
|
||||||
|
{placeholder}
|
||||||
|
{className}
|
||||||
|
bind:value
|
||||||
|
on:select={(e) => {
|
||||||
|
dispatch('select', e.detail);
|
||||||
|
value = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
Reference in new issue
Block a user