Commit f26d80dc authored by Jun Siang Cheah's avatar Jun Siang Cheah
Browse files

Merge remote-tracking branch 'upstream/dev' into feat/oauth

parents 99e7b328 f54a66b8
...@@ -321,9 +321,11 @@ ...@@ -321,9 +321,11 @@
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full"> <div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative"> <div class="relative">
{#if autoScroll === false && messages.length > 0} {#if autoScroll === false && messages.length > 0}
<div class=" absolute -top-12 left-0 right-0 flex justify-center z-30"> <div
class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
>
<button <button
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full" class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
on:click={() => { on:click={() => {
autoScroll = true; autoScroll = true;
scrollToBottom(); scrollToBottom();
......
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
return; return;
} }
if (assistantSpeaking) { if (assistantSpeaking && !($settings?.voiceInterruption ?? false)) {
// Mute the audio if the assistant is speaking // Mute the audio if the assistant is speaking
analyser.maxDecibels = 0; analyser.maxDecibels = 0;
analyser.minDecibels = -1; analyser.minDecibels = -1;
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
</script> </script>
{#key mounted} {#key mounted}
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-10"> <div class="m-auto w-full max-w-6xl px-8 lg:px-20 pb-10">
<div class="flex justify-start"> <div class="flex justify-start">
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}> <div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
{#each models as model, modelIdx} {#each models as model, modelIdx}
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
let chatDirection: 'LTR' | 'RTL' = 'LTR'; let chatDirection: 'LTR' | 'RTL' = 'LTR';
let showEmojiInCall = false; let showEmojiInCall = false;
let voiceInterruption = false;
const toggleSplitLargeChunks = async () => { const toggleSplitLargeChunks = async () => {
splitLargeChunks = !splitLargeChunks; splitLargeChunks = !splitLargeChunks;
...@@ -58,6 +59,11 @@ ...@@ -58,6 +59,11 @@
saveSettings({ showEmojiInCall: showEmojiInCall }); saveSettings({ showEmojiInCall: showEmojiInCall });
}; };
const toggleVoiceInterruption = async () => {
voiceInterruption = !voiceInterruption;
saveSettings({ voiceInterruption: voiceInterruption });
};
const toggleUserLocation = async () => { const toggleUserLocation = async () => {
userLocation = !userLocation; userLocation = !userLocation;
...@@ -128,6 +134,7 @@ ...@@ -128,6 +134,7 @@
showUsername = $settings.showUsername ?? false; showUsername = $settings.showUsername ?? false;
showEmojiInCall = $settings.showEmojiInCall ?? false; showEmojiInCall = $settings.showEmojiInCall ?? false;
voiceInterruption = $settings.voiceInterruption ?? false;
chatBubble = $settings.chatBubble ?? true; chatBubble = $settings.chatBubble ?? true;
widescreenMode = $settings.widescreenMode ?? false; widescreenMode = $settings.widescreenMode ?? false;
...@@ -399,6 +406,26 @@ ...@@ -399,6 +406,26 @@
<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div> <div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleVoiceInterruption();
}}
type="button"
>
{#if voiceInterruption === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div> <div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" pr-1.5 overflow-y-scroll max-h-[25rem]"> <div class=" pr-1.5 py-1 overflow-y-scroll max-h-[25rem]">
<div> <div>
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<Tooltip <Tooltip
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
</div> </div>
</Tooltip> </Tooltip>
<div class="mt-1"> <div class="">
<Switch <Switch
bind:state={enableMemory} bind:state={enableMemory}
on:change={async () => { on:change={async () => {
......
<script lang="ts">
import { toast } from 'svelte-sonner';
import { config, functions, models, settings, tools, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import {
getUserValvesSpecById as getToolUserValvesSpecById,
getUserValvesById as getToolUserValvesById,
updateUserValvesById as updateToolUserValvesById
} from '$lib/apis/tools';
import {
getUserValvesSpecById as getFunctionUserValvesSpecById,
getUserValvesById as getFunctionUserValvesById,
updateUserValvesById as updateFunctionUserValvesById
} from '$lib/apis/functions';
import ManageModal from './Personalization/ManageModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
let tab = 'tools';
let selectedId = '';
let loading = false;
let valvesSpec = null;
let valves = {};
const getUserValves = async () => {
loading = true;
if (tab === 'tools') {
valves = await getToolUserValvesById(localStorage.token, selectedId);
valvesSpec = await getToolUserValvesSpecById(localStorage.token, selectedId);
} else if (tab === 'functions') {
valves = await getFunctionUserValvesById(localStorage.token, selectedId);
valvesSpec = await getFunctionUserValvesSpecById(localStorage.token, selectedId);
}
if (valvesSpec) {
// Convert array to string
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? []).join(',');
}
}
}
loading = false;
};
const submitHandler = async () => {
if (valvesSpec) {
// Convert string to array
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim());
}
}
if (tab === 'tools') {
const res = await updateToolUserValvesById(localStorage.token, selectedId, valves).catch(
(error) => {
toast.error(error);
return null;
}
);
if (res) {
toast.success('Valves updated');
valves = res;
}
} else if (tab === 'functions') {
const res = await updateFunctionUserValvesById(
localStorage.token,
selectedId,
valves
).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Valves updated');
valves = res;
}
}
}
};
$: if (tab) {
selectedId = '';
}
$: if (selectedId) {
getUserValves();
}
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
submitHandler();
dispatch('save');
}}
>
<div class="flex flex-col pr-1.5 overflow-y-scroll max-h-[25rem]">
<div>
<div class="flex items-center justify-between mb-2">
<Tooltip content="">
<div class="text-sm font-medium">
{$i18n.t('Manage Valves')}
</div>
</Tooltip>
<div class=" self-end">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded text-xs bg-transparent outline-none text-right"
bind:value={tab}
placeholder="Select"
>
<option value="tools">{$i18n.t('Tools')}</option>
<option value="functions">{$i18n.t('Functions')}</option>
</select>
</div>
</div>
</div>
<div class="space-y-1">
<div class="flex gap-2">
<div class="flex-1">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedId}
on:change={async () => {
await tick();
}}
>
{#if tab === 'tools'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
>{$i18n.t('Select a tool')}</option
>
{#each $tools as tool, toolIdx}
<option value={tool.id} class="bg-gray-100 dark:bg-gray-700">{tool.name}</option>
{/each}
{:else if tab === 'functions'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
>{$i18n.t('Select a function')}</option
>
{#each $functions as func, funcIdx}
<option value={func.id} class="bg-gray-100 dark:bg-700">{func.name}</option>
{/each}
{/if}
</select>
</div>
</div>
</div>
{#if selectedId}
<hr class="dark:border-gray-800 my-3 w-full" />
<div>
{#if !loading}
{#if valvesSpec}
{#each Object.keys(valvesSpec.properties) as property, idx}
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{valvesSpec.properties[property].title}
{#if (valvesSpec?.required ?? []).includes(property)}
<span class=" text-gray-500">*required</span>
{/if}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
valves[property] = (valves[property] ?? null) === null ? '' : null;
}}
>
{#if (valves[property] ?? null) === null}
<span class="ml-2 self-center">
{#if (valvesSpec?.required ?? []).includes(property)}
{$i18n.t('None')}
{:else}
{$i18n.t('Default')}
{/if}
</span>
{:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div>
{#if (valves[property] ?? null) !== null}
<div class="flex mt-0.5 mb-1.5 space-x-2">
<div class=" flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={valvesSpec.properties[property].title}
bind:value={valves[property]}
autocomplete="off"
required
/>
</div>
</div>
{/if}
{#if (valvesSpec.properties[property]?.description ?? null) !== null}
<div class="text-xs text-gray-500">
{valvesSpec.properties[property].description}
</div>
{/if}
</div>
{/each}
{:else}
<div>No valves</div>
{/if}
{:else}
<Spinner className="size-5" />
{/if}
</div>
{/if}
</div>
<div class="flex justify-end text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
import Personalization from './Settings/Personalization.svelte'; import Personalization from './Settings/Personalization.svelte';
import { updateUserSettings } from '$lib/apis/users'; import { updateUserSettings } from '$lib/apis/users';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Valves from './Settings/Valves.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -65,8 +66,8 @@ ...@@ -65,8 +66,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'general' 'general'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'general'; selectedTab = 'general';
}} }}
...@@ -91,8 +92,8 @@ ...@@ -91,8 +92,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'interface' 'interface'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'interface'; selectedTab = 'interface';
}} }}
...@@ -117,8 +118,8 @@ ...@@ -117,8 +118,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'personalization' 'personalization'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'personalization'; selectedTab = 'personalization';
}} }}
...@@ -132,8 +133,8 @@ ...@@ -132,8 +133,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'audio' 'audio'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'audio'; selectedTab = 'audio';
}} }}
...@@ -156,11 +157,35 @@ ...@@ -156,11 +157,35 @@
<div class=" self-center">{$i18n.t('Audio')}</div> <div class=" self-center">{$i18n.t('Audio')}</div>
</button> </button>
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'valves'
? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'valves';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
d="M18.75 12.75h1.5a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM12 6a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 6ZM12 18a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 18ZM3.75 6.75h1.5a.75.75 0 1 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM5.25 18.75h-1.5a.75.75 0 0 1 0-1.5h1.5a.75.75 0 0 1 0 1.5ZM3 12a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 12ZM9 3.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM12.75 12a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0ZM9 15.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Valves')}</div>
</button>
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'chats' 'chats'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'chats'; selectedTab = 'chats';
}} }}
...@@ -185,8 +210,8 @@ ...@@ -185,8 +210,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'account' 'account'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'account'; selectedTab = 'account';
}} }}
...@@ -212,8 +237,8 @@ ...@@ -212,8 +237,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'admin' 'admin'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={async () => { on:click={async () => {
await goto('/admin/settings'); await goto('/admin/settings');
show = false; show = false;
...@@ -240,8 +265,8 @@ ...@@ -240,8 +265,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'about' 'about'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'about'; selectedTab = 'about';
}} }}
...@@ -293,6 +318,13 @@ ...@@ -293,6 +318,13 @@
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'valves'}
<Valves
{saveSettings}
on:save={() => {
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'chats'} {:else if selectedTab === 'chats'}
<Chats {saveSettings} /> <Chats {saveSettings} />
{:else if selectedTab === 'account'} {:else if selectedTab === 'account'}
......
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
/>
</svg>
...@@ -538,7 +538,9 @@ ...@@ -538,7 +538,9 @@
documentsImportInputElement.click(); documentsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Documents Mapping')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Import Documents Mapping')}
</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -565,7 +567,9 @@ ...@@ -565,7 +567,9 @@
saveAs(blob, `documents-mapping-export-${Date.now()}.json`); saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Documents Mapping')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Documents Mapping')}
</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
......
...@@ -13,13 +13,20 @@ ...@@ -13,13 +13,20 @@
deleteFunctionById, deleteFunctionById,
exportFunctions, exportFunctions,
getFunctionById, getFunctionById,
getFunctions getFunctions,
toggleFunctionById
} from '$lib/apis/functions'; } from '$lib/apis/functions';
import ArrowDownTray from '../icons/ArrowDownTray.svelte'; import ArrowDownTray from '../icons/ArrowDownTray.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import ConfirmDialog from '../common/ConfirmDialog.svelte'; import ConfirmDialog from '../common/ConfirmDialog.svelte';
import { getModels } from '$lib/apis'; import { getModels } from '$lib/apis';
import FunctionMenu from './Functions/FunctionMenu.svelte';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
import Switch from '../common/Switch.svelte';
import ValvesModal from './common/ValvesModal.svelte';
import ManifestModal from './common/ManifestModal.svelte';
import Heart from '../icons/Heart.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -28,6 +35,58 @@ ...@@ -28,6 +35,58 @@
let showConfirm = false; let showConfirm = false;
let query = ''; let query = '';
let showManifestModal = false;
let showValvesModal = false;
let selectedFunction = null;
const shareHandler = async (tool) => {
console.log(tool);
};
const cloneHandler = async (func) => {
const _function = await getFunctionById(localStorage.token, func.id).catch((error) => {
toast.error(error);
return null;
});
if (_function) {
sessionStorage.function = JSON.stringify({
..._function,
id: `${_function.id}_clone`,
name: `${_function.name} (Clone)`
});
goto('/workspace/functions/create');
}
};
const exportHandler = async (func) => {
const _function = await getFunctionById(localStorage.token, func.id).catch((error) => {
toast.error(error);
return null;
});
if (_function) {
let blob = new Blob([JSON.stringify([_function])], {
type: 'application/json'
});
saveAs(blob, `function-${_function.id}-export-${Date.now()}.json`);
}
};
const deleteHandler = async (func) => {
const res = await deleteFunctionById(localStorage.token, func.id).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Function deleted successfully');
functions.set(await getFunctions(localStorage.token));
models.set(await getModels(localStorage.token));
}
};
</script> </script>
<svelte:head> <svelte:head>
...@@ -87,18 +146,14 @@ ...@@ -87,18 +146,14 @@
{#each $functions.filter((f) => query === '' || f.name {#each $functions.filter((f) => query === '' || f.name
.toLowerCase() .toLowerCase()
.includes(query.toLowerCase()) || f.id.toLowerCase().includes(query.toLowerCase())) as func} .includes(query.toLowerCase()) || f.id.toLowerCase().includes(query.toLowerCase())) as func}
<button <div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
type="button"
on:click={() => {
goto(`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`);
}}
> >
<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> <a
<a class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`} href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`}
class="flex items-center text-left" >
> <div class="flex items-center text-left">
<div class=" flex-1 self-center pl-1"> <div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5"> <div class=" font-semibold flex items-center gap-1.5">
<div <div
...@@ -107,67 +162,52 @@ ...@@ -107,67 +162,52 @@
{func.type} {func.type}
</div> </div>
<div> {#if func?.meta?.manifest?.version}
<div
class="text-xs font-black px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
v{func?.meta?.manifest?.version ?? ''}
</div>
{/if}
<div class=" line-clamp-1">
{func.name} {func.name}
</div> </div>
</div> </div>
<div class="flex gap-1.5 px-1"> <div class="flex gap-1.5 px-1">
<div class=" text-gray-500 text-xs font-medium">{func.id}</div> <div class=" text-gray-500 text-xs font-medium flex-shrink-0">{func.id}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{func.meta.description} {func.meta.description}
</div> </div>
</div> </div>
</div> </div>
</a> </div>
</div> </a>
<div class="flex flex-row space-x-1 self-center"> <div class="flex flex-row gap-0.5 self-center">
<Tooltip content="Edit"> {#if func?.meta?.manifest?.funding_url ?? false}
<a <Tooltip content="Support">
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" <button
type="button" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`} type="button"
> on:click={() => {
<svg selectedFunction = func;
xmlns="http://www.w3.org/2000/svg" showManifestModal = true;
fill="none" }}
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
> >
<path <Heart />
stroke-linecap="round" </button>
stroke-linejoin="round" </Tooltip>
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" {/if}
/>
</svg>
</a>
</Tooltip>
<Tooltip content="Clone"> <Tooltip content="Valves">
<button <button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
on:click={async (e) => { on:click={() => {
e.stopPropagation(); selectedFunction = func;
showValvesModal = true;
const _function = await getFunctionById(localStorage.token, func.id).catch(
(error) => {
toast.error(error);
return null;
}
);
if (_function) {
sessionStorage.function = JSON.stringify({
..._function,
id: `${_function.id}_clone`,
name: `${_function.name} (Clone)`
});
goto('/workspace/functions/create');
}
}} }}
> >
<svg <svg
...@@ -176,81 +216,59 @@ ...@@ -176,81 +216,59 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="w-4 h-4" class="size-4"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/> />
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="Export"> <FunctionMenu
editHandler={() => {
goto(`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`);
}}
shareHandler={() => {
shareHandler(func);
}}
cloneHandler={() => {
cloneHandler(func);
}}
exportHandler={() => {
exportHandler(func);
}}
deleteHandler={async () => {
deleteHandler(func);
}}
onClose={() => {}}
>
<button <button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
on:click={async (e) => {
e.stopPropagation();
const _function = await getFunctionById(localStorage.token, func.id).catch(
(error) => {
toast.error(error);
return null;
}
);
if (_function) {
let blob = new Blob([JSON.stringify([_function])], {
type: 'application/json'
});
saveAs(blob, `function-${_function.id}-export-${Date.now()}.json`);
}
}}
> >
<ArrowDownTray /> <EllipsisHorizontal className="size-5" />
</button> </button>
</Tooltip> </FunctionMenu>
<Tooltip content="Delete"> <div class=" self-center mx-1">
<button <Switch
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" bind:state={func.is_active}
type="button" on:change={async (e) => {
on:click={async (e) => { toggleFunctionById(localStorage.token, func.id);
e.stopPropagation(); models.set(await getModels(localStorage.token));
const res = await deleteFunctionById(localStorage.token, func.id).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Function deleted successfully');
functions.set(await getFunctions(localStorage.token));
models.set(await getModels(localStorage.token));
}
}} }}
> />
<svg </div>
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</Tooltip>
</div> </div>
</button> </div>
{/each} {/each}
</div> </div>
...@@ -281,7 +299,7 @@ ...@@ -281,7 +299,7 @@
functionsImportInputElement.click(); functionsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Functions')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Functions')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -315,7 +333,7 @@ ...@@ -315,7 +333,7 @@
} }
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Functions')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Functions')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -335,6 +353,42 @@ ...@@ -335,6 +353,42 @@
</div> </div>
</div> </div>
<div class=" my-16">
<div class=" text-lg font-semibold mb-3 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
</div>
<a
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
href="https://openwebui.com/"
target="_blank"
>
<div class=" self-center w-10 flex-shrink-0">
<div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
<path
fill-rule="evenodd"
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
<div class=" self-center">
<div class=" font-bold line-clamp-1">{$i18n.t('Discover a function')}</div>
<div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom functions')}
</div>
</div>
</a>
</div>
<ManifestModal bind:show={showManifestModal} manifest={selectedFunction?.meta?.manifest ?? {}} />
<ValvesModal bind:show={showValvesModal} type="function" id={selectedFunction?.id ?? null} />
<ConfirmDialog <ConfirmDialog
bind:show={showConfirm} bind:show={showConfirm}
on:confirm={() => { on:confirm={() => {
......
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
const i18n = getContext('i18n');
export let editHandler: Function;
export let shareHandler: Function;
export let cloneHandler: Function;
export let exportHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
let show = false;
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<Tooltip content={$i18n.t('More')}>
<slot />
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
editHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
<div class="flex items-center">{$i18n.t('Edit')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
>
<Share />
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
cloneHandler();
}}
>
<DocumentDuplicate />
<div class="flex items-center">{$i18n.t('Clone')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
exportHandler();
}}
>
<ArrowDownTray />
<div class="flex items-center">{$i18n.t('Export')}</div>
</DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-800 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
deleteHandler();
}}
>
<GarbageBin strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Delete')}</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</div>
</Dropdown>
...@@ -256,7 +256,7 @@ ...@@ -256,7 +256,7 @@
<hr class=" dark:border-gray-850 my-2.5" /> <hr class=" dark:border-gray-850 my-2.5" />
<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create"> <a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create">
<div class=" self-center w-10"> <div class=" self-center w-10 flex-shrink-0">
<div <div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
> >
...@@ -271,8 +271,8 @@ ...@@ -271,8 +271,8 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">{$i18n.t('Create a model')}</div> <div class=" font-bold line-clamp-1">{$i18n.t('Create a model')}</div>
<div class=" text-sm">{$i18n.t('Customize models for a specific purpose')}</div> <div class=" text-sm line-clamp-1">{$i18n.t('Customize models for a specific purpose')}</div>
</div> </div>
</a> </a>
...@@ -412,7 +412,7 @@ ...@@ -412,7 +412,7 @@
modelsImportInputElement.click(); modelsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Models')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -436,7 +436,7 @@ ...@@ -436,7 +436,7 @@
downloadModels($models); downloadModels($models);
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Models')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Models')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -494,14 +494,16 @@ ...@@ -494,14 +494,16 @@
</div> </div>
<div class=" my-16"> <div class=" my-16">
<div class=" text-lg font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div> <div class=" text-lg font-semibold mb-3 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
</div>
<a <a
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
href="https://openwebui.com/" href="https://openwebui.com/"
target="_blank" target="_blank"
> >
<div class=" self-center w-10"> <div class=" self-center w-10 flex-shrink-0">
<div <div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
> >
...@@ -516,8 +518,10 @@ ...@@ -516,8 +518,10 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">{$i18n.t('Discover a model')}</div> <div class=" font-bold line-clamp-1">{$i18n.t('Discover a model')}</div>
<div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div> <div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore model presets')}
</div>
</div> </div>
</a> </a>
</div> </div>
...@@ -8,13 +8,16 @@ ...@@ -8,13 +8,16 @@
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import PromptMenu from './Prompts/PromptMenu.svelte';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let importFiles = ''; let importFiles = '';
let query = ''; let query = '';
let promptsImportInputElement: HTMLInputElement; let promptsImportInputElement: HTMLInputElement;
const sharePrompt = async (prompt) => {
const shareHandler = async (prompt) => {
toast.success($i18n.t('Redirecting you to OpenWebUI Community')); toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com'; const url = 'https://openwebui.com';
...@@ -32,7 +35,20 @@ ...@@ -32,7 +35,20 @@
); );
}; };
const deletePrompt = async (command) => { const cloneHandler = async (prompt) => {
sessionStorage.prompt = JSON.stringify(prompt);
goto('/workspace/prompts/create');
};
const exportHandler = async (prompt) => {
let blob = new Blob([JSON.stringify([prompt])], {
type: 'application/json'
});
saveAs(blob, `prompt-export-${Date.now()}.json`);
};
const deleteHandler = async (prompt) => {
const command = prompt.command;
await deletePromptByCommand(localStorage.token, command); await deletePromptByCommand(localStorage.token, command);
await prompts.set(await getPrompts(localStorage.token)); await prompts.set(await getPrompts(localStorage.token));
}; };
...@@ -99,14 +115,14 @@ ...@@ -99,14 +115,14 @@
<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> <div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}> <a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
<div class=" flex-1 self-center pl-5"> <div class=" flex-1 self-center pl-5">
<div class=" font-bold">{prompt.command}</div> <div class=" font-bold line-clamp-1">{prompt.command}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{prompt.title} {prompt.title}
</div> </div>
</div> </div>
</a> </a>
</div> </div>
<div class="flex flex-row space-x-1 self-center"> <div class="flex flex-row gap-0.5 self-center">
<a <a
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
...@@ -128,76 +144,28 @@ ...@@ -128,76 +144,28 @@
</svg> </svg>
</a> </a>
<button <PromptMenu
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" shareHandler={() => {
type="button" shareHandler(prompt);
on:click={() => {
// console.log(modelfile);
sessionStorage.prompt = JSON.stringify(prompt);
goto('/workspace/prompts/create');
}} }}
> cloneHandler={() => {
<svg cloneHandler(prompt);
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
/>
</svg>
</button>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
sharePrompt(prompt);
}} }}
> exportHandler={() => {
<svg exportHandler(prompt);
xmlns="http://www.w3.org/2000/svg" }}
fill="none" deleteHandler={async () => {
viewBox="0 0 24 24" deleteHandler(prompt);
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
/>
</svg>
</button>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
deletePrompt(prompt.command);
}} }}
onClose={() => {}}
> >
<svg <button
xmlns="http://www.w3.org/2000/svg" class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
fill="none" type="button"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
> >
<path <EllipsisHorizontal className="size-5" />
stroke-linecap="round" </button>
stroke-linejoin="round" </PromptMenu>
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div> </div>
</div> </div>
{/each} {/each}
...@@ -245,7 +213,7 @@ ...@@ -245,7 +213,7 @@
promptsImportInputElement.click(); promptsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Prompts')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Prompts')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -273,7 +241,7 @@ ...@@ -273,7 +241,7 @@
saveAs(blob, `prompts-export-${Date.now()}.json`); saveAs(blob, `prompts-export-${Date.now()}.json`);
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Prompts')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Prompts')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -302,14 +270,16 @@ ...@@ -302,14 +270,16 @@
</div> </div>
<div class=" my-16"> <div class=" my-16">
<div class=" text-lg font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div> <div class=" text-lg font-semibold mb-3 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
</div>
<a <a
class=" flex space-x-4 cursor-pointer w-full mb-3 px-3 py-2" class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
href="https://openwebui.com/?type=prompts" href="https://openwebui.com/"
target="_blank" target="_blank"
> >
<div class=" self-center w-10"> <div class=" self-center w-10 flex-shrink-0">
<div <div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
> >
...@@ -324,8 +294,10 @@ ...@@ -324,8 +294,10 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold">{$i18n.t('Discover a prompt')}</div> <div class=" font-bold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
<div class=" text-sm">{$i18n.t('Discover, download, and explore custom prompts')}</div> <div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom prompts')}
</div>
</div> </div>
</a> </a>
</div> </div>
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
const i18n = getContext('i18n');
export let shareHandler: Function;
export let cloneHandler: Function;
export let exportHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
let show = false;
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<Tooltip content={$i18n.t('More')}>
<slot />
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
>
<Share />
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
cloneHandler();
}}
>
<DocumentDuplicate />
<div class="flex items-center">{$i18n.t('Clone')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
exportHandler();
}}
>
<ArrowDownTray />
<div class="flex items-center">{$i18n.t('Export')}</div>
</DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-800 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
deleteHandler();
}}
>
<GarbageBin strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Delete')}</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</div>
</Dropdown>
...@@ -18,6 +18,11 @@ ...@@ -18,6 +18,11 @@
import ArrowDownTray from '../icons/ArrowDownTray.svelte'; import ArrowDownTray from '../icons/ArrowDownTray.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import ConfirmDialog from '../common/ConfirmDialog.svelte'; import ConfirmDialog from '../common/ConfirmDialog.svelte';
import ToolMenu from './Tools/ToolMenu.svelte';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
import ValvesModal from './common/ValvesModal.svelte';
import ManifestModal from './common/ManifestModal.svelte';
import Heart from '../icons/Heart.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -26,6 +31,56 @@ ...@@ -26,6 +31,56 @@
let showConfirm = false; let showConfirm = false;
let query = ''; let query = '';
let showManifestModal = false;
let showValvesModal = false;
let selectedTool = null;
const shareHandler = async (tool) => {
console.log(tool);
};
const cloneHandler = async (tool) => {
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
toast.error(error);
return null;
});
if (_tool) {
sessionStorage.tool = JSON.stringify({
..._tool,
id: `${_tool.id}_clone`,
name: `${_tool.name} (Clone)`
});
goto('/workspace/tools/create');
}
};
const exportHandler = async (tool) => {
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
toast.error(error);
return null;
});
if (_tool) {
let blob = new Blob([JSON.stringify([_tool])], {
type: 'application/json'
});
saveAs(blob, `tool-${_tool.id}-export-${Date.now()}.json`);
}
};
const deleteHandler = async (tool) => {
const res = await deleteToolById(localStorage.token, tool.id).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Tool deleted successfully');
tools.set(await getTools(localStorage.token));
}
};
</script> </script>
<svelte:head> <svelte:head>
...@@ -85,18 +140,14 @@ ...@@ -85,18 +140,14 @@
{#each $tools.filter((t) => query === '' || t.name {#each $tools.filter((t) => query === '' || t.name
.toLowerCase() .toLowerCase()
.includes(query.toLowerCase()) || t.id.toLowerCase().includes(query.toLowerCase())) as tool} .includes(query.toLowerCase()) || t.id.toLowerCase().includes(query.toLowerCase())) as tool}
<button <div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
type="button"
on:click={() => {
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
}}
> >
<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> <a
<a class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`} href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
class="flex items-center text-left" >
> <div class="flex items-center text-left">
<div class=" flex-1 self-center pl-1"> <div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5"> <div class=" font-semibold flex items-center gap-1.5">
<div <div
...@@ -105,65 +156,52 @@ ...@@ -105,65 +156,52 @@
TOOL TOOL
</div> </div>
<div> {#if tool?.meta?.manifest?.version}
<div
class="text-xs font-black px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
v{tool?.meta?.manifest?.version ?? ''}
</div>
{/if}
<div class="line-clamp-1">
{tool.name} {tool.name}
</div> </div>
</div> </div>
<div class="flex gap-1.5 px-1"> <div class="flex gap-1.5 px-1">
<div class=" text-gray-500 text-xs font-medium">{tool.id}</div> <div class=" text-gray-500 text-xs font-medium flex-shrink-0">{tool.id}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{tool.meta.description} {tool.meta.description}
</div> </div>
</div> </div>
</div> </div>
</a> </div>
</div> </a>
<div class="flex flex-row space-x-1 self-center"> <div class="flex flex-row gap-0.5 self-center">
<Tooltip content="Edit"> {#if tool?.meta?.manifest?.funding_url ?? false}
<a <Tooltip content="Support">
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" <button
type="button" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`} type="button"
> on:click={() => {
<svg selectedTool = tool;
xmlns="http://www.w3.org/2000/svg" showManifestModal = true;
fill="none" }}
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
> >
<path <Heart />
stroke-linecap="round" </button>
stroke-linejoin="round" </Tooltip>
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" {/if}
/>
</svg>
</a>
</Tooltip>
<Tooltip content="Clone"> <Tooltip content="Valves">
<button <button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
on:click={async (e) => { on:click={() => {
e.stopPropagation(); selectedTool = tool;
showValvesModal = true;
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => {
toast.error(error);
return null;
});
if (_tool) {
sessionStorage.tool = JSON.stringify({
..._tool,
id: `${_tool.id}_clone`,
name: `${_tool.name} (Clone)`
});
goto('/workspace/tools/create');
}
}} }}
> >
<svg <svg
...@@ -172,77 +210,49 @@ ...@@ -172,77 +210,49 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="w-4 h-4" class="size-4"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/> />
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="Export"> <ToolMenu
<button editHandler={() => {
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
type="button" }}
on:click={async (e) => { shareHandler={() => {
e.stopPropagation(); shareHandler(tool);
}}
const _tool = await getToolById(localStorage.token, tool.id).catch((error) => { cloneHandler={() => {
toast.error(error); cloneHandler(tool);
return null; }}
}); exportHandler={() => {
exportHandler(tool);
if (_tool) { }}
let blob = new Blob([JSON.stringify([_tool])], { deleteHandler={async () => {
type: 'application/json' deleteHandler(tool);
}); }}
saveAs(blob, `tool-${_tool.id}-export-${Date.now()}.json`); onClose={() => {}}
} >
}}
>
<ArrowDownTray />
</button>
</Tooltip>
<Tooltip content="Delete">
<button <button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button" type="button"
on:click={async (e) => {
e.stopPropagation();
const res = await deleteToolById(localStorage.token, tool.id).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Tool deleted successfully');
tools.set(await getTools(localStorage.token));
}
}}
> >
<svg <EllipsisHorizontal className="size-5" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button> </button>
</Tooltip> </ToolMenu>
</div> </div>
</button> </div>
{/each} {/each}
</div> </div>
...@@ -273,7 +283,7 @@ ...@@ -273,7 +283,7 @@
toolsImportInputElement.click(); toolsImportInputElement.click();
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Tools')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -307,7 +317,7 @@ ...@@ -307,7 +317,7 @@
} }
}} }}
> >
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Tools')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Tools')}</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
...@@ -327,6 +337,42 @@ ...@@ -327,6 +337,42 @@
</div> </div>
</div> </div>
<div class=" my-16">
<div class=" text-lg font-semibold mb-3 line-clamp-1">
{$i18n.t('Made by OpenWebUI Community')}
</div>
<a
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
href="https://openwebui.com/"
target="_blank"
>
<div class=" self-center w-10 flex-shrink-0">
<div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
<path
fill-rule="evenodd"
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
<div class=" self-center">
<div class=" font-bold line-clamp-1">{$i18n.t('Discover a tool')}</div>
<div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom tools')}
</div>
</div>
</a>
</div>
<ValvesModal bind:show={showValvesModal} type="tool" id={selectedTool?.id ?? null} />
<ManifestModal bind:show={showManifestModal} manifest={selectedTool?.meta?.manifest ?? {}} />
<ConfirmDialog <ConfirmDialog
bind:show={showConfirm} bind:show={showConfirm}
on:confirm={() => { on:confirm={() => {
......
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
const i18n = getContext('i18n');
export let editHandler: Function;
export let shareHandler: Function;
export let cloneHandler: Function;
export let exportHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
let show = false;
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<Tooltip content={$i18n.t('More')}>
<slot />
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
editHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
<div class="flex items-center">{$i18n.t('Edit')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
>
<Share />
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
cloneHandler();
}}
>
<DocumentDuplicate />
<div class="flex items-center">{$i18n.t('Clone')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
exportHandler();
}}
>
<ArrowDownTray />
<div class="flex items-center">{$i18n.t('Export')}</div>
</DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-800 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
deleteHandler();
}}
>
<GarbageBin strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Delete')}</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</div>
</Dropdown>
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte';
import Modal from '../../common/Modal.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let show = false;
export let manifest = {};
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Show your support!')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
show = false;
}}
>
<div class="px-1 text-sm">
<div class=" my-2">
The developers behind this plugin are passionate volunteers from the community. If you
find this plugin helpful, please consider contributing to its development.
</div>
<div class=" my-2">
Your entire contribution will go directly to the plugin developer; Open WebUI does not
take any percentage. However, the chosen funding platform might have its own fees.
</div>
<hr class=" dark:border-gray-800 my-3" />
<div class="my-2">
Support this plugin: <a
href={manifest.funding_url}
target="_blank"
class=" underline text-blue-400 hover:text-blue-300">{manifest.funding_url}</a
>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center"
type="submit"
>
{$i18n.t('Done')}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte';
import { addUser } from '$lib/apis/auths';
import Modal from '../../common/Modal.svelte';
import {
getFunctionValvesById,
getFunctionValvesSpecById,
updateFunctionValvesById
} from '$lib/apis/functions';
import { getToolValvesById, getToolValvesSpecById, updateToolValvesById } from '$lib/apis/tools';
import Spinner from '../../common/Spinner.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let show = false;
export let type = 'tool';
export let id = null;
let saving = false;
let loading = false;
let valvesSpec = null;
let valves = {};
const submitHandler = async () => {
saving = true;
if (valvesSpec) {
// Convert string to array
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim());
}
}
let res = null;
if (type === 'tool') {
res = await updateToolValvesById(localStorage.token, id, valves).catch((error) => {
toast.error(error);
});
} else if (type === 'function') {
res = await updateFunctionValvesById(localStorage.token, id, valves).catch((error) => {
toast.error(error);
});
}
if (res) {
toast.success('Valves updated successfully');
}
}
saving = false;
};
const initHandler = async () => {
loading = true;
valves = {};
valvesSpec = null;
if (type === 'tool') {
valves = await getToolValvesById(localStorage.token, id);
valvesSpec = await getToolValvesSpecById(localStorage.token, id);
} else if (type === 'function') {
valves = await getFunctionValvesById(localStorage.token, id);
valvesSpec = await getFunctionValvesSpecById(localStorage.token, id);
}
if (!valves) {
valves = {};
}
if (valvesSpec) {
// Convert array to string
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? []).join(',');
}
}
}
loading = false;
};
$: if (show) {
initHandler();
}
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Valves')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class="px-1">
{#if !loading}
{#if valvesSpec}
{#each Object.keys(valvesSpec.properties) as property, idx}
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{valvesSpec.properties[property].title}
{#if (valvesSpec?.required ?? []).includes(property)}
<span class=" text-gray-500">*required</span>
{/if}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
valves[property] = (valves[property] ?? null) === null ? '' : null;
}}
>
{#if (valves[property] ?? null) === null}
<span class="ml-2 self-center">
{#if (valvesSpec?.required ?? []).includes(property)}
{$i18n.t('None')}
{:else}
{$i18n.t('Default')}
{/if}
</span>
{:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div>
{#if (valves[property] ?? null) !== null}
<div class="flex mt-0.5 mb-1.5 space-x-2">
<div class=" flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={valvesSpec.properties[property].title}
bind:value={valves[property]}
autocomplete="off"
required
/>
</div>
</div>
{/if}
{#if (valvesSpec.properties[property]?.description ?? null) !== null}
<div class="text-xs text-gray-500">
{valvesSpec.properties[property].description}
</div>
{/if}
</div>
{/each}
{:else}
<div class="text-sm">No valves</div>
{/if}
{:else}
<Spinner className="size-5" />
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {saving
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={saving}
>
{$i18n.t('Save')}
{#if saving}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
"Allow Chat Deletion": "يستطيع حذف المحادثات", "Allow Chat Deletion": "يستطيع حذف المحادثات",
"Allow non-local voices": "", "Allow non-local voices": "",
"Allow User Location": "", "Allow User Location": "",
"Allow Voice Interruption in Call": "",
"alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات", "alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات",
"Already have an account?": "هل تملك حساب ؟", "Already have an account?": "هل تملك حساب ؟",
"an assistant": "مساعد", "an assistant": "مساعد",
...@@ -165,9 +166,13 @@ ...@@ -165,9 +166,13 @@
"Deleted {{name}}": "حذف {{name}}", "Deleted {{name}}": "حذف {{name}}",
"Description": "وصف", "Description": "وصف",
"Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل", "Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل",
"Discover a function": "",
"Discover a model": "اكتشف نموذجا", "Discover a model": "اكتشف نموذجا",
"Discover a prompt": "اكتشاف موجه", "Discover a prompt": "اكتشاف موجه",
"Discover a tool": "",
"Discover, download, and explore custom functions": "",
"Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة", "Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة",
"Discover, download, and explore custom tools": "",
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج", "Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
"Dismissible": "", "Dismissible": "",
"Display Emoji in Call": "", "Display Emoji in Call": "",
...@@ -180,6 +185,7 @@ ...@@ -180,6 +185,7 @@
"Don't Allow": "لا تسمح بذلك", "Don't Allow": "لا تسمح بذلك",
"Don't have an account?": "ليس لديك حساب؟", "Don't have an account?": "ليس لديك حساب؟",
"Don't like the style": "لا أحب النمط", "Don't like the style": "لا أحب النمط",
"Done": "",
"Download": "تحميل", "Download": "تحميل",
"Download canceled": "تم اللغاء التحميل", "Download canceled": "تم اللغاء التحميل",
"Download Database": "تحميل قاعدة البيانات", "Download Database": "تحميل قاعدة البيانات",
...@@ -312,6 +318,7 @@ ...@@ -312,6 +318,7 @@
"Manage Models": "إدارة النماذج", "Manage Models": "إدارة النماذج",
"Manage Ollama Models": "Ollama إدارة موديلات ", "Manage Ollama Models": "Ollama إدارة موديلات ",
"Manage Pipelines": "إدارة خطوط الأنابيب", "Manage Pipelines": "إدارة خطوط الأنابيب",
"Manage Valves": "",
"March": "مارس", "March": "مارس",
"Max Tokens (num_predict)": "ماكس توكنز (num_predict)", "Max Tokens (num_predict)": "ماكس توكنز (num_predict)",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.", "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
...@@ -463,10 +470,12 @@ ...@@ -463,10 +470,12 @@
"Seed": "Seed", "Seed": "Seed",
"Select a base model": "حدد نموذجا أساسيا", "Select a base model": "حدد نموذجا أساسيا",
"Select a engine": "", "Select a engine": "",
"Select a function": "",
"Select a mode": "أختار موديل", "Select a mode": "أختار موديل",
"Select a model": "أختار الموديل", "Select a model": "أختار الموديل",
"Select a pipeline": "حدد مسارا", "Select a pipeline": "حدد مسارا",
"Select a pipeline url": "حدد عنوان URL لخط الأنابيب", "Select a pipeline url": "حدد عنوان URL لخط الأنابيب",
"Select a tool": "",
"Select an Ollama instance": "أختار سيرفر ", "Select an Ollama instance": "أختار سيرفر ",
"Select Documents": "", "Select Documents": "",
"Select model": " أختار موديل", "Select model": " أختار موديل",
...@@ -499,6 +508,7 @@ ...@@ -499,6 +508,7 @@
"Show Admin Details in Account Pending Overlay": "", "Show Admin Details in Account Pending Overlay": "",
"Show Model": "", "Show Model": "",
"Show shortcuts": "إظهار الاختصارات", "Show shortcuts": "إظهار الاختصارات",
"Show your support!": "",
"Showcased creativity": "أظهر الإبداع", "Showcased creativity": "أظهر الإبداع",
"sidebar": "الشريط الجانبي", "sidebar": "الشريط الجانبي",
"Sign in": "تسجيل الدخول", "Sign in": "تسجيل الدخول",
...@@ -587,6 +597,7 @@ ...@@ -587,6 +597,7 @@
"Users": "المستخدمين", "Users": "المستخدمين",
"Utilize": "يستخدم", "Utilize": "يستخدم",
"Valid time units:": "وحدات زمنية صالحة:", "Valid time units:": "وحدات زمنية صالحة:",
"Valves": "",
"variable": "المتغير", "variable": "المتغير",
"variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.", "variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.",
"Version": "إصدار", "Version": "إصدار",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment