Merge branch 'dev' into k_reranker

This commit is contained in:
Timothy Jaeryang Baek
2025-03-26 20:50:31 -07:00
committed by GitHub
147 changed files with 6065 additions and 1350 deletions
+32 -4
View File
@@ -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
+7 -4
View File
@@ -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}
+18 -16
View File
@@ -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"
+13 -29
View File
@@ -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';
+4 -3
View File
@@ -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
+7 -1
View File
@@ -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'}
+38 -4
View File
@@ -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 = '';
+7 -3
View File
@@ -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}
+36 -4
View File
@@ -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}
+12 -1
View File
@@ -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>
+70 -17
View File
@@ -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 () => {
+8
View File
@@ -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">