Merge remote-tracking branch 'upstream/dev' into feat/always-web-search

# Conflicts:
#	src/lib/components/chat/Chat.svelte
This commit is contained in:
hurxxxx
2025-02-05 16:56:34 +09:00
138 changed files with 3549 additions and 1700 deletions
@@ -51,7 +51,7 @@
models = [];
} else {
const res = await _getModels(localStorage.token).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res) {
@@ -74,7 +74,7 @@
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res) {
@@ -31,7 +31,8 @@
ENABLE_TAGS_GENERATION: true,
ENABLE_SEARCH_QUERY_GENERATION: true,
ENABLE_RETRIEVAL_QUERY_GENERATION: true,
QUERY_GENERATION_PROMPT_TEMPLATE: ''
QUERY_GENERATION_PROMPT_TEMPLATE: '',
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: ''
};
let promptSuggestions = [];
@@ -251,6 +252,20 @@
</div>
</div>
<div class="mt-3">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</Tooltip>
</div>
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
@@ -25,7 +25,8 @@
'duckduckgo',
'tavily',
'jina',
'bing'
'bing',
'exa'
];
let youtubeLanguage = 'en';
@@ -261,6 +262,17 @@
bind:value={webConfig.search.jina_api_key}
/>
</div>
{:else if webConfig.search.engine === 'exa'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Exa API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Exa API Key')}
bind:value={webConfig.search.exa_api_key}
/>
</div>
{:else if webConfig.search.engine === 'bing'}
<div>
<div class=" self-center text-xs font-medium mb-1">
+2 -1
View File
@@ -61,7 +61,8 @@
},
features: {
web_search: true,
image_generation: true
image_generation: true,
code_interpreter: true
}
};
@@ -45,7 +45,8 @@
},
features: {
web_search: true,
image_generation: true
image_generation: true,
code_interpreter: true
}
};
export let userIds = [];
@@ -22,7 +22,8 @@
},
features: {
web_search: true,
image_generation: true
image_generation: true,
code_interpreter: true
}
};
@@ -257,5 +258,13 @@
<Switch bind:state={permissions.features.image_generation} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Code Interpreter')}
</div>
<Switch bind:state={permissions.features.code_interpreter} />
</div>
</div>
</div>
@@ -6,9 +6,9 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
import { toast } from 'svelte-sonner';
@@ -7,11 +7,11 @@
import { updateUserById } from '$lib/apis/users';
import Modal from '$lib/components/common/Modal.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
export let show = false;
export let selectedUser;
@@ -2,10 +2,10 @@
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { getContext, createEventDispatcher } from 'svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
const dispatch = createEventDispatcher();
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
+35 -33
View File
@@ -200,7 +200,7 @@
files = files.filter((item) => item?.itemId !== tempItemId);
}
} catch (e) {
toast.error(e);
toast.error(`${e}`);
files = files.filter((item) => item?.itemId !== tempItemId);
}
};
@@ -398,7 +398,7 @@
dir={$settings?.chatDirection ?? 'LTR'}
>
{#if files.length > 0}
<div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2">
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
{#each files as file, fileIdx}
{#if file.type === 'image'}
<div class=" relative group">
@@ -411,7 +411,7 @@
</div>
<div class=" absolute -top-1 -right-1">
<button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
files.splice(fileIdx, 1);
@@ -453,35 +453,9 @@
</div>
{/if}
<div class=" flex">
<div class="ml-1 self-end mb-1.5 flex space-x-1">
<InputMenu
{screenCaptureHandler}
uploadFilesHandler={() => {
filesInputElement.click();
}}
>
<button
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
type="button"
aria-label="More"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
/>
</svg>
</button>
</InputMenu>
</div>
<div class="px-2.5">
<div
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
>
<RichTextInput
bind:value={content}
@@ -528,8 +502,36 @@
}}
/>
</div>
</div>
<div class="self-end mb-1.5 flex space-x-1 mr-1">
<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
<div class="ml-1 self-end flex space-x-1">
<InputMenu
{screenCaptureHandler}
uploadFilesHandler={() => {
filesInputElement.click();
}}
>
<button
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none"
type="button"
aria-label="More"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
/>
</svg>
</button>
</InputMenu>
</div>
<div class="self-end flex space-x-1 mr-1">
{#if content === ''}
<Tooltip content={$i18n.t('Record voice')}>
<button
@@ -591,7 +593,7 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-6"
class="size-5"
>
<path
fill-rule="evenodd"
@@ -3,12 +3,12 @@
import relativeTime from 'dayjs/plugin/relativeTime';
import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime);
dayjs.extend(isToday);
dayjs.extend(isYesterday);
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
import { getContext, onMount } from 'svelte';
const i18n = getContext<Writable<i18nType>>('i18n');
@@ -155,10 +155,8 @@
<div
class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
>
<Tooltip
content={dayjs(message.created_at / 1000000).format('LLLL')}
>
{dayjs(message.created_at / 1000000).format('LT')}
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
{dayjs(message.created_at / 1000000).format('HH:mm')}
</Tooltip>
</div>
{/if}
@@ -176,9 +174,7 @@
<div
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
>
<Tooltip
content={dayjs(message.created_at / 1000000).format('LLLL')}
>
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
<span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span>
</Tooltip>
</div>
+77 -69
View File
@@ -41,11 +41,13 @@
convertMessagesToHistory,
copyToClipboard,
getMessageContentParts,
createMessagesList,
extractSentencesForAudio,
promptTemplate,
splitStream,
sleep,
removeDetailsWithReasoning
removeDetails,
getPromptVariables
} from '$lib/utils';
import { generateChatCompletion } from '$lib/apis/ollama';
@@ -116,7 +118,7 @@
let selectedToolIds = [];
let imageGenerationEnabled = false;
let webSearchEnabled = $settings?.alwaysOnWebSearch ?? false;
let codeInterpreterEnabled = false;
let chat = null;
let tags = [];
@@ -224,7 +226,7 @@
}
await tick();
saveChatHandler(_chatId);
saveChatHandler(_chatId, history);
};
const chatEventHandler = async (event, cb) => {
@@ -628,7 +630,7 @@
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
toast.error(`${e}`);
}
};
@@ -825,20 +827,6 @@
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
};
const createMessagesList = (responseMessageId) => {
if (responseMessageId === null) {
return [];
}
const message = history.messages[responseMessageId];
if (message?.parentId) {
return [...createMessagesList(message.parentId), message];
} else {
return [message];
}
};
const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
const res = await chatCompleted(localStorage.token, {
model: modelId,
@@ -895,7 +883,7 @@
};
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
const messages = createMessagesList(responseMessageId);
const messages = createMessagesList(history, responseMessageId);
const res = await chatAction(localStorage.token, actionId, {
model: modelId,
@@ -964,7 +952,7 @@
const modelId = selectedModels[0];
const model = $models.filter((m) => m.id === modelId).at(0);
const messages = createMessagesList(history.currentId);
const messages = createMessagesList(history, history.currentId);
const parentMessage = messages.length !== 0 ? messages.at(-1) : null;
const userMessageId = uuidv4();
@@ -1009,9 +997,9 @@
}
if (messages.length === 0) {
await initChatHandler();
await initChatHandler(history);
} else {
await saveChatHandler($chatId);
await saveChatHandler($chatId, history);
}
}
};
@@ -1073,9 +1061,9 @@
}
if (messages.length === 0) {
await initChatHandler();
await initChatHandler(history);
} else {
await saveChatHandler($chatId);
await saveChatHandler($chatId, history);
}
};
@@ -1209,7 +1197,12 @@
);
history.messages[message.id] = message;
await chatCompletedHandler(chatId, message.model, message.id, createMessagesList(message.id));
await chatCompletedHandler(
chatId,
message.model,
message.id,
createMessagesList(history, message.id)
);
}
console.log(data);
@@ -1225,7 +1218,7 @@
const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
console.log('submitPrompt', userPrompt, $chatId);
const messages = createMessagesList(history.currentId);
const messages = createMessagesList(history, history.currentId);
const _selectedModels = selectedModels.map((modelId) =>
$models.map((m) => m.id).includes(modelId) ? modelId : ''
);
@@ -1273,7 +1266,6 @@
}
prompt = '';
await tick();
// Reset chat input textarea
const chatInputElement = document.getElementById('chat-input');
@@ -1315,34 +1307,25 @@
history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
}
// Wait until history/message have been updated
await tick();
// focus on chat input
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
saveSessionSelectedModels();
await sendPrompt(userPrompt, userMessageId, { newChat: true });
await sendPrompt(history, userPrompt, userMessageId, { newChat: true });
};
const sendPrompt = async (
_history,
prompt: string,
parentId: string,
{ modelId = null, modelIdx = null, newChat = false } = {}
) => {
// Create new chat if newChat is true and first user message
if (
newChat &&
history.messages[history.currentId].parentId === null &&
history.messages[history.currentId].role === 'user'
) {
await initChatHandler();
} else {
await saveChatHandler($chatId);
}
let _chatId = JSON.parse(JSON.stringify($chatId));
_history = JSON.parse(JSON.stringify(_history));
const responseMessageIds: Record<PropertyKey, string> = {};
// If modelId is provided, use it, else use selected model
let selectedModelIds = modelId
? [modelId]
@@ -1351,7 +1334,6 @@
: selectedModels;
// Create response messages for each selected model
const responseMessageIds: Record<PropertyKey, string> = {};
for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
const model = $models.filter((m) => m.id === modelId).at(0);
@@ -1386,19 +1368,26 @@
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`] = responseMessageId;
}
}
history = history;
// Create new chat if newChat is true and first user message
if (newChat && _history.messages[_history.currentId].parentId === null) {
_chatId = await initChatHandler(_history);
}
await tick();
_history = JSON.parse(JSON.stringify(history));
// Save chat after all messages have been created
await saveChatHandler($chatId);
await saveChatHandler(_chatId, _history);
const _chatId = JSON.parse(JSON.stringify($chatId));
await Promise.all(
selectedModelIds.map(async (modelId, _modelIdx) => {
console.log('modelId', modelId);
const model = $models.filter((m) => m.id === modelId).at(0);
if (model) {
const messages = createMessagesList(parentId);
const messages = createMessagesList(_history, parentId);
// If there are image files, check if model is vision capable
const hasImages = messages.some((message) =>
message.files?.some((file) => file.type === 'image')
@@ -1414,7 +1403,7 @@
let responseMessageId =
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`];
let responseMessage = history.messages[responseMessageId];
let responseMessage = _history.messages[responseMessageId];
let userContext = null;
if ($settings?.memory ?? false) {
@@ -1443,7 +1432,7 @@
const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
scrollToBottom();
await sendPromptSocket(model, responseMessageId, _chatId);
await sendPromptSocket(_history, model, responseMessageId, _chatId);
if (chatEventEmitter) clearInterval(chatEventEmitter);
} else {
@@ -1456,9 +1445,9 @@
chats.set(await getChatList(localStorage.token, $currentChatPage));
};
const sendPromptSocket = async (model, responseMessageId, _chatId) => {
const responseMessage = history.messages[responseMessageId];
const userMessage = history.messages[responseMessage.parentId];
const sendPromptSocket = async (_history, model, responseMessageId, _chatId) => {
const responseMessage = _history.messages[responseMessageId];
const userMessage = _history.messages[responseMessage.parentId];
let files = JSON.parse(JSON.stringify(chatFiles));
files.push(
@@ -1506,9 +1495,9 @@
}`
}
: undefined,
...createMessagesList(responseMessageId).map((message) => ({
...createMessagesList(_history, responseMessageId).map((message) => ({
...message,
content: removeDetailsWithReasoning(message.content)
content: removeDetails(message.content, ['reasoning', 'code_interpreter'])
}))
]
.filter((message) => message?.content?.trim())
@@ -1559,10 +1548,18 @@
files: (files?.length ?? 0) > 0 ? files : undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
features: {
image_generation: imageGenerationEnabled,
code_interpreter: codeInterpreterEnabled,
web_search: webSearchEnabled
},
variables: {
...getPromptVariables(
$user.name,
$settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined
)
},
session_id: $socket?.id,
chat_id: $chatId,
@@ -1592,12 +1589,15 @@
},
`${WEBUI_BASE_URL}/api`
).catch((error) => {
console.log(error);
toast.error(`${error}`);
responseMessage.error = {
content: error
};
responseMessage.done = true;
history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
return null;
});
@@ -1695,7 +1695,7 @@
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
await sendPrompt(history, userPrompt, userMessageId);
};
const regenerateResponse = async (message) => {
@@ -1707,11 +1707,11 @@
if ((userMessage?.models ?? [...selectedModels]).length == 1) {
// If user message has only one model selected, sendPrompt automatically selects it for regeneration
await sendPrompt(userPrompt, userMessage.id);
await sendPrompt(history, userPrompt, userMessage.id);
} else {
// If there are multiple models selected, use the model of the response message for regeneration
// e.g. many model chat
await sendPrompt(userPrompt, userMessage.id, {
await sendPrompt(history, userPrompt, userMessage.id, {
modelId: message.model,
modelIdx: message.modelIdx
});
@@ -1733,7 +1733,7 @@
.at(0);
if (model) {
await sendPromptSocket(model, responseMessage.id, _chatId);
await sendPromptSocket(history, model, responseMessage.id, _chatId);
}
}
};
@@ -1776,7 +1776,7 @@
}
}
await saveChatHandler(_chatId);
await saveChatHandler(_chatId, history);
} else {
console.error(res);
}
@@ -1785,42 +1785,48 @@
}
};
const initChatHandler = async () => {
const initChatHandler = async (history) => {
let _chatId = $chatId;
if (!$temporaryChatEnabled) {
chat = await createNewChat(localStorage.token, {
id: $chatId,
id: _chatId,
title: $i18n.t('New Chat'),
models: selectedModels,
system: $settings.system ?? undefined,
params: params,
history: history,
messages: createMessagesList(history.currentId),
messages: createMessagesList(history, history.currentId),
tags: [],
timestamp: Date.now()
});
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
await chatId.set(chat.id);
_chatId = chat.id;
await chatId.set(_chatId);
window.history.replaceState(history.state, '', `/c/${chat.id}`);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
window.history.replaceState(history.state, '', `/c/${_chatId}`);
} else {
_chatId = 'local';
await chatId.set('local');
}
await tick();
return _chatId;
};
const saveChatHandler = async (_chatId) => {
const saveChatHandler = async (_chatId, history) => {
if ($chatId == _chatId) {
if (!$temporaryChatEnabled) {
chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels,
history: history,
messages: createMessagesList(history.currentId),
messages: createMessagesList(history, history.currentId),
params: params,
files: chatFiles
});
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
@@ -1924,7 +1930,7 @@
{/if}
<div class="flex flex-col flex-auto z-10 w-full">
{#if $settings?.landingPageMode === 'chat' || createMessagesList(history.currentId).length > 0}
{#if $settings?.landingPageMode === 'chat' || createMessagesList(history, history.currentId).length > 0}
<div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden"
id="messages-container"
@@ -1964,6 +1970,7 @@
bind:autoScroll
bind:selectedToolIds
bind:imageGenerationEnabled
bind:codeInterpreterEnabled
bind:webSearchEnabled
bind:atSelectedModel
transparentBackground={$settings?.backgroundImageUrl ?? false}
@@ -2015,6 +2022,7 @@
bind:autoScroll
bind:selectedToolIds
bind:imageGenerationEnabled
bind:codeInterpreterEnabled
bind:webSearchEnabled
bind:atSelectedModel
transparentBackground={$settings?.backgroundImageUrl ?? false}
+176 -92
View File
@@ -22,23 +22,27 @@
import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils';
import { transcribeAudio } from '$lib/apis/audio';
import { uploadFile } from '$lib/apis/files';
import { getTools } from '$lib/apis/tools';
import { generateAutoCompletion } from '$lib/apis';
import { deleteFileById } from '$lib/apis/files';
import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
import Tooltip from '../common/Tooltip.svelte';
import InputMenu from './MessageInput/InputMenu.svelte';
import Headphone from '../icons/Headphone.svelte';
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import FileItem from '../common/FileItem.svelte';
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
import Commands from './MessageInput/Commands.svelte';
import XMark from '../icons/XMark.svelte';
import RichTextInput from '../common/RichTextInput.svelte';
import { generateAutoCompletion } from '$lib/apis';
import { error, text } from '@sveltejs/kit';
import Tooltip from '../common/Tooltip.svelte';
import FileItem from '../common/FileItem.svelte';
import Image from '../common/Image.svelte';
import { deleteFileById } from '$lib/apis/files';
import XMark from '../icons/XMark.svelte';
import Headphone from '../icons/Headphone.svelte';
import GlobeAlt from '../icons/GlobeAlt.svelte';
import PhotoSolid from '../icons/PhotoSolid.svelte';
import Photo from '../icons/Photo.svelte';
import CommandLine from '../icons/CommandLine.svelte';
const i18n = getContext('i18n');
@@ -65,6 +69,7 @@
export let imageGenerationEnabled = false;
export let webSearchEnabled = false;
export let codeInterpreterEnabled = false;
$: onChange({
prompt,
@@ -211,7 +216,7 @@
files = files.filter((item) => item?.itemId !== tempItemId);
}
} catch (e) {
toast.error(e);
toast.error(`${e}`);
files = files.filter((item) => item?.itemId !== tempItemId);
}
};
@@ -385,7 +390,7 @@
</div>
<div class="w-full relative">
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || imageGenerationEnabled}
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || imageGenerationEnabled || codeInterpreterEnabled}
<div
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10"
>
@@ -421,23 +426,39 @@
</div>
{/if}
{#if imageGenerationEnabled}
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
<div class="pl-1">
<span class="relative flex size-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
/>
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
</span>
</div>
<div class=" ">{$i18n.t('Image generation')}</div>
</div>
</div>
{/if}
{#if webSearchEnabled}
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
<div class="pl-1">
<span class="relative flex size-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"
/>
<span class="relative inline-flex rounded-full size-2 bg-blue-500" />
</span>
</div>
<div class=" translate-y-[0.5px]">{$i18n.t('Search the internet')}</div>
</div>
</div>
{/if}
{#if imageGenerationEnabled}
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
<div class="pl-1">
<span class="relative flex size-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-teal-400 opacity-75"
/>
<span class="relative inline-flex rounded-full size-2 bg-teal-500" />
</span>
</div>
<div class=" translate-y-[0.5px]">{$i18n.t('Generate an image')}</div>
</div>
</div>
{/if}
{#if codeInterpreterEnabled}
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
<div class="pl-1">
@@ -448,7 +469,7 @@
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
</span>
</div>
<div class=" ">{$i18n.t('Search the web')}</div>
<div class=" translate-y-[0.5px]">{$i18n.t('Execute code for analysis')}</div>
</div>
</div>
{/if}
@@ -568,7 +589,7 @@
dir={$settings?.chatDirection ?? 'LTR'}
>
{#if files.length > 0}
<div class="mx-1 mt-2.5 mb-1 flex items-center flex-wrap gap-2">
<div class="mx-2 mt-2.5 -mb-1 flex items-center flex-wrap gap-2">
{#each files as file, fileIdx}
{#if file.type === 'image'}
<div class=" relative group">
@@ -606,7 +627,7 @@
</div>
<div class=" absolute -top-1 -right-1">
<button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
files.splice(fileIdx, 1);
@@ -656,65 +677,10 @@
</div>
{/if}
<div class=" flex">
<div class="ml-1 self-end mb-1.5 flex space-x-1">
<InputMenu
bind:imageGenerationEnabled
bind:webSearchEnabled
bind:selectedToolIds
{screenCaptureHandler}
uploadFilesHandler={() => {
filesInputElement.click();
}}
uploadGoogleDriveHandler={async () => {
try {
const fileData = await createPicker();
if (fileData) {
const file = new File([fileData.blob], fileData.name, {
type: fileData.blob.type
});
await uploadFileHandler(file);
} else {
console.log('No file was selected from Google Drive');
}
} catch (error) {
console.error('Google Drive Error:', error);
toast.error(
$i18n.t('Error accessing Google Drive: {{error}}', {
error: error.message
})
);
}
}}
onClose={async () => {
await tick();
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
}}
>
<button
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none"
type="button"
aria-label="More"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
/>
</svg>
</button>
</InputMenu>
</div>
<div class="px-2.5">
{#if $settings?.richTextInput ?? true}
<div
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
>
<RichTextInput
bind:this={chatInputElement}
@@ -918,7 +884,7 @@
<textarea
id="chat-input"
bind:this={chatInputElement}
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 rounded-xl resize-none"
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
bind:value={prompt}
on:keypress={(e) => {
@@ -1107,8 +1073,122 @@
}}
/>
{/if}
</div>
<div class="self-end mb-1.5 flex space-x-1 mr-1">
<div class=" flex justify-between mt-1.5 mb-2.5 mx-0.5">
<div class="ml-1 self-end gap-0.5 flex items-center">
<InputMenu
bind:selectedToolIds
{screenCaptureHandler}
{inputFilesHandler}
uploadFilesHandler={() => {
filesInputElement.click();
}}
uploadGoogleDriveHandler={async () => {
try {
const fileData = await createPicker();
if (fileData) {
const file = new File([fileData.blob], fileData.name, {
type: fileData.blob.type
});
await uploadFileHandler(file);
} else {
console.log('No file was selected from Google Drive');
}
} catch (error) {
console.error('Google Drive Error:', error);
toast.error(
$i18n.t('Error accessing Google Drive: {{error}}', {
error: error.message
})
);
}
}}
onClose={async () => {
await tick();
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
}}
>
<button
class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none"
type="button"
aria-label="More"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
/>
</svg>
</button>
</InputMenu>
{#if $_user}
{#if $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)}
<Tooltip content={$i18n.t('Search the internet')} placement="top">
<button
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
type="button"
class="px-1.5 sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {webSearchEnabled
? 'bg-blue-100 dark:bg-blue-500/20 text-blue-500 dark:text-blue-400'
: 'bg-transparent text-gray-600 dark:text-gray-400 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
>
<GlobeAlt className="size-5" strokeWidth="1.75" />
<span
class="hidden sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
>{$i18n.t('Web Search')}</span
>
</button>
</Tooltip>
{/if}
{#if $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)}
<Tooltip content={$i18n.t('Generate an image')} placement="top">
<button
on:click|preventDefault={() =>
(imageGenerationEnabled = !imageGenerationEnabled)}
type="button"
class="px-1.5 sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {imageGenerationEnabled
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
>
<Photo className="size-5" strokeWidth="1.75" />
<span
class="hidden sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
>{$i18n.t('Image')}</span
>
</button>
</Tooltip>
{/if}
{#if $_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter}
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
<button
on:click|preventDefault={() =>
(codeInterpreterEnabled = !codeInterpreterEnabled)}
type="button"
class="px-1.5 sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {codeInterpreterEnabled
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
>
<CommandLine className="size-5" strokeWidth="1.75" />
<span
class="hidden sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
>{$i18n.t('Code Interpreter')}</span
>
</button>
</Tooltip>
{/if}
{/if}
</div>
<div class="self-end flex space-x-1 mr-1">
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
<Tooltip content={$i18n.t('Record voice')}>
<button
@@ -1163,7 +1243,9 @@
<div class=" flex items-center">
<Tooltip content={$i18n.t('Call')}>
<button
class=" bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full p-2 self-center"
class=" {webSearchEnabled
? 'bg-blue-500 text-white hover:bg-blue-400 '
: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100'} transition rounded-full p-1.5 self-center"
type="button"
on:click={async () => {
if (selectedModels.length > 1) {
@@ -1216,7 +1298,9 @@
<button
id="send-message-button"
class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
? webSearchEnabled
? 'bg-blue-500 text-white hover:bg-blue-400 '
: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === ''}
@@ -1225,7 +1309,7 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-6"
class="size-5"
>
<path
fill-rule="evenodd"
@@ -1250,7 +1334,7 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
class="size-5"
>
<path
fill-rule="evenodd"
@@ -15,35 +15,23 @@
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
import PhotoSolid from '$lib/components/icons/PhotoSolid.svelte';
import CommandLineSolid from '$lib/components/icons/CommandLineSolid.svelte';
const i18n = getContext('i18n');
export let screenCaptureHandler: Function;
export let uploadFilesHandler: Function;
export let inputFilesHandler: Function;
export let uploadGoogleDriveHandler: Function;
export let selectedToolIds: string[] = [];
export let webSearchEnabled: boolean;
export let imageGenerationEnabled: boolean;
export let onClose: Function;
let tools = {};
let show = false;
let showImageGeneration = false;
$: showImageGeneration =
$config?.features?.enable_image_generation &&
($user.role === 'admin' || $user?.permissions?.features?.image_generation);
let showWebSearch = false;
$: showWebSearch =
$config?.features?.enable_web_search &&
($user.role === 'admin' || $user?.permissions?.features?.web_search);
$: if (show) {
init();
}
@@ -65,8 +53,31 @@
return a;
}, {});
};
const detectMobile = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
};
function handleFileChange(event) {
const inputFiles = Array.from(event.target?.files);
if (inputFiles && inputFiles.length > 0) {
console.log(inputFiles);
inputFilesHandler(inputFiles);
}
}
</script>
<!-- Hidden file input used to open the camera on mobile -->
<input
id="camera-input"
type="file"
accept="image/*"
capture="environment"
on:change={handleFileChange}
style="display: none;"
/>
<Dropdown
bind:show
on:change={(e) => {
@@ -132,62 +143,32 @@
<hr class="border-black/5 dark:border-white/5 my-1" />
{/if}
{#if showImageGeneration}
<button
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
<Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
className="w-full"
>
<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-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
imageGenerationEnabled = !imageGenerationEnabled;
}}
>
<div class="flex-1 flex items-center gap-2">
<PhotoSolid />
<div class=" line-clamp-1">{$i18n.t('Image')}</div>
</div>
<Switch state={imageGenerationEnabled} />
</button>
{/if}
{#if showWebSearch}
<button
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
on:click={() => {
webSearchEnabled = !webSearchEnabled;
}}
>
<div class="flex-1 flex items-center gap-2">
<GlobeAltSolid />
<div class=" line-clamp-1">{$i18n.t('Web Search')}</div>
</div>
<Switch state={webSearchEnabled} />
</button>
{/if}
{#if showImageGeneration || showWebSearch}
<hr class="border-black/5 dark:border-white/5 my-1" />
{/if}
{#if !$mobile}
<Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
className="w-full"
>
<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-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
if (fileUploadEnabled) {
if (fileUploadEnabled) {
if (!detectMobile()) {
screenCaptureHandler();
} else {
const cameraInputElement = document.getElementById('camera-input');
if (cameraInputElement) {
cameraInputElement.click();
}
}
}}
>
<CameraSolid />
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
</DropdownMenu.Item>
</Tooltip>
{/if}
}
}}
>
<CameraSolid />
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
</DropdownMenu.Item>
</Tooltip>
<Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
+1 -1
View File
@@ -233,7 +233,7 @@
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
await sendPrompt(history, userPrompt, userMessageId);
} else {
// Edit user message
history.messages[messageId].content = content;
@@ -25,6 +25,7 @@
export let token;
export let lang = '';
export let code = '';
export let attributes = {};
export let className = 'my-2';
export let editorClassName = '';
@@ -49,6 +50,7 @@
let stdout = null;
let stderr = null;
let result = null;
let files = null;
let copied = false;
let saved = false;
@@ -109,86 +111,7 @@
};
const executePython = async (code) => {
if (!code.includes('input') && !code.includes('matplotlib')) {
executePythonAsWorker(code);
} else {
result = null;
stdout = null;
stderr = null;
executing = true;
document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
let pyodide = await loadPyodide({
indexURL: '/pyodide/',
stdout: (text) => {
console.log('Python output:', text);
if (stdout) {
stdout += `${text}\n`;
} else {
stdout = `${text}\n`;
}
},
stderr: (text) => {
console.log('An error occurred:', text);
if (stderr) {
stderr += `${text}\n`;
} else {
stderr = `${text}\n`;
}
},
packages: ['micropip']
});
try {
const micropip = pyodide.pyimport('micropip');
// await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
let packages = [
code.includes('requests') ? 'requests' : null,
code.includes('bs4') ? 'beautifulsoup4' : null,
code.includes('numpy') ? 'numpy' : null,
code.includes('pandas') ? 'pandas' : null,
code.includes('matplotlib') ? 'matplotlib' : null,
code.includes('sklearn') ? 'scikit-learn' : null,
code.includes('scipy') ? 'scipy' : null,
code.includes('re') ? 'regex' : null,
code.includes('seaborn') ? 'seaborn' : null
].filter(Boolean);
console.log(packages);
await micropip.install(packages);
result = await pyodide.runPythonAsync(`from js import prompt
def input(p):
return prompt(p)
__builtins__.input = input`);
result = await pyodide.runPython(code);
if (!result) {
result = '[NO OUTPUT]';
}
console.log(result);
console.log(stdout);
console.log(stderr);
const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
if (pltCanvasElement?.innerHTML !== '') {
pltCanvasElement.classList.add('pt-4');
}
} catch (error) {
console.error('Error:', error);
stderr = error;
}
executing = false;
}
executePythonAsWorker(code);
};
const executePythonAsWorker = async (code) => {
@@ -206,7 +129,11 @@ __builtins__.input = input`);
code.includes('sklearn') ? 'scikit-learn' : null,
code.includes('scipy') ? 'scipy' : null,
code.includes('re') ? 'regex' : null,
code.includes('seaborn') ? 'seaborn' : null
code.includes('seaborn') ? 'seaborn' : null,
code.includes('sympy') ? 'sympy' : null,
code.includes('tiktoken') ? 'tiktoken' : null,
code.includes('matplotlib') ? 'matplotlib' : null,
code.includes('pytz') ? 'pytz' : null
].filter(Boolean);
console.log(packages);
@@ -233,7 +160,31 @@ __builtins__.input = input`);
console.log(id, data);
data['stdout'] && (stdout = data['stdout']);
if (data['stdout']) {
stdout = data['stdout'];
const stdoutLines = stdout.split('\n');
for (const [idx, line] of stdoutLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
stdout = stdout.replace(`${line}\n`, ``);
}
}
}
data['stderr'] && (stderr = data['stderr']);
data['result'] && (result = data['result']);
@@ -279,6 +230,36 @@ __builtins__.input = input`);
$: dispatch('code', { lang, code });
$: if (attributes) {
onAttributesUpdate();
}
const onAttributesUpdate = () => {
if (attributes?.output) {
// Create a helper function to unescape HTML entities
const unescapeHtml = (html) => {
const textArea = document.createElement('textarea');
textArea.innerHTML = html;
return textArea.value;
};
try {
// Unescape the HTML-encoded string
const unescapedOutput = unescapeHtml(attributes.output);
// Parse the unescaped string into JSON
const output = JSON.parse(unescapedOutput);
// Assign the parsed values to variables
stdout = output.stdout;
stderr = output.stderr;
result = output.result;
} catch (error) {
console.error('Error:', error);
}
}
};
onMount(async () => {
console.log('codeblock', lang, code);
@@ -376,18 +357,43 @@ __builtins__.input = input`);
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
{#if executing || stdout || stderr || result}
<div
class="bg-gray-50 dark:bg-[#202123] dark:text-white !rounded-b-lg py-4 px-4 flex flex-col gap-2"
>
{#if executing}
<div class=" ">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else}
{#if stdout || stderr}
<div class=" ">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr}</div>
</div>
{/if}
{#if result || files}
<div class=" ">
<div class=" text-gray-500 text-xs mb-1">RESULT</div>
{#if result}
<div class="text-sm">{`${JSON.stringify(result)}`}</div>
{/if}
{#if files}
<div class="flex flex-col gap-2">
{#each files as file}
{#if file.type.startsWith('image')}
<img src={file.data} alt="Output" />
{/if}
{/each}
</div>
{/if}
</div>
{/if}
{/if}
</div>
{/if}
{/if}
@@ -23,6 +23,7 @@
export let id: string;
export let tokens: Token[];
export let top = true;
export let attributes = {};
export let save = false;
export let onSourceClick: Function = () => {};
@@ -83,6 +84,7 @@
{token}
lang={token?.lang ?? ''}
code={token?.text ?? ''}
{attributes}
{save}
on:code={(e) => {
dispatch('code', e.detail);
@@ -195,9 +197,13 @@
</ul>
{/if}
{:else if token.type === 'details'}
<Collapsible title={token.summary} attributes={token?.attributes} className="w-fit space-y-1">
<Collapsible title={token.summary} attributes={token?.attributes} className="w-full space-y-1">
<div class=" mb-1.5" slot="content">
<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
<svelte:self
id={`${id}-${tokenIdx}-d`}
tokens={marked.lexer(token.text)}
attributes={token?.attributes}
/>
</div>
</Collapsible>
{:else if token.type === 'html'}
@@ -78,6 +78,7 @@
{rateMessage}
{actionMessage}
{submitMessage}
{deleteMessage}
{continueResponse}
{regenerateResponse}
{addMessages}
@@ -95,6 +96,7 @@
{rateMessage}
{actionMessage}
{submitMessage}
{deleteMessage}
{continueResponse}
{regenerateResponse}
{mergeResponses}
@@ -16,9 +16,9 @@
import Markdown from './Markdown.svelte';
import Name from './Name.svelte';
import Skeleton from './Skeleton.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
const i18n = getContext('i18n');
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
export let chatId;
export let history;
@@ -34,6 +34,8 @@
export let actionMessage: Function;
export let submitMessage: Function;
export let deleteMessage: Function;
export let continueResponse: Function;
export let regenerateResponse: Function;
export let mergeResponses: Function;
@@ -228,6 +230,7 @@
{editMessage}
{saveMessage}
{rateMessage}
{deleteMessage}
{actionMessage}
{submitMessage}
{continueResponse}
@@ -114,6 +114,7 @@
export let saveMessage: Function;
export let rateMessage: Function;
export let actionMessage: Function;
export let deleteMessage: Function;
export let submitMessage: Function;
export let continueResponse: Function;
@@ -461,6 +462,10 @@
feedbackLoading = false;
};
const deleteMessageHandler = async () => {
deleteMessage(message.id);
};
$: if (!edit) {
(async () => {
await tick();
@@ -1176,6 +1181,36 @@
</button>
</Tooltip>
{#if siblings.length > 1}
<Tooltip content={$i18n.t('Delete')} placement="bottom">
<button
type="button"
id="continue-response-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={() => {
deleteMessageHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.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 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
{/if}
{#if isLastMessage}
{#each model?.actions ?? [] as action}
<Tooltip content={action.name} placement="bottom">
@@ -13,10 +13,10 @@
import FileItem from '$lib/components/common/FileItem.svelte';
import Markdown from './Markdown.svelte';
import Image from '$lib/components/common/Image.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
const i18n = getContext('i18n');
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
export let user;
@@ -35,6 +35,7 @@
export let selectedToolIds = [];
export let imageGenerationEnabled = false;
export let codeInterpreterEnabled = false;
export let webSearchEnabled = false;
let models = [];
@@ -196,6 +197,7 @@
bind:autoScroll
bind:selectedToolIds
bind:imageGenerationEnabled
bind:codeInterpreterEnabled
bind:webSearchEnabled
bind:atSelectedModel
{transparentBackground}
@@ -218,6 +220,7 @@
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ??
[]}
inputValue={prompt}
on:select={(e) => {
selectSuggestionPrompt(e.detail);
}}
@@ -12,6 +12,7 @@
export let params = {
// Advanced
stream_response: null, // Set stream responses for this model individually
function_calling: null,
seed: null,
stop: null,
temperature: null,
@@ -81,6 +82,35 @@
</Tooltip>
</div>
<div>
<Tooltip
content={$i18n.t(
'Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.'
)}
placement="top-start"
className="inline-tooltip"
>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Function Calling')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
params.function_calling = (params?.function_calling ?? null) === null ? 'native' : null;
}}
type="button"
>
{#if params.function_calling === 'native'}
<span class="ml-2 self-center">{$i18n.t('Native')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
</Tooltip>
</div>
<div class=" py-0.5 w-full justify-between">
<Tooltip
content={$i18n.t(
@@ -39,7 +39,7 @@
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res) {
@@ -46,6 +46,7 @@
let params = {
// Advanced
stream_response: null,
function_calling: null,
seed: null,
temperature: null,
frequency_penalty: null,
@@ -11,10 +11,10 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { error } from '@sveltejs/kit';
import EditMemoryModal from './EditMemoryModal.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
const i18n = getContext('i18n');
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
export let show = false;
+113 -35
View File
@@ -1,53 +1,131 @@
<script lang="ts">
import Fuse from 'fuse.js';
import Bolt from '$lib/components/icons/Bolt.svelte';
import { onMount, getContext, createEventDispatcher } from 'svelte';
import { WEBUI_NAME } from '$lib/stores';
import { WEBUI_VERSION } from '$lib/constants';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let suggestionPrompts = [];
export let className = '';
export let inputValue = '';
let prompts = [];
let sortedPrompts = [];
$: prompts = (suggestionPrompts ?? [])
.reduce((acc, current) => [...acc, ...[current]], [])
.sort(() => Math.random() - 0.5);
const fuseOptions = {
keys: ['content', 'title'],
threshold: 0.5
};
let fuse;
let filteredPrompts = [];
// Initialize Fuse
$: fuse = new Fuse(sortedPrompts, fuseOptions);
// Update the filteredPrompts if inputValue changes
// Only increase version if something wirklich geändert hat
$: getFilteredPrompts(inputValue);
// Helper function to check if arrays are the same
// (based on unique IDs oder content)
function arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if ((a[i].id ?? a[i].content) !== (b[i].id ?? b[i].content)) {
return false;
}
}
return true;
}
const getFilteredPrompts = (inputValue) => {
const newFilteredPrompts = inputValue.trim()
? fuse.search(inputValue.trim()).map((result) => result.item)
: sortedPrompts;
// Compare with the oldFilteredPrompts
// If there's a difference, update array + version
if (!arraysEqual(filteredPrompts, newFilteredPrompts)) {
filteredPrompts = newFilteredPrompts;
}
};
$: if (suggestionPrompts) {
sortedPrompts = [...(suggestionPrompts ?? [])].sort(() => Math.random() - 0.5);
getFilteredPrompts(inputValue);
}
</script>
{#if prompts.length > 0}
<div class="mb-1 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
<div class="mb-1 flex gap-1 text-xs font-medium items-center text-gray-400 dark:text-gray-600">
{#if filteredPrompts.length > 0}
<Bolt />
{$i18n.t('Suggested')}
</div>
{/if}
{:else}
<!-- Keine Vorschläge -->
<div class=" h-40 max-h-full overflow-auto scrollbar-none {className}">
{#each prompts as prompt, promptIdx}
<button
class="flex flex-col flex-1 shrink-0 w-full justify-between px-3 py-2 rounded-xl bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition group"
on:click={() => {
dispatch('select', prompt.content);
}}
<div
class="flex w-full text-center items-center justify-center self-start text-gray-400 dark:text-gray-600"
>
<div class="flex flex-col text-left">
{#if prompt.title && prompt.title[0] !== ''}
<div
class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
>
{prompt.title[0]}
</div>
<div class="text-xs text-gray-500 font-normal line-clamp-1">{prompt.title[1]}</div>
{:else}
<div
class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
>
{prompt.content}
</div>
<div class="text-xs text-gray-500 font-normal line-clamp-1">Prompt</div>
{/if}
</div>
</button>
{/each}
{$WEBUI_NAME} ‧ v{WEBUI_VERSION}
</div>
{/if}
</div>
<div class="h-40 overflow-auto scrollbar-none {className} items-start">
{#if filteredPrompts.length > 0}
{#each filteredPrompts as prompt, idx (prompt.id || prompt.content)}
<button
class="waterfall flex flex-col flex-1 shrink-0 w-full justify-between
px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
dark:hover:bg-white/5 transition group"
style="animation-delay: {idx * 60}ms"
on:click={() => dispatch('select', prompt.content)}
>
<div class="flex flex-col text-left">
{#if prompt.title && prompt.title[0] !== ''}
<div
class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
>
{prompt.title[0]}
</div>
<div class="text-xs text-gray-500 font-normal line-clamp-1">
{prompt.title[1]}
</div>
{:else}
<div
class="font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition line-clamp-1"
>
{prompt.content}
</div>
<div class="text-xs text-gray-500 font-normal line-clamp-1">{$i18n.t('Prompt')}</div>
{/if}
</div>
</button>
{/each}
{/if}
</div>
<style>
/* Waterfall animation for the suggestions */
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.waterfall {
opacity: 0;
animation-name: fadeInUp;
animation-duration: 200ms;
animation-fill-mode: forwards;
animation-timing-function: ease;
}
</style>
@@ -80,6 +80,12 @@
{:else}
{$i18n.t('Thinking...')}
{/if}
{:else if attributes?.type === 'code_interpreter'}
{#if attributes?.done === 'true'}
{$i18n.t('Analyzed')}
{:else}
{$i18n.t('Analyzing...')}
{/if}
{:else}
{title}
{/if}
+1 -1
View File
@@ -119,7 +119,7 @@
{#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"
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click|stopPropagation={() => {
dispatch('dismiss');
+32 -51
View File
@@ -3,68 +3,49 @@
export let value = '';
export let placeholder = '';
export let rows = 1;
export let required = false;
export let className =
'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none h-full';
export let onKeydown: Function = () => {};
'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none h-full';
let textareaElement;
$: if (textareaElement) {
if (textareaElement.innerText !== value && value !== '') {
textareaElement.innerText = value ?? '';
}
}
// Adjust height on mount and after setting the element.
onMount(async () => {
await tick();
resize();
requestAnimationFrame(() => {
// setInterveal to cehck until textareaElement is set
const interval = setInterval(() => {
if (textareaElement) {
clearInterval(interval);
resize();
}
}, 100);
});
});
// Handle paste event to ensure only plaintext is pasted
function handlePaste(event: ClipboardEvent) {
event.preventDefault(); // Prevent the default paste action
const clipboardData = event.clipboardData?.getData('text/plain'); // Get plaintext from clipboard
// Insert plaintext into the textarea
document.execCommand('insertText', false, clipboardData);
}
const resize = () => {
if (textareaElement) {
textareaElement.style.height = '';
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
}
};
</script>
<div
contenteditable="true"
<textarea
bind:this={textareaElement}
class="{className} whitespace-pre-wrap relative {value
? !value.trim()
? 'placeholder'
: ''
: 'placeholder'}"
style="field-sizing: content; -moz-user-select: text !important;"
on:input={() => {
const text = textareaElement.innerText;
if (text === '\n') {
value = '';
return;
}
value = text;
bind:value
{placeholder}
class={className}
style="field-sizing: content;"
{rows}
{required}
on:input={(e) => {
resize();
}}
on:focus={() => {
resize();
}}
on:paste={handlePaste}
on:keydown={onKeydown}
data-placeholder={placeholder}
/>
<style>
.placeholder::before {
/* absolute */
position: absolute;
content: attr(data-placeholder);
color: #adb5bd;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
pointer-events: none;
touch-action: none;
}
</style>
@@ -0,0 +1,19 @@
<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="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
@@ -0,0 +1,11 @@
<script lang="ts">
export let className = 'size-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M2.25 6a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3V6Zm3.97.97a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06l-2.25 2.25a.75.75 0 0 1-1.06-1.06l1.72-1.72-1.72-1.72a.75.75 0 0 1 0-1.06Zm4.28 4.28a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 0-1.5h-3Z"
clip-rule="evenodd"
/>
</svg>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
stroke-width={strokeWidth}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m3 16 5-7 6 6.5m6.5 2.5L16 13l-4.286 6M14 10h.01M4 19h16a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1Z"
/>
</svg>
+2 -2
View File
@@ -10,7 +10,7 @@
let showShortcuts = false;
</script>
<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-20">
<div class=" hidden lg:flex fixed bottom-0 right-0 px-1 py-1 z-20">
<button
id="show-shortcuts-button"
class="hidden"
@@ -29,7 +29,7 @@
>
<Tooltip content={$i18n.t('Help')} placement="left">
<button
class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-5 flex items-center justify-center text-[0.7rem] rounded-full"
class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-4 flex items-center justify-center text-[0.7rem] rounded-full"
>
?
</button>
@@ -4,9 +4,9 @@
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { getContext, createEventDispatcher } from 'svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(localizedFormat);
dayjs.extend(localizedFormat);
const dispatch = createEventDispatcher();
@@ -87,7 +87,13 @@
};
const cloneChatHandler = async (id) => {
const res = await cloneChatById(localStorage.token, id).catch((error) => {
const res = await cloneChatById(
localStorage.token,
id,
$i18n.t('Clone of {{TITLE}}', {
TITLE: title
})
).catch((error) => {
toast.error(`${error}`);
return null;
});
+26 -1
View File
@@ -33,6 +33,7 @@
let loading = false;
let stopResponseFlag = false;
let systemTextareaElement: HTMLTextAreaElement;
let messagesContainerElement: HTMLDivElement;
let showSystem = false;
@@ -58,8 +59,29 @@
console.log('stopResponse');
};
const resizeSystemTextarea = async () => {
await tick();
if (systemTextareaElement) {
systemTextareaElement.style.height = '';
systemTextareaElement.style.height = Math.min(systemTextareaElement.scrollHeight, 555) + 'px';
}
};
$: if (showSystem) {
resizeSystemTextarea();
}
const chatCompletionHandler = async () => {
if (selectedModelId === '') {
toast.error($i18n.t('Please select a model.'));
return;
}
const model = $models.find((model) => model.id === selectedModelId);
if (!model) {
selectedModelId = '';
return;
}
const [res, controller] = await chatCompletion(
localStorage.token,
@@ -258,10 +280,13 @@
<div slot="content">
<div class="pt-1 px-1.5">
<textarea
id="system-textarea"
bind:this={systemTextareaElement}
class="w-full h-full bg-transparent resize-none outline-none text-sm"
bind:value={system}
placeholder={$i18n.t("You're a helpful assistant.")}
on:input={() => {
resizeSystemTextarea();
}}
rows="4"
/>
</div>
@@ -0,0 +1,76 @@
<script lang="ts">
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let message;
export let idx;
export let onDelete;
let textAreaElement: HTMLTextAreaElement;
onMount(() => {
textAreaElement.style.height = '';
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
});
</script>
<div class="flex gap-2 group">
<div class="flex items-start pt-1">
<div
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
>
{$i18n.t(message.role)}
</div>
</div>
<div class="flex-1">
<!-- $i18n.t('a user') -->
<!-- $i18n.t('an assistant') -->
<textarea
id="{message.role}-{idx}-textarea"
bind:this={textAreaElement}
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
placeholder={$i18n.t(`Enter {{role}} message here`, {
role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
})}
rows="1"
on:input={(e) => {
textAreaElement.style.height = '';
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
}}
on:focus={(e) => {
textAreaElement.style.height = '';
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}}
bind:value={message.content}
/>
</div>
<div class=" pt-1">
<button
class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
on:click={() => {
onDelete();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</button>
</div>
</div>
@@ -1,77 +1,20 @@
<script lang="ts">
import { onMount, getContext } from 'svelte';
import Message from './Message.svelte';
const i18n = getContext('i18n');
export let messages = [];
let textAreaElement: HTMLTextAreaElement;
onMount(() => {
messages.forEach((message, idx) => {
textAreaElement.style.height = '';
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
});
});
</script>
<div class="py-3 space-y-3">
{#each messages as message, idx}
<div class="flex gap-2 group">
<div class="flex items-start pt-1">
<div
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left rounded-lg transition"
>
{$i18n.t(message.role)}
</div>
</div>
<div class="flex-1">
<!-- $i18n.t('a user') -->
<!-- $i18n.t('an assistant') -->
<textarea
id="{message.role}-{idx}-textarea"
bind:this={textAreaElement}
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
placeholder={$i18n.t(`Enter {{role}} message here`, {
role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
})}
rows="1"
on:input={(e) => {
textAreaElement.style.height = '';
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
}}
on:focus={(e) => {
textAreaElement.style.height = '';
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}}
bind:value={message.content}
/>
</div>
<div class=" pt-1">
<button
class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
on:click={() => {
messages = messages.filter((message, messageIdx) => messageIdx !== idx);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</button>
</div>
</div>
<Message
{message}
{idx}
onDelete={() => {
messages = messages.filter((message, messageIdx) => messageIdx !== idx);
}}
/>
{/each}
</div>
@@ -54,7 +54,7 @@
const deleteHandler = async (item) => {
const res = await deleteKnowledgeById(localStorage.token, item.id).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res) {
@@ -31,7 +31,7 @@
description,
accessControl
).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res) {
@@ -112,10 +112,7 @@
<div class="mt-2">
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
<AccessControl
bind:accessControl
accessRoles={['read', 'write']}
/>
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
</div>
</div>
@@ -149,7 +149,7 @@
try {
const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
toast.error(e);
toast.error(`${e}`);
return null;
});
@@ -169,7 +169,7 @@
toast.error($i18n.t('Failed to upload file.'));
}
} catch (e) {
toast.error(e);
toast.error(`${e}`);
}
};
@@ -339,7 +339,7 @@
const syncDirectoryHandler = async () => {
if ((knowledge?.files ?? []).length > 0) {
const res = await resetKnowledgeById(localStorage.token, id).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res) {
@@ -357,7 +357,7 @@
const addFileHandler = async (fileId) => {
const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
(e) => {
toast.error(e);
toast.error(`${e}`);
return null;
}
);
@@ -386,7 +386,7 @@
}
} catch (e) {
console.error('Error in deleteFileHandler:', e);
toast.error(e);
toast.error(`${e}`);
}
};
@@ -395,7 +395,7 @@
const content = selectedFile.data.content;
const res = updateFileDataContentById(localStorage.token, fileId, content).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
const updatedKnowledge = await updateFileFromKnowledgeById(
@@ -403,7 +403,7 @@
id,
fileId
).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res && updatedKnowledge) {
@@ -430,7 +430,7 @@
description: knowledge.description,
access_control: knowledge.access_control
}).catch((e) => {
toast.error(e);
toast.error(`${e}`);
});
if (res) {
@@ -522,7 +522,7 @@
id = $page.params.id;
const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
toast.error(e);
toast.error(`${e}`);
return null;
});
+1 -1
View File
@@ -60,7 +60,7 @@
const deleteModelHandler = async (model) => {
const res = await deleteModelById(localStorage.token, model.id).catch((e) => {
toast.error(e);
toast.error(`${e}`);
return null;
});
@@ -531,10 +531,7 @@
<div class="my-2">
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
<AccessControl
bind:accessControl
accessRoles={['read', 'write']}
/>
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
</div>
</div>
@@ -136,7 +136,7 @@
<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
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full {saving
? ' cursor-not-allowed'
: ''}"
type="submit"