Merge remote-tracking branch 'upstream/dev' into feat/always-web-search
# Conflicts: # src/lib/components/chat/Chat.svelte
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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') : ''}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user