Merge branch 'dev' into k_reranker
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tags from './common/Tags.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -31,6 +32,7 @@
|
||||
|
||||
let prefixId = '';
|
||||
let enable = true;
|
||||
let tags = [];
|
||||
|
||||
let modelId = '';
|
||||
let modelIds = [];
|
||||
@@ -77,9 +79,9 @@
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
if (!ollama && (!url || !key)) {
|
||||
if (!ollama && !url) {
|
||||
loading = false;
|
||||
toast.error('URL and Key are required');
|
||||
toast.error('URL is required');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,6 +90,7 @@
|
||||
key,
|
||||
config: {
|
||||
enable: enable,
|
||||
tags: tags,
|
||||
prefix_id: prefixId,
|
||||
model_ids: modelIds
|
||||
}
|
||||
@@ -101,6 +104,7 @@
|
||||
url = '';
|
||||
key = '';
|
||||
prefixId = '';
|
||||
tags = [];
|
||||
modelIds = [];
|
||||
};
|
||||
|
||||
@@ -110,6 +114,7 @@
|
||||
key = connection.key;
|
||||
|
||||
enable = connection.config?.enable ?? true;
|
||||
tags = connection.config?.tags ?? [];
|
||||
prefixId = connection.config?.prefix_id ?? '';
|
||||
modelIds = connection.config?.model_ids ?? [];
|
||||
}
|
||||
@@ -179,7 +184,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip content="Verify Connection" className="self-end -mb-1">
|
||||
<Tooltip content={$i18n.t('Verify Connection')} className="self-end -mb-1">
|
||||
<button
|
||||
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
on:click={() => {
|
||||
@@ -218,7 +223,7 @@
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={key}
|
||||
placeholder={$i18n.t('API Key')}
|
||||
required={!ollama}
|
||||
required={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,6 +249,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<Tags
|
||||
bind:tags
|
||||
on:add={(e) => {
|
||||
tags = [
|
||||
...tags,
|
||||
{
|
||||
name: e.detail
|
||||
}
|
||||
];
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
tags = tags.filter((tag) => tag.name !== e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||
@@ -20,6 +21,7 @@
|
||||
|
||||
let showManageModal = false;
|
||||
let showConfigModal = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
</script>
|
||||
|
||||
<AddConnectionModal
|
||||
@@ -31,7 +33,9 @@
|
||||
key: config?.key ?? '',
|
||||
config: config
|
||||
}}
|
||||
{onDelete}
|
||||
onDelete={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
onSubmit={(connection) => {
|
||||
url = connection.url;
|
||||
config = { ...connection.config, key: connection.key };
|
||||
@@ -39,6 +43,14 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirmDialog}
|
||||
on:confirm={() => {
|
||||
onDelete();
|
||||
showConfigModal = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<ManageOllamaModal bind:show={showManageModal} urlIdx={idx} />
|
||||
|
||||
<div class="flex gap-1.5">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
import { connect } from 'socket.io-client';
|
||||
|
||||
@@ -19,8 +20,16 @@
|
||||
export let config = {};
|
||||
|
||||
let showConfigModal = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirmDialog}
|
||||
on:confirm={() => {
|
||||
onDelete();
|
||||
}}
|
||||
/>
|
||||
|
||||
<AddConnectionModal
|
||||
edit
|
||||
bind:show={showConfigModal}
|
||||
@@ -29,7 +38,9 @@
|
||||
key,
|
||||
config
|
||||
}}
|
||||
{onDelete}
|
||||
onDelete={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
onSubmit={(connection) => {
|
||||
url = connection.url;
|
||||
key = connection.key;
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
let contentExtractionEngine = 'default';
|
||||
let tikaServerUrl = '';
|
||||
let showTikaServerUrl = false;
|
||||
let doclingServerUrl = '';
|
||||
let showDoclingServerUrl = false;
|
||||
let documentIntelligenceEndpoint = '';
|
||||
let documentIntelligenceKey = '';
|
||||
let showDocumentIntelligenceConfig = false;
|
||||
@@ -176,6 +178,10 @@
|
||||
toast.error($i18n.t('Tika Server URL required.'));
|
||||
return;
|
||||
}
|
||||
if (contentExtractionEngine === 'docling' && doclingServerUrl === '') {
|
||||
toast.error($i18n.t('Docling Server URL required.'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
contentExtractionEngine === 'document_intelligence' &&
|
||||
(documentIntelligenceEndpoint === '' || documentIntelligenceKey === '')
|
||||
@@ -210,6 +216,7 @@
|
||||
content_extraction: {
|
||||
engine: contentExtractionEngine,
|
||||
tika_server_url: tikaServerUrl,
|
||||
docling_server_url: doclingServerUrl,
|
||||
document_intelligence_config: {
|
||||
key: documentIntelligenceKey,
|
||||
endpoint: documentIntelligenceEndpoint
|
||||
@@ -270,7 +277,10 @@
|
||||
|
||||
contentExtractionEngine = res.content_extraction.engine;
|
||||
tikaServerUrl = res.content_extraction.tika_server_url;
|
||||
doclingServerUrl = res.content_extraction.docling_server_url;
|
||||
|
||||
showTikaServerUrl = contentExtractionEngine === 'tika';
|
||||
showDoclingServerUrl = contentExtractionEngine === 'docling';
|
||||
documentIntelligenceEndpoint = res.content_extraction.document_intelligence_config.endpoint;
|
||||
documentIntelligenceKey = res.content_extraction.document_intelligence_config.key;
|
||||
showDocumentIntelligenceConfig = contentExtractionEngine === 'document_intelligence';
|
||||
@@ -338,6 +348,7 @@
|
||||
>
|
||||
<option value="">{$i18n.t('Default')} </option>
|
||||
<option value="tika">{$i18n.t('Tika')}</option>
|
||||
<option value="docling">{$i18n.t('Docling')}</option>
|
||||
<option value="document_intelligence">{$i18n.t('Document Intelligence')}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -352,6 +363,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if contentExtractionEngine === 'docling'}
|
||||
<div class="flex w-full mt-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('Enter Docling Server URL')}
|
||||
bind:value={doclingServerUrl}
|
||||
/>
|
||||
</div>
|
||||
{:else if contentExtractionEngine === 'document_intelligence'}
|
||||
<div class="my-0.5 flex gap-2 pr-2">
|
||||
<input
|
||||
@@ -388,8 +407,12 @@
|
||||
<div class="flex items-center relative">
|
||||
<Tooltip
|
||||
content={BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||
? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||
? $i18n.t(
|
||||
'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
)
|
||||
: $i18n.t(
|
||||
'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'
|
||||
)}
|
||||
>
|
||||
<Switch bind:state={BYPASS_EMBEDDING_AND_RETRIEVAL} />
|
||||
</Tooltip>
|
||||
@@ -626,8 +649,12 @@
|
||||
<div class="flex items-center relative">
|
||||
<Tooltip
|
||||
content={RAG_FULL_CONTEXT
|
||||
? 'Inject entire contents as context for comprehensive processing, this is recommended for complex queries.'
|
||||
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||
? $i18n.t(
|
||||
'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
)
|
||||
: $i18n.t(
|
||||
'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'
|
||||
)}
|
||||
>
|
||||
<Switch bind:state={RAG_FULL_CONTEXT} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
@@ -44,6 +45,7 @@
|
||||
|
||||
let imageInputElement;
|
||||
let loading = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
|
||||
const addModelHandler = () => {
|
||||
if (selectedModelId) {
|
||||
@@ -115,6 +117,14 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirmDialog}
|
||||
on:confirm={() => {
|
||||
dispatch('delete', model);
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
|
||||
@@ -378,8 +388,7 @@
|
||||
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-950 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
dispatch('delete', model);
|
||||
show = false;
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Delete')}
|
||||
|
||||
@@ -191,11 +191,15 @@
|
||||
}
|
||||
|
||||
if (config.comfyui.COMFYUI_WORKFLOW) {
|
||||
config.comfyui.COMFYUI_WORKFLOW = JSON.stringify(
|
||||
JSON.parse(config.comfyui.COMFYUI_WORKFLOW),
|
||||
null,
|
||||
2
|
||||
);
|
||||
try {
|
||||
config.comfyui.COMFYUI_WORKFLOW = JSON.stringify(
|
||||
JSON.parse(config.comfyui.COMFYUI_WORKFLOW),
|
||||
null,
|
||||
2
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
requiredWorkflowNodes = requiredWorkflowNodes.map((node) => {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
if (modelListElement) {
|
||||
sortable = Sortable.create(modelListElement, {
|
||||
animation: 150,
|
||||
handle: '.item-handle',
|
||||
onUpdate: async (event) => {
|
||||
positionChangeHandler();
|
||||
}
|
||||
@@ -47,7 +48,7 @@
|
||||
<div class=" flex gap-2 w-full justify-between items-center" id="model-item-{modelId}">
|
||||
<Tooltip content={modelId} placement="top-start">
|
||||
<div class="flex items-center gap-1">
|
||||
<EllipsisVertical className="size-4 cursor-move" />
|
||||
<EllipsisVertical className="size-4 cursor-move item-handle" />
|
||||
|
||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||
{#if $models.find((model) => model.id === modelId)}
|
||||
|
||||
@@ -462,8 +462,12 @@
|
||||
<div class="flex items-center relative">
|
||||
<Tooltip
|
||||
content={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||
? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||
? $i18n.t(
|
||||
'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
)
|
||||
: $i18n.t(
|
||||
'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'
|
||||
)}
|
||||
>
|
||||
<Switch bind:state={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Users from './Users.svelte';
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -25,6 +26,7 @@
|
||||
|
||||
let selectedTab = 'general';
|
||||
let loading = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
|
||||
export let name = '';
|
||||
export let description = '';
|
||||
@@ -88,6 +90,14 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirmDialog}
|
||||
on:confirm={() => {
|
||||
onDelete();
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal size="md" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
@@ -263,18 +273,19 @@
|
||||
{/if}
|
||||
</div> -->
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||
<div class="flex justify-between pt-3 text-sm font-medium gap-1.5">
|
||||
{#if edit}
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
onDelete();
|
||||
show = false;
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Delete')}
|
||||
</button>
|
||||
{:else}
|
||||
<div></div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -19,6 +20,8 @@
|
||||
export let user;
|
||||
|
||||
let chats = null;
|
||||
let showDeleteConfirmDialog = false;
|
||||
let chatToDelete = null;
|
||||
|
||||
const deleteChatHandler = async (chatId) => {
|
||||
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
|
||||
@@ -50,6 +53,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirmDialog}
|
||||
on:confirm={() => {
|
||||
if (chatToDelete) {
|
||||
deleteChatHandler(chatToDelete);
|
||||
chatToDelete = null;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal size="lg" bind:show>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
||||
<div class=" text-lg font-medium self-center capitalize">
|
||||
@@ -142,7 +155,8 @@
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
on:click={async () => {
|
||||
deleteChatHandler(chat.id);
|
||||
chatToDelete = chat.id;
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -73,10 +73,13 @@
|
||||
<div class="text-2xl font-medium capitalize">{channel.name}</div>
|
||||
|
||||
<div class=" text-gray-500">
|
||||
This channel was created on {dayjs(channel.created_at / 1000000).format(
|
||||
'MMMM D, YYYY'
|
||||
)}. This is the very beginning of the {channel.name}
|
||||
channel.
|
||||
{$i18n.t(
|
||||
'This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.',
|
||||
{
|
||||
createdAt: dayjs(channel.created_at / 1000000).format('MMMM D, YYYY'),
|
||||
channelName: channel.name
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -212,7 +212,14 @@
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
let _messageId = JSON.parse(JSON.stringify(message.id));
|
||||
|
||||
let messageChildrenIds = history.messages[_messageId].childrenIds;
|
||||
let messageChildrenIds = [];
|
||||
if (_messageId === null) {
|
||||
messageChildrenIds = Object.keys(history.messages).filter(
|
||||
(id) => history.messages[id].parentId === null
|
||||
);
|
||||
} else {
|
||||
messageChildrenIds = history.messages[_messageId].childrenIds;
|
||||
}
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
_messageId = messageChildrenIds.at(-1);
|
||||
@@ -286,18 +293,10 @@
|
||||
} else if (type === 'chat:tags') {
|
||||
chat = await getChatById(localStorage.token, $chatId);
|
||||
allTags.set(await getAllTags(localStorage.token));
|
||||
} else if (type === 'message') {
|
||||
} else if (type === 'chat:message:delta' || type === 'message') {
|
||||
message.content += data.content;
|
||||
} else if (type === 'replace') {
|
||||
} else if (type === 'chat:message' || type === 'replace') {
|
||||
message.content = data.content;
|
||||
} else if (type === 'action') {
|
||||
if (data.action === 'continue') {
|
||||
const continueButton = document.getElementById('continue-response-button');
|
||||
|
||||
if (continueButton) {
|
||||
continueButton.click();
|
||||
}
|
||||
}
|
||||
} else if (type === 'confirmation') {
|
||||
eventCallback = cb;
|
||||
|
||||
@@ -887,6 +886,8 @@
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
|
||||
taskId = null;
|
||||
};
|
||||
|
||||
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
|
||||
@@ -1276,12 +1277,13 @@
|
||||
prompt = '';
|
||||
|
||||
// Reset chat input textarea
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
if (!($settings?.richTextInput ?? true)) {
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
if (chatInputElement) {
|
||||
await tick();
|
||||
chatInputElement.style.height = '';
|
||||
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 320) + 'px';
|
||||
if (chatInputElement) {
|
||||
await tick();
|
||||
chatInputElement.style.height = '';
|
||||
}
|
||||
}
|
||||
|
||||
const _files = JSON.parse(JSON.stringify(files));
|
||||
|
||||
@@ -30,45 +30,45 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin' || $user?.permissions.chat?.controls}
|
||||
<div class=" dark:text-gray-200 text-sm font-primary py-0.5 px-0.5">
|
||||
{#if chatFiles.length > 0}
|
||||
<Collapsible title={$i18n.t('Files')} open={true} buttonClassName="w-full">
|
||||
<div class="flex flex-col gap-1 mt-1.5" slot="content">
|
||||
{#each chatFiles as file, fileIdx}
|
||||
<FileItem
|
||||
className="w-full"
|
||||
item={file}
|
||||
edit={true}
|
||||
url={file?.url ? file.url : null}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
size={file?.size}
|
||||
dismissible={true}
|
||||
on:dismiss={() => {
|
||||
// Remove the file from the chatFiles array
|
||||
<div class=" dark:text-gray-200 text-sm font-primary py-0.5 px-0.5">
|
||||
{#if chatFiles.length > 0}
|
||||
<Collapsible title={$i18n.t('Files')} open={true} buttonClassName="w-full">
|
||||
<div class="flex flex-col gap-1 mt-1.5" slot="content">
|
||||
{#each chatFiles as file, fileIdx}
|
||||
<FileItem
|
||||
className="w-full"
|
||||
item={file}
|
||||
edit={true}
|
||||
url={file?.url ? file.url : null}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
size={file?.size}
|
||||
dismissible={true}
|
||||
on:dismiss={() => {
|
||||
// Remove the file from the chatFiles array
|
||||
|
||||
chatFiles.splice(fileIdx, 1);
|
||||
chatFiles = chatFiles;
|
||||
}}
|
||||
on:click={() => {
|
||||
console.log(file);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||
{/if}
|
||||
|
||||
<Collapsible bind:open={showValves} title={$i18n.t('Valves')} buttonClassName="w-full">
|
||||
<div class="text-sm" slot="content">
|
||||
<Valves show={showValves} />
|
||||
chatFiles.splice(fileIdx, 1);
|
||||
chatFiles = chatFiles;
|
||||
}}
|
||||
on:click={() => {
|
||||
console.log(file);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||
{/if}
|
||||
|
||||
<Collapsible bind:open={showValves} title={$i18n.t('Valves')} buttonClassName="w-full">
|
||||
<div class="text-sm" slot="content">
|
||||
<Valves show={showValves} />
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
{#if $user.role === 'admin' || $user?.permissions.chat?.controls}
|
||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||
|
||||
<Collapsible title={$i18n.t('System Prompt')} open={true} buttonClassName="w-full">
|
||||
<div class="" slot="content">
|
||||
@@ -90,10 +90,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm dark:text-gray-300 text-center py-2 px-10">
|
||||
{$i18n.t('You do not have permission to access this feature.')}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="line-clamp-1">
|
||||
{item?.name}
|
||||
{decodeURIComponent(item?.name)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -120,7 +120,12 @@
|
||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||
}
|
||||
|
||||
prompt = text;
|
||||
const promptWords = prompt.split(' ');
|
||||
|
||||
promptWords.pop();
|
||||
promptWords.push(`${text}`);
|
||||
|
||||
prompt = promptWords.join(' ');
|
||||
|
||||
const chatInputContainerElement = document.getElementById('chat-input-container');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<div class="flex text-xs font-medium flex-wrap">
|
||||
{#each citations as citation, idx}
|
||||
<button
|
||||
id={`source-${id}-${idx}`}
|
||||
id={`source-${id}-${idx + 1}`}
|
||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
@@ -117,14 +117,14 @@
|
||||
<div
|
||||
class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
|
||||
>
|
||||
{citation.source.name}
|
||||
{decodeURIComponent(citation.source.name)}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Collapsible
|
||||
id="collapsible-sources"
|
||||
id={`collapsible-${id}`}
|
||||
bind:open={isCollapsibleOpen}
|
||||
className="w-full max-w-full "
|
||||
buttonClassName="w-fit max-w-full"
|
||||
@@ -157,7 +157,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-1 truncate">
|
||||
{citation.source.name}
|
||||
{decodeURIComponent(citation.source.name)}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -181,7 +181,7 @@
|
||||
<div class="flex text-xs font-medium flex-wrap">
|
||||
{#each citations as citation, idx}
|
||||
<button
|
||||
id={`source-${id}-${idx}`}
|
||||
id={`source-${id}-${idx + 1}`}
|
||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
@@ -194,7 +194,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-1 truncate">
|
||||
{citation.source.name}
|
||||
{decodeURIComponent(citation.source.name)}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
: `#`}
|
||||
target="_blank"
|
||||
>
|
||||
{document?.metadata?.name ?? document.source.name}
|
||||
{decodeURIComponent(document?.metadata?.name ?? document.source.name)}
|
||||
</a>
|
||||
{#if document?.metadata?.page}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
@@ -128,11 +128,11 @@
|
||||
{percentage.toFixed(2)}%
|
||||
</span>
|
||||
<span class="text-gray-500 dark:text-gray-500">
|
||||
({document.distance.toFixed(4)})
|
||||
({(document?.distance ?? 0).toFixed(4)})
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-gray-500 dark:text-gray-500">
|
||||
{document.distance.toFixed(4)}
|
||||
{(document?.distance ?? 0).toFixed(4)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
export let save = false;
|
||||
export let run = true;
|
||||
export let collapsed = false;
|
||||
|
||||
export let token;
|
||||
export let lang = '';
|
||||
@@ -60,7 +61,6 @@
|
||||
let result = null;
|
||||
let files = null;
|
||||
|
||||
let collapsed = false;
|
||||
let copied = false;
|
||||
let saved = false;
|
||||
|
||||
@@ -441,7 +441,9 @@
|
||||
|
||||
{#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))}
|
||||
{#if executing}
|
||||
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
||||
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">
|
||||
{$i18n.t('Running')}
|
||||
</div>
|
||||
{:else if run}
|
||||
<button
|
||||
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<script lang="ts" context="module">
|
||||
import { marked, type Token } from 'marked';
|
||||
|
||||
type AlertType = 'NOTE' | 'TIP' | 'IMPORTANT' | 'WARNING' | 'CAUTION';
|
||||
|
||||
interface AlertTheme {
|
||||
border: string;
|
||||
text: string;
|
||||
icon: ComponentType;
|
||||
}
|
||||
|
||||
export interface AlertData {
|
||||
type: AlertType;
|
||||
text: string;
|
||||
tokens: Token[];
|
||||
}
|
||||
|
||||
const alertStyles: Record<AlertType, AlertTheme> = {
|
||||
NOTE: {
|
||||
border: 'border-sky-500',
|
||||
text: 'text-sky-500',
|
||||
icon: Info
|
||||
},
|
||||
TIP: {
|
||||
border: 'border-emerald-500',
|
||||
text: 'text-emerald-500',
|
||||
icon: LightBlub
|
||||
},
|
||||
IMPORTANT: {
|
||||
border: 'border-purple-500',
|
||||
text: 'text-purple-500',
|
||||
icon: Star
|
||||
},
|
||||
WARNING: {
|
||||
border: 'border-yellow-500',
|
||||
text: 'text-yellow-500',
|
||||
icon: ArrowRightCircle
|
||||
},
|
||||
CAUTION: {
|
||||
border: 'border-rose-500',
|
||||
text: 'text-rose-500',
|
||||
icon: Bolt
|
||||
}
|
||||
};
|
||||
|
||||
export function alertComponent(token: Token): AlertData | false {
|
||||
const regExpStr = `^(?:\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\])\\s*?\n*`;
|
||||
const regExp = new RegExp(regExpStr);
|
||||
const matches = token.text?.match(regExp);
|
||||
|
||||
if (matches && matches.length) {
|
||||
const alertType = matches[1] as AlertType;
|
||||
const newText = token.text.replace(regExp, '');
|
||||
const newTokens = marked.lexer(newText);
|
||||
return {
|
||||
type: alertType,
|
||||
text: newText,
|
||||
tokens: newTokens
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Info from '$lib/components/icons/Info.svelte';
|
||||
import Star from '$lib/components/icons/Star.svelte';
|
||||
import LightBlub from '$lib/components/icons/LightBlub.svelte';
|
||||
import Bolt from '$lib/components/icons/Bolt.svelte';
|
||||
import ArrowRightCircle from '$lib/components/icons/ArrowRightCircle.svelte';
|
||||
import MarkdownTokens from './MarkdownTokens.svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
export let token: Token;
|
||||
export let alert: AlertData;
|
||||
export let id = '';
|
||||
export let tokenIdx = 0;
|
||||
export let onTaskClick: ((event: MouseEvent) => void) | undefined = undefined;
|
||||
export let onSourceClick: ((event: MouseEvent) => void) | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<!--
|
||||
|
||||
Renders the following Markdown as alerts:
|
||||
|
||||
> [!NOTE]
|
||||
> Example note
|
||||
|
||||
> [!TIP]
|
||||
> Example tip
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Example important
|
||||
|
||||
> [!CAUTION]
|
||||
> Example caution
|
||||
|
||||
> [!WARNING]
|
||||
> Example warning
|
||||
|
||||
-->
|
||||
<div class={`border-l-2 pl-2 ${alertStyles[alert.type].border}`}>
|
||||
<p class={alertStyles[alert.type].text}>
|
||||
<svelte:component this={alertStyles[alert.type].icon} className="inline-block size-4" />
|
||||
<b>{alert.type}</b>
|
||||
</p>
|
||||
<MarkdownTokens id={`${id}-${tokenIdx}`} tokens={alert.tokens} {onTaskClick} {onSourceClick} />
|
||||
</div>
|
||||
@@ -14,10 +14,13 @@
|
||||
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
||||
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
|
||||
import KatexRenderer from './KatexRenderer.svelte';
|
||||
import AlertRenderer, { alertComponent } from './AlertRenderer.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||
|
||||
import Source from './Source.svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -84,6 +87,7 @@
|
||||
{#if token.raw.includes('```')}
|
||||
<CodeBlock
|
||||
id={`${id}-${tokenIdx}`}
|
||||
collapsed={$settings?.collapseCodeBlocks ?? false}
|
||||
{token}
|
||||
lang={token?.lang ?? ''}
|
||||
code={token?.text ?? ''}
|
||||
@@ -119,7 +123,7 @@
|
||||
class="px-3! py-1.5! cursor-pointer border border-gray-100 dark:border-gray-850"
|
||||
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
|
||||
>
|
||||
<div class="flex flex-col gap-1.5 text-left">
|
||||
<div class="gap-1.5 text-left">
|
||||
<div class="shrink-0 break-normal">
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
||||
@@ -140,7 +144,7 @@
|
||||
class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
|
||||
style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
|
||||
>
|
||||
<div class="flex flex-col break-normal">
|
||||
<div class="break-normal">
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
||||
tokens={cell.tokens}
|
||||
@@ -170,9 +174,14 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if token.type === 'blockquote'}
|
||||
<blockquote dir="auto">
|
||||
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
|
||||
</blockquote>
|
||||
{@const alert = alertComponent(token)}
|
||||
{#if alert}
|
||||
<AlertRenderer {token} {alert} />
|
||||
{:else}
|
||||
<blockquote dir="auto">
|
||||
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
|
||||
</blockquote>
|
||||
{/if}
|
||||
{:else if token.type === 'list'}
|
||||
{#if token.ordered}
|
||||
<ol start={token.start || 1}>
|
||||
@@ -242,6 +251,7 @@
|
||||
{:else if token.type === 'details'}
|
||||
<Collapsible
|
||||
title={token.summary}
|
||||
open={$settings?.expandDetails ?? false}
|
||||
attributes={token?.attributes}
|
||||
className="w-full space-y-1"
|
||||
dir="auto"
|
||||
|
||||
@@ -559,7 +559,7 @@
|
||||
<div class="flex-auto w-0 pl-1">
|
||||
<Name>
|
||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
||||
<span class="line-clamp-1">
|
||||
<span class="line-clamp-1 text-black dark:text-white">
|
||||
{model?.name ?? message.model}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -748,7 +748,9 @@
|
||||
onSourceClick={async (id, idx) => {
|
||||
console.log(id, idx);
|
||||
let sourceButton = document.getElementById(`source-${message.id}-${idx}`);
|
||||
const sourcesCollapsible = document.getElementById(`collapsible-sources`);
|
||||
const sourcesCollapsible = document.getElementById(
|
||||
`collapsible-${message.id}`
|
||||
);
|
||||
|
||||
if (sourceButton) {
|
||||
sourceButton.click();
|
||||
@@ -1269,7 +1271,7 @@
|
||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
id="continue-response-button"
|
||||
id="delete-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"
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if !isFirstMessage && !readOnly}
|
||||
{#if !readOnly && (!isFirstMessage || siblings.length > 1)}
|
||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded-sm dark:hover:text-white hover:text-black transition"
|
||||
|
||||
@@ -61,10 +61,11 @@
|
||||
$: selectedModel = items.find((item) => item.value === value) ?? '';
|
||||
|
||||
let searchValue = '';
|
||||
|
||||
let selectedTag = '';
|
||||
let selectedConnectionType = '';
|
||||
|
||||
let ollamaVersion = null;
|
||||
|
||||
let selectedModelIdx = 0;
|
||||
|
||||
const fuse = new Fuse(
|
||||
@@ -72,7 +73,7 @@
|
||||
const _item = {
|
||||
...item,
|
||||
modelName: item.model?.name,
|
||||
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
|
||||
tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '),
|
||||
desc: item.model?.info?.meta?.description
|
||||
};
|
||||
return _item;
|
||||
@@ -93,14 +94,61 @@
|
||||
if (selectedTag === '') {
|
||||
return true;
|
||||
}
|
||||
return item.model?.info?.meta?.tags?.map((tag) => tag.name).includes(selectedTag);
|
||||
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
|
||||
})
|
||||
: items.filter((item) => {
|
||||
if (selectedTag === '') {
|
||||
return true;
|
||||
}
|
||||
return item.model?.info?.meta?.tags?.map((tag) => tag.name).includes(selectedTag);
|
||||
});
|
||||
.filter((item) => {
|
||||
if (selectedConnectionType === '') {
|
||||
return true;
|
||||
} else if (selectedConnectionType === 'ollama') {
|
||||
return item.model?.owned_by === 'ollama';
|
||||
} else if (selectedConnectionType === 'openai') {
|
||||
return item.model?.owned_by === 'openai';
|
||||
} else if (selectedConnectionType === 'direct') {
|
||||
return item.model?.direct;
|
||||
}
|
||||
})
|
||||
: items
|
||||
.filter((item) => {
|
||||
if (selectedTag === '') {
|
||||
return true;
|
||||
}
|
||||
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
|
||||
})
|
||||
.filter((item) => {
|
||||
if (selectedConnectionType === '') {
|
||||
return true;
|
||||
} else if (selectedConnectionType === 'ollama') {
|
||||
return item.model?.owned_by === 'ollama';
|
||||
} else if (selectedConnectionType === 'openai') {
|
||||
return item.model?.owned_by === 'openai';
|
||||
} else if (selectedConnectionType === 'direct') {
|
||||
return item.model?.direct;
|
||||
}
|
||||
});
|
||||
|
||||
$: if (selectedTag || selectedConnectionType) {
|
||||
resetView();
|
||||
} else {
|
||||
resetView();
|
||||
}
|
||||
|
||||
const resetView = async () => {
|
||||
await tick();
|
||||
|
||||
const selectedInFiltered = filteredItems.findIndex((item) => item.value === value);
|
||||
|
||||
if (selectedInFiltered >= 0) {
|
||||
// The selected model is visible in the current filter
|
||||
selectedModelIdx = selectedInFiltered;
|
||||
} else {
|
||||
// The selected model is not visible, default to first item in filtered list
|
||||
selectedModelIdx = 0;
|
||||
}
|
||||
|
||||
await tick();
|
||||
const item = document.querySelector(`[data-arrow-selected="true"]`);
|
||||
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||
};
|
||||
|
||||
const pullModelHandler = async () => {
|
||||
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||
@@ -234,7 +282,7 @@
|
||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||
|
||||
if (items) {
|
||||
tags = items.flatMap((item) => item.model?.info?.meta?.tags ?? []).map((tag) => tag.name);
|
||||
tags = items.flatMap((item) => item.model?.tags ?? []).map((tag) => tag.name);
|
||||
|
||||
// Remove duplicates and sort
|
||||
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
|
||||
@@ -262,8 +310,9 @@
|
||||
bind:open={show}
|
||||
onOpenChange={async () => {
|
||||
searchValue = '';
|
||||
selectedModelIdx = 0;
|
||||
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
||||
|
||||
resetView();
|
||||
}}
|
||||
closeFocus={false}
|
||||
>
|
||||
@@ -326,28 +375,78 @@
|
||||
|
||||
<div class="px-3 mb-2 max-h-64 overflow-y-auto scrollbar-hidden group relative">
|
||||
{#if tags}
|
||||
<div class=" flex w-full sticky">
|
||||
<div
|
||||
class=" flex w-full sticky top-0 z-10 bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none"
|
||||
on:wheel={(e) => {
|
||||
if (e.deltaY !== 0) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.scrollLeft += e.deltaY;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
|
||||
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
|
||||
bind:this={tagsContainerElement}
|
||||
>
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedTag === ''
|
||||
class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
|
||||
selectedConnectionType === ''
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||
on:click={() => {
|
||||
selectedConnectionType = '';
|
||||
selectedTag = '';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('All')}
|
||||
</button>
|
||||
|
||||
{#if items.find((item) => item.model?.owned_by === 'ollama') && items.find((item) => item.model?.owned_by === 'openai')}
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'ollama'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||
on:click={() => {
|
||||
selectedTag = '';
|
||||
selectedConnectionType = 'ollama';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Local')}
|
||||
</button>
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'openai'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||
on:click={() => {
|
||||
selectedTag = '';
|
||||
selectedConnectionType = 'openai';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('External')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if items.find((item) => item.model?.direct)}
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'direct'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||
on:click={() => {
|
||||
selectedTag = '';
|
||||
selectedConnectionType = 'direct';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Direct')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#each tags as tag}
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedTag === tag
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||
on:click={() => {
|
||||
selectedConnectionType = '';
|
||||
selectedTag = tag;
|
||||
}}
|
||||
>
|
||||
@@ -366,6 +465,7 @@
|
||||
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
|
||||
: ''}"
|
||||
data-arrow-selected={index === selectedModelIdx}
|
||||
data-value={item.value}
|
||||
on:click={() => {
|
||||
value = item.value;
|
||||
selectedModelIdx = index;
|
||||
@@ -374,9 +474,9 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
{#if $mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
|
||||
{#if $mobile && (item?.model?.tags ?? []).length > 0}
|
||||
<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
|
||||
{#each item.model?.info?.meta.tags as tag}
|
||||
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
||||
<div
|
||||
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
@@ -398,31 +498,37 @@
|
||||
alt="Model"
|
||||
class="rounded-full size-5 flex items-center mr-2"
|
||||
/>
|
||||
{item.label}
|
||||
|
||||
<div class="flex items-center line-clamp-1">
|
||||
<div class="line-clamp-1">
|
||||
{item.label}
|
||||
</div>
|
||||
|
||||
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
|
||||
<div class="flex ml-1 items-center translate-y-[0.5px]">
|
||||
<Tooltip
|
||||
content={`${
|
||||
item.model.ollama?.details?.quantization_level
|
||||
? item.model.ollama?.details?.quantization_level + ' '
|
||||
: ''
|
||||
}${
|
||||
item.model.ollama?.size
|
||||
? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
|
||||
: ''
|
||||
}`}
|
||||
className="self-end"
|
||||
>
|
||||
<span
|
||||
class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
|
||||
>{item.model.ollama?.details?.parameter_size ?? ''}</span
|
||||
>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
|
||||
<div class="flex ml-1 items-center translate-y-[0.5px]">
|
||||
<Tooltip
|
||||
content={`${
|
||||
item.model.ollama?.details?.quantization_level
|
||||
? item.model.ollama?.details?.quantization_level + ' '
|
||||
: ''
|
||||
}${
|
||||
item.model.ollama?.size
|
||||
? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
|
||||
: ''
|
||||
}`}
|
||||
className="self-end"
|
||||
>
|
||||
<span
|
||||
class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
|
||||
>{item.model.ollama?.details?.parameter_size ?? ''}</span
|
||||
>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- {JSON.stringify(item.info)} -->
|
||||
@@ -496,11 +602,11 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
|
||||
{#if !$mobile && (item?.model?.tags ?? []).length > 0}
|
||||
<div
|
||||
class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
|
||||
>
|
||||
{#each item.model?.info?.meta.tags as tag}
|
||||
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
||||
<Tooltip content={tag.name} className="flex-shrink-0">
|
||||
<div
|
||||
class=" text-xs font-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
|
||||
@@ -114,37 +114,21 @@
|
||||
</div>
|
||||
</button>
|
||||
</Menu>
|
||||
{:else if $mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
|
||||
<Tooltip content={$i18n.t('Controls')}>
|
||||
<button
|
||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={async () => {
|
||||
await showControls.set(!$showControls);
|
||||
}}
|
||||
aria-label="Controls"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if !$mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
|
||||
<Tooltip content={$i18n.t('Controls')}>
|
||||
<button
|
||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={async () => {
|
||||
await showControls.set(!$showControls);
|
||||
}}
|
||||
aria-label="Controls"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
<Tooltip content={$i18n.t('Controls')}>
|
||||
<button
|
||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={async () => {
|
||||
await showControls.set(!$showControls);
|
||||
}}
|
||||
aria-label="Controls"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('New Chat')}>
|
||||
<button
|
||||
|
||||
@@ -961,6 +961,7 @@
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Context Length')}
|
||||
{$i18n.t('(Ollama)')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
export let onDelete = () => {};
|
||||
export let onSubmit = () => {};
|
||||
@@ -17,6 +18,7 @@
|
||||
export let config = {};
|
||||
|
||||
let showConfigModal = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
</script>
|
||||
|
||||
<AddConnectionModal
|
||||
@@ -28,7 +30,9 @@
|
||||
key,
|
||||
config
|
||||
}}
|
||||
{onDelete}
|
||||
onDelete={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
onSubmit={(connection) => {
|
||||
url = connection.url;
|
||||
key = connection.key;
|
||||
@@ -37,6 +41,14 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirmDialog}
|
||||
on:confirm={() => {
|
||||
onDelete();
|
||||
showConfigModal = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex w-full gap-2 items-center">
|
||||
<Tooltip
|
||||
className="w-full relative"
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
let chatDirection: 'LTR' | 'RTL' = 'LTR';
|
||||
let ctrlEnterToSend = false;
|
||||
|
||||
let collapseCodeBlocks = false;
|
||||
let expandDetails = false;
|
||||
|
||||
let imageCompression = false;
|
||||
let imageCompressionSize = {
|
||||
width: '',
|
||||
@@ -55,6 +58,16 @@
|
||||
|
||||
let webSearch = null;
|
||||
|
||||
const toggleExpandDetails = () => {
|
||||
expandDetails = !expandDetails;
|
||||
saveSettings({ expandDetails });
|
||||
};
|
||||
|
||||
const toggleCollapseCodeBlocks = () => {
|
||||
collapseCodeBlocks = !collapseCodeBlocks;
|
||||
saveSettings({ collapseCodeBlocks });
|
||||
};
|
||||
|
||||
const toggleSplitLargeChunks = async () => {
|
||||
splitLargeChunks = !splitLargeChunks;
|
||||
saveSettings({ splitLargeChunks: splitLargeChunks });
|
||||
@@ -227,6 +240,9 @@
|
||||
richTextInput = $settings.richTextInput ?? true;
|
||||
largeTextAsFile = $settings.largeTextAsFile ?? false;
|
||||
|
||||
collapseCodeBlocks = $settings.collapseCodeBlocks ?? false;
|
||||
expandDetails = $settings.expandDetails ?? false;
|
||||
|
||||
landingPageMode = $settings.landingPageMode ?? '';
|
||||
chatBubble = $settings.chatBubble ?? true;
|
||||
widescreenMode = $settings.widescreenMode ?? false;
|
||||
@@ -570,6 +586,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Always Collapse Code Blocks')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleCollapseCodeBlocks();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if collapseCodeBlocks === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Always Expand Details')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleExpandDetails();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if expandDetails === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import Chats from './Settings/Chats.svelte';
|
||||
import User from '../icons/User.svelte';
|
||||
import Personalization from './Settings/Personalization.svelte';
|
||||
import SearchInput from '../layout/Sidebar/SearchInput.svelte';
|
||||
import Search from '../icons/Search.svelte';
|
||||
import Connections from './Settings/Connections.svelte';
|
||||
|
||||
|
||||
@@ -45,9 +45,10 @@
|
||||
if (inputValue.length > 500) {
|
||||
filteredPrompts = [];
|
||||
} else {
|
||||
const newFilteredPrompts = inputValue.trim()
|
||||
? fuse.search(inputValue.trim()).map((result) => result.item)
|
||||
: sortedPrompts;
|
||||
const newFilteredPrompts =
|
||||
inputValue.trim() && fuse
|
||||
? fuse.search(inputValue.trim()).map((result) => result.item)
|
||||
: sortedPrompts;
|
||||
|
||||
// Compare with the oldFilteredPrompts
|
||||
// If there's a difference, update array + version
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export let state = 'unchecked';
|
||||
export let indeterminate = false;
|
||||
export let disabled = false;
|
||||
|
||||
let _state = 'unchecked';
|
||||
|
||||
@@ -14,8 +15,12 @@
|
||||
class=" outline -outline-offset-1 outline-[1.5px] outline-gray-200 dark:outline-gray-600 {state !==
|
||||
'unchecked'
|
||||
? 'bg-black outline-black '
|
||||
: 'hover:outline-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800'} text-white transition-all rounded-sm inline-block w-3.5 h-3.5 relative"
|
||||
: 'hover:outline-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800'} text-white transition-all rounded-sm inline-block w-3.5 h-3.5 relative {disabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (disabled) return;
|
||||
|
||||
if (_state === 'unchecked') {
|
||||
_state = 'checked';
|
||||
dispatch('change', _state);
|
||||
@@ -30,6 +35,7 @@
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
{disabled}
|
||||
>
|
||||
<div class="top-0 left-0 absolute w-full flex justify-center">
|
||||
{#if _state === 'checked'}
|
||||
|
||||
@@ -33,15 +33,49 @@
|
||||
|
||||
const updateValue = () => {
|
||||
if (_value !== value) {
|
||||
const changes = findChanges(_value, value);
|
||||
_value = value;
|
||||
if (codeEditor) {
|
||||
codeEditor.dispatch({
|
||||
changes: [{ from: 0, to: codeEditor.state.doc.length, insert: _value }]
|
||||
});
|
||||
|
||||
if (codeEditor && changes.length > 0) {
|
||||
codeEditor.dispatch({ changes });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds multiple diffs in two strings and generates minimal change edits.
|
||||
*/
|
||||
function findChanges(oldStr, newStr) {
|
||||
let changes = [];
|
||||
let oldIndex = 0,
|
||||
newIndex = 0;
|
||||
|
||||
while (oldIndex < oldStr.length || newIndex < newStr.length) {
|
||||
if (oldStr[oldIndex] !== newStr[newIndex]) {
|
||||
let start = oldIndex;
|
||||
|
||||
// Identify the changed portion
|
||||
while (oldIndex < oldStr.length && oldStr[oldIndex] !== newStr[newIndex]) {
|
||||
oldIndex++;
|
||||
}
|
||||
while (newIndex < newStr.length && newStr[newIndex] !== oldStr[start]) {
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
from: start,
|
||||
to: oldIndex, // Replace the differing part
|
||||
insert: newStr.substring(start, newIndex)
|
||||
});
|
||||
} else {
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export let id = '';
|
||||
export let lang = '';
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
{#if !small}
|
||||
<div class="flex flex-col justify-center -space-y-0.5 px-2.5 w-full">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
|
||||
{name}
|
||||
{decodeURIComponent(name)}
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-between text-gray-500 text-xs line-clamp-1">
|
||||
@@ -101,7 +101,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Tooltip content={name} className="flex flex-col w-full" placement="top-start">
|
||||
<Tooltip
|
||||
content={decodeURIComponent(name)}
|
||||
className="flex flex-col w-full"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="flex flex-col justify-center -space-y-0.5 px-2.5 w-full">
|
||||
<div class=" dark:text-gray-100 text-sm flex justify-between items-center">
|
||||
{#if loading}
|
||||
@@ -109,7 +113,7 @@
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="font-medium line-clamp-1 flex-1">{name}</div>
|
||||
<div class="font-medium line-clamp-1 flex-1">{decodeURIComponent(name)}</div>
|
||||
<div class="text-gray-500 text-xs capitalize shrink-0">{formatFileSize(size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,8 +87,12 @@
|
||||
<div>
|
||||
<Tooltip
|
||||
content={enableFullContent
|
||||
? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||
? $i18n.t(
|
||||
'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||
)
|
||||
: $i18n.t(
|
||||
'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'
|
||||
)}
|
||||
>
|
||||
<div class="flex items-center gap-1.5 text-xs">
|
||||
{#if enableFullContent}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import panzoom, { type PanZoom } from 'panzoom';
|
||||
|
||||
export let show = false;
|
||||
export let src = '';
|
||||
@@ -9,6 +10,25 @@
|
||||
|
||||
let previewElement = null;
|
||||
|
||||
let instance: PanZoom;
|
||||
|
||||
let sceneParentElement: HTMLElement;
|
||||
let sceneElement: HTMLElement;
|
||||
|
||||
$: if (sceneElement) {
|
||||
instance = panzoom(sceneElement, {
|
||||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
|
||||
zoomSpeed: 0.065
|
||||
});
|
||||
}
|
||||
const resetPanZoomViewport = () => {
|
||||
instance.moveTo(0, 0);
|
||||
instance.zoomAbs(0, 0, 1);
|
||||
console.log(instance.getTransform());
|
||||
};
|
||||
|
||||
const downloadImage = (url, filename, prefixName = '') => {
|
||||
fetch(url)
|
||||
.then((response) => response.blob())
|
||||
@@ -62,11 +82,16 @@
|
||||
bind:this={previewElement}
|
||||
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-9999 overflow-hidden overscroll-contain"
|
||||
>
|
||||
<div class=" absolute left-0 w-full flex justify-between select-none">
|
||||
<div class=" absolute left-0 w-full flex justify-between select-none z-10">
|
||||
<div>
|
||||
<button
|
||||
class=" p-5"
|
||||
on:click={() => {
|
||||
on:pointerdown={(e) => {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
show = false;
|
||||
}}
|
||||
on:click={(e) => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
@@ -86,7 +111,12 @@
|
||||
<div>
|
||||
<button
|
||||
class=" p-5"
|
||||
on:click={() => {
|
||||
on:pointerdown={(e) => {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
downloadImage(src, src.substring(src.lastIndexOf('/') + 1), alt);
|
||||
}}
|
||||
on:click={(e) => {
|
||||
downloadImage(src, src.substring(src.lastIndexOf('/') + 1), alt);
|
||||
}}
|
||||
>
|
||||
@@ -106,6 +136,8 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<img {src} {alt} class=" mx-auto h-full object-scale-down select-none" draggable="false" />
|
||||
<div bind:this={sceneElement} class="flex h-full max-h-full justify-center items-center">
|
||||
<img {src} {alt} class=" mx-auto h-full object-scale-down select-none" draggable="false" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{:else if (valvesSpec.properties[property]?.type ?? null) !== 'string'}
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
type="text"
|
||||
@@ -92,6 +92,17 @@
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<textarea
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={valvesSpec.properties[property].title}
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
required
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import jsPDF from 'jspdf';
|
||||
import html2canvas from 'html2canvas-pro';
|
||||
|
||||
import { downloadChatAsPDF } from '$lib/apis/utils';
|
||||
import { copyToClipboard, createMessagesList } from '$lib/utils';
|
||||
|
||||
@@ -14,7 +17,8 @@
|
||||
showControls,
|
||||
showArtifacts,
|
||||
mobile,
|
||||
temporaryChatEnabled
|
||||
temporaryChatEnabled,
|
||||
theme
|
||||
} from '$lib/stores';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
@@ -58,27 +62,76 @@
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
const history = chat.chat.history;
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
const blob = await downloadChatAsPDF(localStorage.token, chat.chat.title, messages);
|
||||
const containerElement = document.getElementById('messages-container');
|
||||
|
||||
// Create a URL for the blob
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
if (containerElement) {
|
||||
try {
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
// Create a link element to trigger the download
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `chat-${chat.chat.title}.pdf`;
|
||||
console.log('isDarkMode', isDarkMode);
|
||||
|
||||
// Append the link to the body and click it programmatically
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
// Define a fixed virtual screen size
|
||||
const virtualWidth = 800; // Fixed width (adjust as needed)
|
||||
// Clone the container to avoid layout shifts
|
||||
const clonedElement = containerElement.cloneNode(true);
|
||||
clonedElement.classList.add('text-black');
|
||||
clonedElement.classList.add('dark:text-white');
|
||||
clonedElement.style.width = `${virtualWidth}px`; // Apply fixed width
|
||||
clonedElement.style.height = 'auto'; // Allow content to expand
|
||||
|
||||
// Remove the link from the body
|
||||
document.body.removeChild(a);
|
||||
document.body.appendChild(clonedElement); // Temporarily add to DOM
|
||||
|
||||
// Revoke the URL to release memory
|
||||
window.URL.revokeObjectURL(url);
|
||||
// Render to canvas with predefined width
|
||||
const canvas = await html2canvas(clonedElement, {
|
||||
backgroundColor: isDarkMode ? '#000' : '#fff',
|
||||
useCORS: true,
|
||||
scale: 2, // Keep at 1x to avoid unexpected enlargements
|
||||
width: virtualWidth, // Set fixed virtual screen width
|
||||
windowWidth: virtualWidth // Ensure consistent rendering
|
||||
});
|
||||
|
||||
document.body.removeChild(clonedElement); // Clean up temp element
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
// A4 page settings
|
||||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||
const imgWidth = 210; // A4 width in mm
|
||||
const pageHeight = 297; // A4 height in mm
|
||||
|
||||
// Maintain aspect ratio
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
// Set page background for dark mode
|
||||
if (isDarkMode) {
|
||||
pdf.setFillColor(0, 0, 0);
|
||||
pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // Apply black bg
|
||||
}
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
|
||||
// Handle additional pages
|
||||
while (heightLeft > 0) {
|
||||
position -= pageHeight;
|
||||
pdf.addPage();
|
||||
|
||||
if (isDarkMode) {
|
||||
pdf.setFillColor(0, 0, 0);
|
||||
pdf.rect(0, 0, imgWidth, pageHeight, 'F');
|
||||
}
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
}
|
||||
|
||||
pdf.save(`chat-${chat.chat.title}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const downloadJSONExport = async () => {
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
let allChatsLoaded = false;
|
||||
|
||||
let folders = {};
|
||||
let newFolderId = null;
|
||||
|
||||
const initFolders = async () => {
|
||||
const folderList = await getFolders(localStorage.token).catch((error) => {
|
||||
@@ -90,6 +91,11 @@
|
||||
for (const folder of folderList) {
|
||||
// Ensure folder is added to folders with its data
|
||||
folders[folder.id] = { ...(folders[folder.id] || {}), ...folder };
|
||||
|
||||
if (newFolderId && folder.id === newFolderId) {
|
||||
folders[folder.id].new = true;
|
||||
newFolderId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Tie child folders to their parents
|
||||
@@ -150,6 +156,7 @@
|
||||
});
|
||||
|
||||
if (res) {
|
||||
newFolderId = res.id;
|
||||
await initFolders();
|
||||
}
|
||||
};
|
||||
@@ -611,6 +618,7 @@
|
||||
bind:value={search}
|
||||
on:input={searchDebounceHandler}
|
||||
placeholder={$i18n.t('Search')}
|
||||
showClearButton={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -198,6 +198,19 @@
|
||||
});
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
const chatTitleInputKeydownHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
editChatTitle(id, chatTitle);
|
||||
confirmEdit = false;
|
||||
chatTitle = '';
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
confirmEdit = false;
|
||||
chatTitle = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={id} />
|
||||
@@ -246,6 +259,7 @@
|
||||
bind:value={chatTitle}
|
||||
id="chat-title-input-{id}"
|
||||
class=" bg-transparent w-full outline-hidden mr-10"
|
||||
on:keydown={chatTitleInputKeydownHandler}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import jsPDF from 'jspdf';
|
||||
import html2canvas from 'html2canvas-pro';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
@@ -23,7 +26,7 @@
|
||||
getChatPinnedStatusById,
|
||||
toggleChatPinnedStatusById
|
||||
} from '$lib/apis/chats';
|
||||
import { chats } from '$lib/stores';
|
||||
import { chats, theme } from '$lib/stores';
|
||||
import { createMessagesList } from '$lib/utils';
|
||||
import { downloadChatAsPDF } from '$lib/apis/utils';
|
||||
import Download from '$lib/components/icons/Download.svelte';
|
||||
@@ -77,31 +80,76 @@
|
||||
|
||||
const downloadPdf = async () => {
|
||||
const chat = await getChatById(localStorage.token, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
|
||||
const containerElement = document.getElementById('messages-container');
|
||||
|
||||
if (containerElement) {
|
||||
try {
|
||||
const isDarkMode = $theme.includes('dark'); // Check theme mode
|
||||
|
||||
// Define a fixed virtual screen size
|
||||
const virtualWidth = 1024; // Fixed width (adjust as needed)
|
||||
const virtualHeight = 1400; // Fixed height (adjust as needed)
|
||||
|
||||
// Clone the container to avoid layout shifts
|
||||
const clonedElement = containerElement.cloneNode(true);
|
||||
clonedElement.style.width = `${virtualWidth}px`; // Apply fixed width
|
||||
clonedElement.style.height = 'auto'; // Allow content to expand
|
||||
|
||||
document.body.appendChild(clonedElement); // Temporarily add to DOM
|
||||
|
||||
// Render to canvas with predefined width
|
||||
const canvas = await html2canvas(clonedElement, {
|
||||
backgroundColor: isDarkMode ? '#000' : '#fff',
|
||||
useCORS: true,
|
||||
scale: 2, // Keep at 1x to avoid unexpected enlargements
|
||||
width: virtualWidth, // Set fixed virtual screen width
|
||||
windowWidth: virtualWidth, // Ensure consistent rendering
|
||||
windowHeight: virtualHeight
|
||||
});
|
||||
|
||||
document.body.removeChild(clonedElement); // Clean up temp element
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
// A4 page settings
|
||||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||
const imgWidth = 210; // A4 width in mm
|
||||
const pageHeight = 297; // A4 height in mm
|
||||
|
||||
// Maintain aspect ratio
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
// Set page background for dark mode
|
||||
if (isDarkMode) {
|
||||
pdf.setFillColor(0, 0, 0);
|
||||
pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // Apply black bg
|
||||
}
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
|
||||
// Handle additional pages
|
||||
while (heightLeft > 0) {
|
||||
position -= pageHeight;
|
||||
pdf.addPage();
|
||||
|
||||
if (isDarkMode) {
|
||||
pdf.setFillColor(0, 0, 0);
|
||||
pdf.rect(0, 0, imgWidth, pageHeight, 'F');
|
||||
}
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
}
|
||||
|
||||
pdf.save(`chat-${chat.chat.title}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF', error);
|
||||
}
|
||||
}
|
||||
|
||||
const history = chat.chat.history;
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
const blob = await downloadChatAsPDF(localStorage.token, chat.chat.title, messages);
|
||||
|
||||
// Create a URL for the blob
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a link element to trigger the download
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `chat-${chat.chat.title}.pdf`;
|
||||
|
||||
// Append the link to the body and click it programmatically
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Remove the link from the body
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Revoke the URL to release memory
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadJSONExport = async () => {
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
dragged = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
open = folders[folderId].is_expanded;
|
||||
if (folderElement) {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
@@ -215,6 +215,13 @@
|
||||
// Event listener for when dragging ends
|
||||
folderElement.addEventListener('dragend', onDragEnd);
|
||||
}
|
||||
|
||||
if (folders[folderId]?.new) {
|
||||
delete folders[folderId].new;
|
||||
|
||||
await tick();
|
||||
editHandler();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -297,15 +304,15 @@
|
||||
console.log('Edit');
|
||||
await tick();
|
||||
name = folders[folderId].name;
|
||||
edit = true;
|
||||
|
||||
edit = true;
|
||||
await tick();
|
||||
|
||||
// focus on the input
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(`folder-${folderId}-input`);
|
||||
const input = document.getElementById(`folder-${folderId}-input`);
|
||||
|
||||
if (input) {
|
||||
input.focus();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const exportHandler = async () => {
|
||||
@@ -394,6 +401,9 @@
|
||||
id="folder-{folderId}-input"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
on:focus={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
on:blur={() => {
|
||||
nameUpdateHandler();
|
||||
edit = false;
|
||||
@@ -427,7 +437,10 @@
|
||||
>
|
||||
<FolderMenu
|
||||
on:rename={() => {
|
||||
editHandler();
|
||||
// Requires a timeout to prevent the click event from closing the dropdown
|
||||
setTimeout(() => {
|
||||
editHandler();
|
||||
}, 200);
|
||||
}}
|
||||
on:delete={() => {
|
||||
showDeleteConfirm = true;
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { tags } from '$lib/stores';
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let placeholder = '';
|
||||
export let value = '';
|
||||
export let showClearButton = false;
|
||||
|
||||
let selectedIdx = 0;
|
||||
|
||||
@@ -59,6 +61,11 @@
|
||||
loading = false;
|
||||
};
|
||||
|
||||
const clearSearchInput = () => {
|
||||
value = '';
|
||||
dispatch('input');
|
||||
};
|
||||
|
||||
const documentClickHandler = (e) => {
|
||||
const searchContainer = document.getElementById('search-container');
|
||||
const chatSearch = document.getElementById('chat-search');
|
||||
@@ -98,7 +105,7 @@
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-hidden"
|
||||
class="w-full rounded-r-xl py-1.5 pl-2.5 text-sm bg-transparent dark:text-gray-300 outline-hidden"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Search')}
|
||||
bind:value
|
||||
on:input={() => {
|
||||
@@ -140,6 +147,17 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if showClearButton && value}
|
||||
<div class="self-center pr-2 pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||
<button
|
||||
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:click={clearSearchInput}
|
||||
>
|
||||
<XMark className="size-3" strokeWidth="2" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if focused && (filteredOptions.length > 0 || filteredTags.length > 0)}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { mobile, showSidebar, knowledge as _knowledge } from '$lib/stores';
|
||||
import { mobile, showSidebar, knowledge as _knowledge, config } from '$lib/stores';
|
||||
|
||||
import { updateFileDataContentById, uploadFile, deleteFileById } from '$lib/apis/files';
|
||||
import {
|
||||
@@ -131,6 +131,22 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
($config?.file?.max_size ?? null) !== null &&
|
||||
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
|
||||
) {
|
||||
console.log('File exceeds max size limit:', {
|
||||
fileSize: file.size,
|
||||
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
|
||||
});
|
||||
toast.error(
|
||||
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
|
||||
maxSize: $config?.file?.max_size
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
knowledge.files = [...(knowledge.files ?? []), fileItem];
|
||||
|
||||
try {
|
||||
@@ -681,7 +697,7 @@
|
||||
href={selectedFile.id ? `/api/v1/files/${selectedFile.id}/content` : '#'}
|
||||
target="_blank"
|
||||
>
|
||||
{selectedFile?.meta?.name}
|
||||
{decodeURIComponent(selectedFile?.meta?.name)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,10 +39,17 @@
|
||||
<div class=" flex items-center gap-2 mr-3">
|
||||
<div class="self-center flex items-center">
|
||||
<Checkbox
|
||||
state={_filters[filter].selected ? 'checked' : 'unchecked'}
|
||||
state={_filters[filter].is_global
|
||||
? 'checked'
|
||||
: _filters[filter].selected
|
||||
? 'checked'
|
||||
: 'unchecked'}
|
||||
disabled={_filters[filter].is_global}
|
||||
on:change={(e) => {
|
||||
_filters[filter].selected = e.detail === 'checked';
|
||||
selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected);
|
||||
if (!_filters[filter].is_global) {
|
||||
_filters[filter].selected = e.detail === 'checked';
|
||||
selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -113,8 +113,8 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option class=" text-gray-700" value="private" selected>Private</option>
|
||||
<option class=" text-gray-700" value="public" selected>Public</option>
|
||||
<option class=" text-gray-700" value="private" selected>{$i18n.t('Private')}</option>
|
||||
<option class=" text-gray-700" value="public" selected>{$i18n.t('Public')}</option>
|
||||
</select>
|
||||
|
||||
<div class=" text-xs text-gray-400 font-medium">
|
||||
|
||||
Reference in New Issue
Block a user