Unverified Commit 6e843ab5 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #3882 from open-webui/dev

0.3.9
parents eff736ac b3a0d47a
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
import { createOpenAITextStream } from '$lib/apis/streaming'; import { createOpenAITextStream } from '$lib/apis/streaming';
import { queryMemory } from '$lib/apis/memories'; import { queryMemory } from '$lib/apis/memories';
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users'; import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis'; import { chatCompleted, generateTitle, generateSearchQuery, chatAction } from '$lib/apis';
import Banner from '../common/Banner.svelte'; import Banner from '../common/Banner.svelte';
import MessageInput from '$lib/components/chat/MessageInput.svelte'; import MessageInput from '$lib/components/chat/MessageInput.svelte';
...@@ -78,6 +78,8 @@ ...@@ -78,6 +78,8 @@
let showEventConfirmation = false; let showEventConfirmation = false;
let eventConfirmationTitle = ''; let eventConfirmationTitle = '';
let eventConfirmationMessage = ''; let eventConfirmationMessage = '';
let eventConfirmationInput = false;
let eventConfirmationInputPlaceholder = '';
let eventCallback = null; let eventCallback = null;
let showModelSelector = true; let showModelSelector = true;
...@@ -96,6 +98,8 @@ ...@@ -96,6 +98,8 @@
let title = ''; let title = '';
let prompt = ''; let prompt = '';
let chatFiles = [];
let files = []; let files = [];
let messages = []; let messages = [];
let history = { let history = {
...@@ -104,6 +108,7 @@ ...@@ -104,6 +108,7 @@
}; };
let params = {}; let params = {};
let valves = {};
$: if (history.currentId !== null) { $: if (history.currentId !== null) {
let _messages = []; let _messages = [];
...@@ -156,12 +161,27 @@ ...@@ -156,12 +161,27 @@
} else { } else {
message.citations = [data]; message.citations = [data];
} }
} else if (type === 'message') {
message.content += data.content;
} else if (type === 'replace') {
message.content = data.content;
} else if (type === 'confirmation') { } else if (type === 'confirmation') {
eventCallback = cb; eventCallback = cb;
eventConfirmationInput = false;
showEventConfirmation = true; showEventConfirmation = true;
eventConfirmationTitle = data.title; eventConfirmationTitle = data.title;
eventConfirmationMessage = data.message; eventConfirmationMessage = data.message;
} else if (type === 'input') {
eventCallback = cb;
eventConfirmationInput = true;
showEventConfirmation = true;
eventConfirmationTitle = data.title;
eventConfirmationMessage = data.message;
eventConfirmationInputPlaceholder = data.placeholder;
} else { } else {
console.log('Unknown message type', data); console.log('Unknown message type', data);
} }
...@@ -315,6 +335,7 @@ ...@@ -315,6 +335,7 @@
} }
params = chatContent?.params ?? {}; params = chatContent?.params ?? {};
chatFiles = chatContent?.files ?? {};
autoScroll = true; autoScroll = true;
await tick(); await tick();
...@@ -347,7 +368,7 @@ ...@@ -347,7 +368,7 @@
} }
}; };
const chatCompletedHandler = async (modelId, responseMessageId, messages) => { const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
await mermaid.run({ await mermaid.run({
querySelector: '.mermaid' querySelector: '.mermaid'
}); });
...@@ -361,7 +382,7 @@ ...@@ -361,7 +382,7 @@
info: m.info ? m.info : undefined, info: m.info ? m.info : undefined,
timestamp: m.timestamp timestamp: m.timestamp
})), })),
chat_id: $chatId, chat_id: chatId,
session_id: $socket?.id, session_id: $socket?.id,
id: responseMessageId id: responseMessageId
}).catch((error) => { }).catch((error) => {
...@@ -383,6 +404,65 @@ ...@@ -383,6 +404,65 @@
}; };
} }
} }
if ($chatId == chatId) {
if ($settings.saveChatHistory ?? true) {
chat = await updateChatById(localStorage.token, chatId, {
models: selectedModels,
messages: messages,
history: history,
params: params,
files: chatFiles
});
await chats.set(await getChatList(localStorage.token));
}
}
};
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId) => {
const res = await chatAction(localStorage.token, actionId, {
model: modelId,
messages: messages.map((m) => ({
id: m.id,
role: m.role,
content: m.content,
info: m.info ? m.info : undefined,
timestamp: m.timestamp
})),
chat_id: chatId,
session_id: $socket?.id,
id: responseMessageId
}).catch((error) => {
toast.error(error);
messages.at(-1).error = { content: error };
return null;
});
if (res !== null) {
// Update chat history with the new messages
for (const message of res.messages) {
history.messages[message.id] = {
...history.messages[message.id],
...(history.messages[message.id].content !== message.content
? { originalContent: history.messages[message.id].content }
: {}),
...message
};
}
}
if ($chatId == chatId) {
if ($settings.saveChatHistory ?? true) {
chat = await updateChatById(localStorage.token, chatId, {
models: selectedModels,
messages: messages,
history: history,
params: params,
files: chatFiles
});
await chats.set(await getChatList(localStorage.token));
}
}
}; };
const getChatEventEmitter = async (modelId: string, chatId: string = '') => { const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
...@@ -439,6 +519,13 @@ ...@@ -439,6 +519,13 @@
} }
const _files = JSON.parse(JSON.stringify(files)); const _files = JSON.parse(JSON.stringify(files));
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
chatFiles = chatFiles.filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
files = []; files = [];
prompt = ''; prompt = '';
...@@ -679,25 +766,10 @@ ...@@ -679,25 +766,10 @@
} }
}); });
let files = []; let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
files = model.info.meta.knowledge; files.push(...model.info.meta.knowledge);
} }
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? []),
...(responseMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
eventTarget.dispatchEvent( eventTarget.dispatchEvent(
new CustomEvent('chat:start', { new CustomEvent('chat:start', {
...@@ -729,6 +801,7 @@ ...@@ -729,6 +801,7 @@
keep_alive: $settings.keepAlive ?? undefined, keep_alive: $settings.keepAlive ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
files: files.length > 0 ? files : undefined, files: files.length > 0 ? files : undefined,
...(Object.keys(valves).length ? { valves } : {}),
session_id: $socket?.id, session_id: $socket?.id,
chat_id: $chatId, chat_id: $chatId,
id: responseMessageId id: responseMessageId
...@@ -752,7 +825,7 @@ ...@@ -752,7 +825,7 @@
controller.abort('User: Stop Response'); controller.abort('User: Stop Response');
} else { } else {
const messages = createMessagesList(responseMessageId); const messages = createMessagesList(responseMessageId);
await chatCompletedHandler(model.id, responseMessageId, messages); await chatCompletedHandler(_chatId, model.id, responseMessageId, messages);
} }
_response = responseMessage.content; _response = responseMessage.content;
...@@ -860,7 +933,8 @@ ...@@ -860,7 +933,8 @@
messages: messages, messages: messages,
history: history, history: history,
models: selectedModels, models: selectedModels,
params: params params: params,
files: chatFiles
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
...@@ -914,7 +988,7 @@ ...@@ -914,7 +988,7 @@
scrollToBottom(); scrollToBottom();
} }
if (messages.length == 2 && messages.at(1).content !== '') { if (messages.length == 2 && messages.at(1).content !== '' && selectedModels[0] === model.id) {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
const _title = await generateChatTitle(userPrompt); const _title = await generateChatTitle(userPrompt);
await setChatTitle(_chatId, _title); await setChatTitle(_chatId, _title);
...@@ -927,24 +1001,10 @@ ...@@ -927,24 +1001,10 @@
let _response = null; let _response = null;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
let files = []; let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
files = model.info.meta.knowledge; files.push(...model.info.meta.knowledge);
} }
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? []),
...(responseMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
scrollToBottom(); scrollToBottom();
...@@ -1033,6 +1093,7 @@ ...@@ -1033,6 +1093,7 @@
max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined, max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
files: files.length > 0 ? files : undefined, files: files.length > 0 ? files : undefined,
...(Object.keys(valves).length ? { valves } : {}),
session_id: $socket?.id, session_id: $socket?.id,
chat_id: $chatId, chat_id: $chatId,
id: responseMessageId id: responseMessageId
...@@ -1064,7 +1125,7 @@ ...@@ -1064,7 +1125,7 @@
} else { } else {
const messages = createMessagesList(responseMessageId); const messages = createMessagesList(responseMessageId);
await chatCompletedHandler(model.id, responseMessageId, messages); await chatCompletedHandler(_chatId, model.id, responseMessageId, messages);
} }
_response = responseMessage.content; _response = responseMessage.content;
...@@ -1137,7 +1198,8 @@ ...@@ -1137,7 +1198,8 @@
models: selectedModels, models: selectedModels,
messages: messages, messages: messages,
history: history, history: history,
params: params params: params,
files: chatFiles
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
...@@ -1175,7 +1237,7 @@ ...@@ -1175,7 +1237,7 @@
scrollToBottom(); scrollToBottom();
} }
if (messages.length == 2) { if (messages.length == 2 && selectedModels[0] === model.id) {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
const _title = await generateChatTitle(userPrompt); const _title = await generateChatTitle(userPrompt);
...@@ -1408,8 +1470,14 @@ ...@@ -1408,8 +1470,14 @@
bind:show={showEventConfirmation} bind:show={showEventConfirmation}
title={eventConfirmationTitle} title={eventConfirmationTitle}
message={eventConfirmationMessage} message={eventConfirmationMessage}
input={eventConfirmationInput}
inputPlaceholder={eventConfirmationInputPlaceholder}
on:confirm={(e) => { on:confirm={(e) => {
if (e.detail) {
eventCallback(e.detail);
} else {
eventCallback(true); eventCallback(true);
}
}} }}
on:cancel={() => { on:cancel={() => {
eventCallback(false); eventCallback(false);
...@@ -1511,6 +1579,7 @@ ...@@ -1511,6 +1579,7 @@
{sendPrompt} {sendPrompt}
{continueGeneration} {continueGeneration}
{regenerateResponse} {regenerateResponse}
{chatActionHandler}
/> />
</div> </div>
</div> </div>
...@@ -1539,6 +1608,18 @@ ...@@ -1539,6 +1608,18 @@
</div> </div>
</div> </div>
<ChatControls bind:show={showControls} bind:params /> <ChatControls
models={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e);
if (model) {
return [...a, model];
}
return a;
}, [])}
bind:show={showControls}
bind:chatFiles
bind:params
bind:valves
/>
</div> </div>
{/if} {/if}
...@@ -6,7 +6,12 @@ ...@@ -6,7 +6,12 @@
export let show = false; export let show = false;
export let models = [];
export let chatId = null; export let chatId = null;
export let chatFiles = [];
export let valves = {};
export let params = {}; export let params = {};
let largeScreen = false; let largeScreen = false;
...@@ -43,6 +48,9 @@ ...@@ -43,6 +48,9 @@
on:close={() => { on:close={() => {
show = false; show = false;
}} }}
{models}
bind:chatFiles
bind:valves
bind:params bind:params
/> />
</div> </div>
...@@ -56,6 +64,9 @@ ...@@ -56,6 +64,9 @@
on:close={() => { on:close={() => {
show = false; show = false;
}} }}
{models}
bind:chatFiles
bind:valves
bind:params bind:params
/> />
</div> </div>
......
...@@ -5,7 +5,13 @@ ...@@ -5,7 +5,13 @@
import XMark from '$lib/components/icons/XMark.svelte'; import XMark from '$lib/components/icons/XMark.svelte';
import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte'; import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
import Valves from '$lib/components/common/Valves.svelte';
import FileItem from '$lib/components/common/FileItem.svelte';
export let models = [];
export let chatFiles = [];
export let valves = {};
export let params = {}; export let params = {};
</script> </script>
...@@ -23,15 +29,51 @@ ...@@ -23,15 +29,51 @@
</div> </div>
<div class=" dark:text-gray-200 text-sm font-primary"> <div class=" dark:text-gray-200 text-sm font-primary">
{#if chatFiles.length > 0}
<div>
<div class="mb-1.5 font-medium">{$i18n.t('Files')}</div>
<div>
{#each chatFiles as file}
<FileItem
className="w-full"
url={`${file?.url}`}
name={file.name}
type={file.type}
dismissible={true}
on:dismiss={() => {
// Remove the file from the chatFiles array
chatFiles = chatFiles.filter((f) => f.id !== file.id);
}}
/>
{/each}
</div>
</div>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
{/if}
{#if models.length === 1 && models[0]?.pipe?.valves_spec}
<div>
<div class=" font-medium">{$i18n.t('Valves')}</div>
<div>
<Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves />
</div>
</div>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
{/if}
<div> <div>
<div class="mb-1.5 font-medium">System Prompt</div> <div class="mb-1.5 font-medium">{$i18n.t('System Prompt')}</div>
<div> <div>
<textarea <textarea
bind:value={params.system} bind:value={params.system}
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none" class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
rows="3" rows="3"
placeholder="Enter system prompt" placeholder={$i18n.t('Enter system prompt')}
/> />
</div> </div>
</div> </div>
...@@ -39,7 +81,7 @@ ...@@ -39,7 +81,7 @@
<hr class="my-2 border-gray-100 dark:border-gray-800" /> <hr class="my-2 border-gray-100 dark:border-gray-800" />
<div> <div>
<div class="mb-1.5 font-medium">Advanced Params</div> <div class="mb-1.5 font-medium">{$i18n.t('Advanced Params')}</div>
<div> <div>
<AdvancedParams bind:params /> <AdvancedParams bind:params />
......
...@@ -40,6 +40,8 @@ ...@@ -40,6 +40,8 @@
import Headphone from '../icons/Headphone.svelte'; import Headphone from '../icons/Headphone.svelte';
import VoiceRecording from './MessageInput/VoiceRecording.svelte'; import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import { transcribeAudio } from '$lib/apis/audio'; import { transcribeAudio } from '$lib/apis/audio';
import FileItem from '../common/FileItem.svelte';
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -297,24 +299,7 @@ ...@@ -297,24 +299,7 @@
}); });
</script> </script>
{#if dragged} <FilesOverlay show={dragged} />
<div
class="fixed {$showSidebar
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
>
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<AddFilesPlaceholder />
</div>
</div>
</div>
</div>
{/if}
<div class="w-full font-primary"> <div class="w-full font-primary">
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> <div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
...@@ -500,10 +485,10 @@ ...@@ -500,10 +485,10 @@
dir={$settings?.chatDirection ?? 'LTR'} dir={$settings?.chatDirection ?? 'LTR'}
> >
{#if files.length > 0} {#if files.length > 0}
<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2"> <div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2">
{#each files as file, fileIdx} {#each files as file, fileIdx}
<div class=" relative group">
{#if file.type === 'image'} {#if file.type === 'image'}
<div class=" relative group">
<div class="relative"> <div class="relative">
<img <img
src={file.url} src={file.url}
...@@ -534,115 +519,6 @@ ...@@ -534,115 +519,6 @@
</Tooltip> </Tooltip>
{/if} {/if}
</div> </div>
{:else if ['doc', 'file'].includes(file.type)}
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if file.status === 'processed'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else}
<svg
class=" w-6 h-6 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle
class="spinner_qM83"
cx="4"
cy="12"
r="2.5"
/><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle
class="spinner_qM83 spinner_ZTLf"
cx="20"
cy="12"
r="2.5"
/></svg
>
{/if}
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div>
</div>
{:else if file.type === 'collection'}
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
/>
<path
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</div>
{/if}
<div class=" absolute -top-1 -right-1"> <div class=" absolute -top-1 -right-1">
<button <button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition" class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
...@@ -665,6 +541,17 @@ ...@@ -665,6 +541,17 @@
</button> </button>
</div> </div>
</div> </div>
{:else}
<FileItem
name={file.name}
type={file.type}
dismissible={true}
on:dismiss={() => {
files.splice(fileIdx, 1);
files = files;
}}
/>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
......
<script lang="ts">
import { showSidebar } from '$lib/stores';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
export let show = false;
let overlayElement = null;
$: if (show && overlayElement) {
document.body.appendChild(overlayElement);
document.body.style.overflow = 'hidden';
} else if (overlayElement) {
document.body.removeChild(overlayElement);
document.body.style.overflow = 'unset';
}
</script>
{#if show}
<div
bind:this={overlayElement}
class="fixed {$showSidebar
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
: 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-[9999] touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
>
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<AddFilesPlaceholder />
</div>
</div>
</div>
</div>
{/if}
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
export let sendPrompt: Function; export let sendPrompt: Function;
export let continueGeneration: Function; export let continueGeneration: Function;
export let regenerateResponse: Function; export let regenerateResponse: Function;
export let chatActionHandler: Function;
export let user = $_user; export let user = $_user;
export let prompt; export let prompt;
...@@ -335,6 +336,9 @@ ...@@ -335,6 +336,9 @@
copyToClipboard={copyToClipboardWithToast} copyToClipboard={copyToClipboardWithToast}
{continueGeneration} {continueGeneration}
{regenerateResponse} {regenerateResponse}
on:action={async (e) => {
await chatActionHandler(chatId, e.detail, message.model, message.id);
}}
on:save={async (e) => { on:save={async (e) => {
console.log('save', e); console.log('save', e);
......
<script lang="ts"> <script lang="ts">
import { getContext, onMount, tick } from 'svelte'; import { getContext, onMount, tick } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte'; import Modal from '$lib/components/common/Modal.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -54,9 +53,25 @@ ...@@ -54,9 +53,25 @@
<div class="text-sm font-medium dark:text-gray-300"> <div class="text-sm font-medium dark:text-gray-300">
{$i18n.t('Source')} {$i18n.t('Source')}
</div> </div>
{#if document.source?.name}
<div class="text-sm dark:text-gray-400">
<a
href={document?.metadata?.file_id
? `/api/v1/files/${document?.metadata?.file_id}/content`
: document.source.name.includes('http')
? document.source.name
: `#`}
target="_blank"
>
{document?.metadata?.name ?? document.source.name}
</a>
</div>
{:else}
<div class="text-sm dark:text-gray-400"> <div class="text-sm dark:text-gray-400">
{document.source?.name ?? $i18n.t('No source available')} {$i18n.t('No source available')}
</div> </div>
{/if}
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" text-sm font-medium dark:text-gray-300"> <div class=" text-sm font-medium dark:text-gray-300">
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte'; import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import WebSearchResults from './ResponseMessage/WebSearchResults.svelte'; import WebSearchResults from './ResponseMessage/WebSearchResults.svelte';
import Sparkles from '$lib/components/icons/Sparkles.svelte';
export let message; export let message;
export let siblings; export let siblings;
...@@ -54,6 +55,7 @@ ...@@ -54,6 +55,7 @@
export let copyToClipboard: Function; export let copyToClipboard: Function;
export let continueGeneration: Function; export let continueGeneration: Function;
export let regenerateResponse: Function; export let regenerateResponse: Function;
export let chatActionHandler: Function;
let model = null; let model = null;
$: model = $models.find((m) => m.id === message.model); $: model = $models.find((m) => m.id === message.model);
...@@ -1020,6 +1022,33 @@ ...@@ -1020,6 +1022,33 @@
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
{#each model?.actions ?? [] as action}
<Tooltip content={action.name} placement="bottom">
<button
type="button"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => {
dispatch('action', action.id);
}}
>
{#if action.icon_url}
<img
src={action.icon_url}
class="w-4 h-4 {action.icon_url.includes('svg')
? 'dark:invert-[80%]'
: ''}"
style="fill: currentColor;"
alt={action.name}
/>
{:else}
<Sparkles strokeWidth="2.1" className="size-4" />
{/if}
</button>
</Tooltip>
{/each}
{/if} {/if}
{/if} {/if}
{/if} {/if}
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
import { user as _user } from '$lib/stores'; import { user as _user } from '$lib/stores';
import { getFileContentById } from '$lib/apis/files'; import { getFileContentById } from '$lib/apis/files';
import FileItem from '$lib/components/common/FileItem.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -99,106 +100,11 @@ ...@@ -99,106 +100,11 @@
{#if file.type === 'image'} {#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" /> <img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else if file.type === 'file'} {:else if file.type === 'file'}
<button <FileItem url={`${file?.url}/content`} name={file.name} type={$i18n.t('File')} />
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
on:click={async () => {
if (file?.url) {
window.open(`${file?.url}/content`, '_blank').focus();
}
}}
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('File')}</div>
</div>
</button>
{:else if file.type === 'doc'} {:else if file.type === 'doc'}
<button <FileItem url={`${file?.url}`} name={file.name} type={$i18n.t('Document')} />
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
on:click={() => {
if (file?.url) {
window.open(file?.url, '_blank').focus();
}
}}
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div>
</button>
{:else if file.type === 'collection'} {:else if file.type === 'collection'}
<button <FileItem name={file?.title ?? `#${file.name}`} type={$i18n.t('Collection')} />
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
/>
<path
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</button>
{/if} {/if}
</div> </div>
{/each} {/each}
......
<script lang="ts"> <script lang="ts">
import Switch from '$lib/components/common/Switch.svelte';
import { getContext, createEventDispatcher } from 'svelte'; import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -720,10 +721,22 @@ ...@@ -720,10 +721,22 @@
{#if (params?.use_mmap ?? null) === null} {#if (params?.use_mmap ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if (params?.use_mmap ?? null) !== null}
<div class="flex justify-between items-center mt-1">
<div class="text-xs text-gray-500">
{params.use_mmap ? 'Enabled' : 'Disabled'}
</div>
<div class=" pr-2">
<Switch bind:state={params.use_mmap} />
</div>
</div>
{/if}
</div> </div>
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
...@@ -740,10 +753,22 @@ ...@@ -740,10 +753,22 @@
{#if (params?.use_mlock ?? null) === null} {#if (params?.use_mlock ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if (params?.use_mlock ?? null) !== null}
<div class="flex justify-between items-center mt-1">
<div class="text-xs text-gray-500">
{params.use_mlock ? 'Enabled' : 'Disabled'}
</div>
<div class=" pr-2">
<Switch bind:state={params.use_mlock} />
</div>
</div>
{/if}
</div> </div>
<div class=" py-0.5 w-full justify-between"> <div class=" py-0.5 w-full justify-between">
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import Valves from '$lib/components/common/Valves.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -170,91 +171,7 @@ ...@@ -170,91 +171,7 @@
<div> <div>
{#if !loading} {#if !loading}
{#if valvesSpec} <Valves {valvesSpec} bind:valves />
{#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
? valvesSpec.properties[property]?.default ?? ''
: 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}
<!-- {valves[property]} -->
<div class="flex mt-0.5 mb-1.5 space-x-2">
<div class=" flex-1">
{#if valvesSpec.properties[property]?.enum ?? null}
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={valves[property]}
>
{#each valvesSpec.properties[property].enum as option}
<option value={option} selected={option === valves[property]}>
{option}
</option>
{/each}
</select>
{:else if (valvesSpec.properties[property]?.type ?? null) === 'boolean'}
<div class="flex justify-between items-center">
<div class="text-xs text-gray-500">
{valves[property] ? 'Enabled' : 'Disabled'}
</div>
<div class=" pr-2">
<Switch bind:state={valves[property]} />
</div>
</div>
{:else}
<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
/>
{/if}
</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} {:else}
<Spinner className="size-5" /> <Spinner className="size-5" />
{/if} {/if}
......
...@@ -13,9 +13,14 @@ ...@@ -13,9 +13,14 @@
export let cancelLabel = $i18n.t('Cancel'); export let cancelLabel = $i18n.t('Cancel');
export let confirmLabel = $i18n.t('Confirm'); export let confirmLabel = $i18n.t('Confirm');
export let input = false;
export let inputPlaceholder = '';
export let show = false; export let show = false;
let modalElement = null; let modalElement = null;
let mounted = false; let mounted = false;
let inputValue = '';
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
...@@ -73,6 +78,16 @@ ...@@ -73,6 +78,16 @@
{:else} {:else}
{$i18n.t('This action cannot be undone. Do you wish to continue?')} {$i18n.t('This action cannot be undone. Do you wish to continue?')}
{/if} {/if}
{#if input}
<textarea
bind:value={inputValue}
placeholder={inputPlaceholder ? inputPlaceholder : $i18n.t('Enter your message')}
class="w-full mt-2 rounded-lg px-4 py-2 text-sm dark:text-gray-300 dark:bg-gray-900 outline-none resize-none"
rows="3"
required
/>
{/if}
</div> </div>
</slot> </slot>
...@@ -91,7 +106,7 @@ ...@@ -91,7 +106,7 @@
class="bg-gray-900 hover:bg-gray-850 text-gray-100 dark:bg-gray-100 dark:hover:bg-white dark:text-gray-800 font-medium w-full py-2.5 rounded-lg transition" class="bg-gray-900 hover:bg-gray-850 text-gray-100 dark:bg-gray-100 dark:hover:bg-white dark:text-gray-800 font-medium w-full py-2.5 rounded-lg transition"
on:click={() => { on:click={() => {
show = false; show = false;
dispatch('confirm'); dispatch('confirm', inputValue);
}} }}
type="button" type="button"
> >
......
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let className = 'w-72';
export let url: string | null = null;
export let clickHandler: Function | null = null;
export let dismissible = false;
export let status = 'processed';
export let name: string;
export let type: string;
</script>
<div class="relative group">
<button
class="h-14 {className} flex items-center space-x-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-100 dark:border-gray-800 text-left"
type="button"
on:click={async () => {
if (clickHandler === null) {
if (url) {
if (type === 'file') {
window.open(`${url}/content`, '_blank').focus();
} else {
window.open(`${url}`, '_blank').focus();
}
}
} else {
clickHandler();
}
}}
>
<div class="p-4 py-[1.1rem] bg-red-400 text-white rounded-l-xl">
{#if status === 'processed'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class=" size-5"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else}
<svg
class=" size-5 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
{/if}
</div>
<div class="flex flex-col justify-center -space-y-0.5 pl-1.5 pr-4 w-full">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{name}
</div>
<div class=" text-gray-500 text-xs">
{#if type === 'file'}
{$i18n.t('File')}
{:else if type === 'doc'}
{$i18n.t('Document')}
{:else if type === 'collection'}
{$i18n.t('Collection')}
{:else}
<span class=" capitalize">{type}</span>
{/if}
</div>
</div>
</button>
{#if dismissible}
<div class=" absolute -top-1 -right-1">
<button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
dispatch('dismiss');
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<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>
{/if}
</div>
<script lang="ts"> <script lang="ts">
export let value: string = ''; export let value: string = '';
export let placeholder = ''; export let placeholder = '';
export let required = true;
export let readOnly = false; export let readOnly = false;
export let outerClassName = 'flex flex-1'; export let outerClassName = 'flex flex-1';
export let inputClassName = export let inputClassName =
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
class={inputClassName} class={inputClassName}
{placeholder} {placeholder}
bind:value bind:value
required={!readOnly} required={required && !readOnly}
disabled={readOnly} disabled={readOnly}
autocomplete="off" autocomplete="off"
{...{ type: show ? 'text' : 'password' }} {...{ type: show ? 'text' : 'password' }}
......
<script>
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
import Switch from './Switch.svelte';
export let valvesSpec = null;
export let valves = {};
</script>
{#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
? valvesSpec.properties[property]?.default ?? ''
: 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}
<!-- {valves[property]} -->
<div class="flex mt-0.5 mb-1.5 space-x-2">
<div class=" flex-1">
{#if valvesSpec.properties[property]?.enum ?? null}
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none border border-gray-100 dark:border-gray-800"
bind:value={valves[property]}
>
{#each valvesSpec.properties[property].enum as option}
<option value={option} selected={option === valves[property]}>
{option}
</option>
{/each}
</select>
{:else if (valvesSpec.properties[property]?.type ?? null) === 'boolean'}
<div class="flex justify-between items-center">
<div class="text-xs text-gray-500">
{valves[property] ? 'Enabled' : 'Disabled'}
</div>
<div class=" pr-2">
<Switch bind:state={valves[property]} />
</div>
</div>
{:else}
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none border border-gray-100 dark:border-gray-800"
type="text"
placeholder={valvesSpec.properties[property].title}
bind:value={valves[property]}
autocomplete="off"
required
/>
{/if}
</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}
<script lang="ts">
export let className = 'w-4 h-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="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"
/>
</svg>
...@@ -8,14 +8,16 @@ ...@@ -8,14 +8,16 @@
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents'; import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants'; import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
import { uploadDocToVectorDB } from '$lib/apis/rag'; import { processDocToVectorDB, uploadDocToVectorDB } from '$lib/apis/rag';
import { transformFileName } from '$lib/utils'; import { blobToFile, transformFileName } from '$lib/utils';
import Checkbox from '$lib/components/common/Checkbox.svelte'; import Checkbox from '$lib/components/common/Checkbox.svelte';
import EditDocModal from '$lib/components/documents/EditDocModal.svelte'; import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte'; import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import AddDocModal from '$lib/components/documents/AddDocModal.svelte'; import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
import { transcribeAudio } from '$lib/apis/audio';
import { uploadFile } from '$lib/apis/files';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -50,7 +52,28 @@ ...@@ -50,7 +52,28 @@
}; };
const uploadDoc = async (file) => { const uploadDoc = async (file) => {
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => { console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (transcribeRes) {
console.log(transcribeRes);
const blob = new Blob([transcribeRes.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
}
// Upload the file to the server
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
const res = await processDocToVectorDB(localStorage.token, uploadedFile.id).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
}); });
......
...@@ -122,12 +122,17 @@ ...@@ -122,12 +122,17 @@
if (res) { if (res) {
if (func.is_global) { if (func.is_global) {
toast.success($i18n.t('Filter is now globally enabled')); func.type === 'filter'
? toast.success($i18n.t('Filter is now globally enabled'))
: toast.success($i18n.t('Function is now globally enabled'));
} else { } else {
toast.success($i18n.t('Filter is now globally disabled')); func.type === 'filter'
? toast.success($i18n.t('Filter is now globally disabled'))
: toast.success($i18n.t('Function is now globally disabled'));
} }
functions.set(await getFunctions(localStorage.token)); functions.set(await getFunctions(localStorage.token));
models.set(await getModels(localStorage.token));
} }
}; };
</script> </script>
...@@ -230,7 +235,7 @@ ...@@ -230,7 +235,7 @@
</a> </a>
<div class="flex flex-row gap-0.5 self-center"> <div class="flex flex-row gap-0.5 self-center">
{#if func?.meta?.manifest?.funding_url ?? false} {#if func?.meta?.manifest?.funding_url ?? false}
<Tooltip content="Support"> <Tooltip content={$i18n.t('Support')}>
<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"
...@@ -244,7 +249,7 @@ ...@@ -244,7 +249,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
<Tooltip content="Valves"> <Tooltip content={$i18n.t('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"
...@@ -294,7 +299,7 @@ ...@@ -294,7 +299,7 @@
showDeleteConfirm = true; showDeleteConfirm = true;
}} }}
toggleGlobalHandler={() => { toggleGlobalHandler={() => {
if (func.type === 'filter') { if (['filter', 'action'].includes(func.type)) {
toggleGlobalHandler(func); toggleGlobalHandler(func);
} }
}} }}
...@@ -309,7 +314,7 @@ ...@@ -309,7 +314,7 @@
</FunctionMenu> </FunctionMenu>
<div class=" self-center mx-1"> <div class=" self-center mx-1">
<Tooltip content={func.is_active ? 'Enabled' : 'Disabled'}> <Tooltip content={func.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch <Switch
bind:state={func.is_active} bind:state={func.is_active}
on:change={async (e) => { on:change={async (e) => {
...@@ -489,15 +494,15 @@ ...@@ -489,15 +494,15 @@
<div>Please carefully review the following warnings:</div> <div>Please carefully review the following warnings:</div>
<ul class=" mt-1 list-disc pl-4 text-xs"> <ul class=" mt-1 list-disc pl-4 text-xs">
<li>Functions allow arbitrary code execution.</li> <li>{$i18n.t('Functions allow arbitrary code execution.')}</li>
<li>Do not install functions from sources you do not fully trust.</li> <li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li>
</ul> </ul>
</div> </div>
<div class="my-3"> <div class="my-3">
I acknowledge that I have read and I understand the implications of my action. I am aware of {$i18n.t(
the risks associated with executing arbitrary code and I have verified the trustworthiness of 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
the source. )}
</div> </div>
</div> </div>
</ConfirmDialog> </ConfirmDialog>
...@@ -309,7 +309,7 @@ class Pipe: ...@@ -309,7 +309,7 @@ class Pipe:
<input <input
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
type="text" type="text"
placeholder="Function Name (e.g. My Filter)" placeholder={$i18n.t('Function Name (e.g. My Filter)')}
bind:value={name} bind:value={name}
required required
/> />
...@@ -317,7 +317,7 @@ class Pipe: ...@@ -317,7 +317,7 @@ class Pipe:
<input <input
class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
type="text" type="text"
placeholder="Function ID (e.g. my_filter)" placeholder={$i18n.t('Function ID (e.g. my_filter)')}
bind:value={id} bind:value={id}
required required
disabled={edit} disabled={edit}
...@@ -326,7 +326,9 @@ class Pipe: ...@@ -326,7 +326,9 @@ class Pipe:
<input <input
class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none"
type="text" type="text"
placeholder="Function Description (e.g. A filter to remove profanity from text)" placeholder={$i18n.t(
'Function Description (e.g. A filter to remove profanity from text)'
)}
bind:value={meta.description} bind:value={meta.description}
required required
/> />
...@@ -348,10 +350,10 @@ class Pipe: ...@@ -348,10 +350,10 @@ class Pipe:
<div class="pb-3 flex justify-between"> <div class="pb-3 flex justify-between">
<div class="flex-1 pr-3"> <div class="flex-1 pr-3">
<div class="text-xs text-gray-500 line-clamp-2"> <div class="text-xs text-gray-500 line-clamp-2">
<span class=" font-semibold dark:text-gray-200">Warning:</span> Functions allow <span class=" font-semibold dark:text-gray-200">{$i18n.t('Warning:')}</span>
arbitrary code execution <br />— {$i18n.t('Functions allow arbitrary code execution')} <br />—
<span class=" font-medium dark:text-gray-400" <span class=" font-medium dark:text-gray-400"
>don't install random functions from sources you don't trust.</span >{$i18n.t(`don't install random functions from sources you don't trust.`)}</span
> >
</div> </div>
</div> </div>
...@@ -376,18 +378,18 @@ class Pipe: ...@@ -376,18 +378,18 @@ class Pipe:
> >
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500">
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3"> <div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
<div>Please carefully review the following warnings:</div> <div>{$i18n.t('Please carefully review the following warnings:')}</div>
<ul class=" mt-1 list-disc pl-4 text-xs"> <ul class=" mt-1 list-disc pl-4 text-xs">
<li>Functions allow arbitrary code execution.</li> <li>{$i18n.t('Functions allow arbitrary code execution.')}</li>
<li>Do not install functions from sources you do not fully trust.</li> <li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li>
</ul> </ul>
</div> </div>
<div class="my-3"> <div class="my-3">
I acknowledge that I have read and I understand the implications of my action. I am aware of {$i18n.t(
the risks associated with executing arbitrary code and I have verified the trustworthiness of 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.'
the source. )}
</div> </div>
</div> </div>
</ConfirmDialog> </ConfirmDialog>
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
align="start" align="start"
transition={flyAndScale} transition={flyAndScale}
> >
{#if func.type === 'filter'} {#if ['filter', 'action'].includes(func.type)}
<div <div
class="flex gap-2 justify-between items-center px-3 py-2 text-sm font-medium cursor-pointerrounded-md" class="flex gap-2 justify-between items-center px-3 py-2 text-sm font-medium cursor-pointerrounded-md"
> >
......
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