Merge branch 'upstream-dev' into dev

This commit is contained in:
Jannik Streidl
2024-10-12 15:18:59 +02:00
103 changed files with 2956 additions and 3023 deletions

View File

@@ -2,20 +2,27 @@
import { getContext } from 'svelte';
export let title = '';
export let content = '';
const i18n = getContext('i18n');
</script>
<div class=" text-center text-6xl mb-3">📄</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">
{#if title}
{title}
{:else}
{$i18n.t('Add Files')}
{/if}
</div>
<slot
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
{$i18n.t('Drop any files here to add to the conversation')}
<div class="px-3">
<div class="text-center text-6xl mb-3">📄</div>
<div class="text-center dark:text-white text-xl font-semibold z-50">
{#if title}
{title}
{:else}
{$i18n.t('Add Files')}
{/if}
</div>
</slot>
<slot
><div class="px-2 mt-2 text-center text-sm dark:text-gray-200 w-full">
{#if content}
{content}
{:else}
{$i18n.t('Drop any files here to add to the conversation')}
{/if}
</div>
</slot>
</div>

View File

@@ -38,6 +38,7 @@
let embeddingEngine = '';
let embeddingModel = '';
let embeddingBatchSize = 1;
let rerankingModel = '';
let fileMaxSize = null;
@@ -53,7 +54,6 @@
let OpenAIKey = '';
let OpenAIUrl = '';
let OpenAIBatchSize = 1;
let querySettings = {
template: '',
@@ -100,12 +100,16 @@
const res = await updateEmbeddingConfig(localStorage.token, {
embedding_engine: embeddingEngine,
embedding_model: embeddingModel,
...(embeddingEngine === 'openai' || embeddingEngine === 'ollama'
? {
embedding_batch_size: embeddingBatchSize
}
: {}),
...(embeddingEngine === 'openai'
? {
openai_config: {
key: OpenAIKey,
url: OpenAIUrl,
batch_size: OpenAIBatchSize
url: OpenAIUrl
}
}
: {})
@@ -193,10 +197,10 @@
if (embeddingConfig) {
embeddingEngine = embeddingConfig.embedding_engine;
embeddingModel = embeddingConfig.embedding_model;
embeddingBatchSize = embeddingConfig.embedding_batch_size ?? 1;
OpenAIKey = embeddingConfig.openai_config.key;
OpenAIUrl = embeddingConfig.openai_config.url;
OpenAIBatchSize = embeddingConfig.openai_config.batch_size ?? 1;
}
};
@@ -309,6 +313,8 @@
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
</div>
{/if}
{#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'}
<div class="flex mt-0.5 space-x-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
<div class=" flex-1">
@@ -318,13 +324,13 @@
min="1"
max="2048"
step="1"
bind:value={OpenAIBatchSize}
bind:value={embeddingBatchSize}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={OpenAIBatchSize}
bind:value={embeddingBatchSize}
type="number"
class=" bg-transparent text-center w-14"
min="-2"

View File

@@ -10,6 +10,7 @@
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
import Tooltip from '../common/Tooltip.svelte';
import SvgPanZoom from '../common/SVGPanZoom.svelte';
import ArrowLeft from '../icons/ArrowLeft.svelte';
export let overlay = false;
export let history;
@@ -183,6 +184,17 @@
<div class=" absolute top-0 left-0 right-0 bottom-0 z-10"></div>
{/if}
<div class="absolute pointer-events-none z-50 w-full flex items-center justify-start p-4">
<button
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
on:click={() => {
showArtifacts.set(false);
}}
>
<ArrowLeft className="size-3.5 text-gray-900 dark:text-white" />
</button>
</div>
<div class=" absolute pointer-events-none z-50 w-full flex items-center justify-end p-4">
<button
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
@@ -192,7 +204,7 @@
showArtifacts.set(false);
}}
>
<XMark className="size-3 text-gray-900 dark:text-white" />
<XMark className="size-3.5 text-gray-900 dark:text-white" />
</button>
</div>

View File

@@ -53,7 +53,7 @@
updateChatById
} from '$lib/apis/chats';
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
import { processWebSearch } from '$lib/apis/retrieval';
import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
import { createOpenAITextStream } from '$lib/apis/streaming';
import { queryMemory } from '$lib/apis/memories';
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
@@ -78,6 +78,7 @@
let loaded = false;
const eventTarget = new EventTarget();
let controlPane;
let controlPaneComponent;
let stopResponseFlag = false;
let autoScroll = true;
@@ -199,6 +200,20 @@
eventConfirmationTitle = data.title;
eventConfirmationMessage = data.message;
} else if (type === 'execute') {
eventCallback = cb;
try {
// Use Function constructor to evaluate code in a safer way
const asyncFunction = new Function(`return (async () => { ${data.code} })()`);
const result = await asyncFunction(); // Await the result of the async function
if (cb) {
cb(result);
}
} catch (error) {
console.error('Error executing code:', error);
}
} else if (type === 'input') {
eventCallback = cb;
@@ -276,14 +291,9 @@
if (controlPane && !$mobile) {
try {
if (value) {
const currentSize = controlPane.getSize();
if (currentSize === 0) {
const size = parseInt(localStorage?.chatControlsSize ?? '30');
controlPane.resize(size ? size : 30);
}
controlPaneComponent.openPane();
} else {
controlPane.resize(0);
controlPane.collapse();
}
} catch (e) {
// ignore
@@ -293,6 +303,7 @@
if (!value) {
showCallOverlay.set(false);
showOverview.set(false);
showArtifacts.set(false);
}
});
@@ -308,6 +319,74 @@
$socket?.off('chat-events');
});
// File upload functions
const uploadWeb = async (url) => {
console.log(url);
const fileItem = {
type: 'doc',
name: url,
collection_name: '',
status: 'uploading',
url: url,
error: ''
};
try {
files = [...files, fileItem];
const res = await processWeb(localStorage.token, '', url);
if (res) {
fileItem.status = 'uploaded';
fileItem.collection_name = res.collection_name;
fileItem.file = {
...res.file,
...fileItem.file
};
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(JSON.stringify(e));
}
};
const uploadYoutubeTranscription = async (url) => {
console.log(url);
const fileItem = {
type: 'doc',
name: url,
collection_name: '',
status: 'uploading',
context: 'full',
url: url,
error: ''
};
try {
files = [...files, fileItem];
const res = await processYoutubeVideo(localStorage.token, url);
if (res) {
fileItem.status = 'uploaded';
fileItem.collection_name = res.collection_name;
fileItem.file = {
...res.file,
...fileItem.file
};
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
}
};
//////////////////////////
// Web functions
//////////////////////////
@@ -345,7 +424,17 @@
console.log($config?.default_models.split(',') ?? '');
selectedModels = $config?.default_models.split(',');
} else {
selectedModels = [''];
if ($models.length > 0) {
selectedModels = [$models[0].id];
} else {
selectedModels = [''];
}
}
if ($page.url.searchParams.get('youtube')) {
uploadYoutubeTranscription(
`https://www.youtube.com/watch?v=${$page.url.searchParams.get('youtube')}`
);
}
if ($page.url.searchParams.get('web-search') === 'true') {
@@ -366,6 +455,11 @@
.filter((id) => id);
}
if ($page.url.searchParams.get('call') === 'true') {
showCallOverlay.set(true);
showControls.set(true);
}
if ($page.url.searchParams.get('q')) {
prompt = $page.url.searchParams.get('q') ?? '';
@@ -375,11 +469,6 @@
}
}
if ($page.url.searchParams.get('call') === 'true') {
showCallOverlay.set(true);
showControls.set(true);
}
selectedModels = selectedModels.map((modelId) =>
$models.map((m) => m.id).includes(modelId) ? modelId : ''
);
@@ -1855,6 +1944,7 @@
system: $settings.system ?? undefined,
params: params,
history: history,
messages: createMessagesList(history.currentId),
tags: [],
timestamp: Date.now()
});
@@ -1920,6 +2010,7 @@
class="h-screen max-h-[100dvh] {$showSidebar
? 'md:max-w-[calc(100%-260px)]'
: ''} w-full max-w-full flex flex-col"
id="chat-container"
>
{#if $settings?.backgroundImageUrl ?? null}
<div
@@ -1935,7 +2026,17 @@
{/if}
<Navbar
{chat}
chat={{
id: $chatId,
chat: {
title: $chatTitle,
models: selectedModels,
system: $settings.system ?? undefined,
params: params,
history: history,
timestamp: Date.now()
}
}}
title={$chatTitle}
bind:selectedModels
shareEnabled={!!history.currentId}
@@ -2050,6 +2151,15 @@
transparentBackground={$settings?.backgroundImageUrl ?? false}
{stopResponse}
{createMessagePair}
on:upload={async (e) => {
const { type, data } = e.detail;
if (type === 'web') {
await uploadWeb(data);
} else if (type === 'youtube') {
await uploadYoutubeTranscription(data);
}
}}
on:submit={async (e) => {
if (e.detail) {
prompt = '';
@@ -2066,38 +2176,50 @@
</div>
</div>
{:else}
<Placeholder
{history}
{selectedModels}
bind:files
bind:prompt
bind:autoScroll
bind:selectedToolIds
bind:webSearchEnabled
bind:atSelectedModel
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e);
if (model?.info?.meta?.toolIds ?? false) {
return [...new Set([...a, ...model.info.meta.toolIds])];
}
return a;
}, [])}
transparentBackground={$settings?.backgroundImageUrl ?? false}
{stopResponse}
{createMessagePair}
on:submit={async (e) => {
if (e.detail) {
prompt = '';
await tick();
submitPrompt(e.detail);
}
}}
/>
<div class="overflow-auto w-full h-full flex items-center">
<Placeholder
{history}
{selectedModels}
bind:files
bind:prompt
bind:autoScroll
bind:selectedToolIds
bind:webSearchEnabled
bind:atSelectedModel
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e);
if (model?.info?.meta?.toolIds ?? false) {
return [...new Set([...a, ...model.info.meta.toolIds])];
}
return a;
}, [])}
transparentBackground={$settings?.backgroundImageUrl ?? false}
{stopResponse}
{createMessagePair}
on:upload={async (e) => {
const { type, data } = e.detail;
if (type === 'web') {
await uploadWeb(data);
} else if (type === 'youtube') {
await uploadYoutubeTranscription(data);
}
}}
on:submit={async (e) => {
if (e.detail) {
prompt = '';
await tick();
submitPrompt(e.detail);
}
}}
/>
</div>
{/if}
</div>
</Pane>
<ChatControls
bind:this={controlPaneComponent}
bind:history
bind:chatFiles
bind:params

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { SvelteFlowProvider } from '@xyflow/svelte';
import { slide } from 'svelte/transition';
import { Pane, PaneResizer } from 'paneforge';
import { onDestroy, onMount, tick } from 'svelte';
import { mobile, showControls, showCallOverlay, showOverview, showArtifacts } from '$lib/stores';
@@ -10,9 +11,9 @@
import CallOverlay from './MessageInput/CallOverlay.svelte';
import Drawer from '../common/Drawer.svelte';
import Overview from './Overview.svelte';
import { Pane, PaneResizer } from 'paneforge';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import Artifacts from './Artifacts.svelte';
import { min } from '@floating-ui/utils';
export let history;
export let models = [];
@@ -35,6 +36,16 @@
let largeScreen = false;
let dragged = false;
let minSize = 0;
export const openPane = () => {
if (parseInt(localStorage?.chatControlsSize)) {
pane.resize(parseInt(localStorage?.chatControlsSize));
} else {
pane.resize(minSize);
}
};
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
@@ -71,6 +82,32 @@
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
// Select the container element you want to observe
const container = document.getElementById('chat-container');
// initialize the minSize based on the container width
minSize = Math.floor((350 / container.clientWidth) * 100);
// Create a new ResizeObserver instance
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const width = entry.contentRect.width;
// calculate the percentage of 200px
const percentage = (350 / width) * 100;
// set the minSize to the percentage, must be an integer
minSize = Math.floor(percentage);
if ($showControls) {
if (pane && pane.isExpanded() && pane.getSize() < minSize) {
pane.resize(minSize);
}
}
}
});
// Start observing the container's size changes
resizeObserver.observe(container);
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mouseup', onMouseUp);
});
@@ -163,23 +200,29 @@
</div>
</PaneResizer>
{/if}
<Pane
bind:pane
defaultSize={$showControls
? parseInt(localStorage?.chatControlsSize ?? '30')
? parseInt(localStorage?.chatControlsSize ?? '30')
: 30
: 0}
defaultSize={0}
onResize={(size) => {
if (size === 0) {
showControls.set(false);
} else {
if (!$showControls) {
showControls.set(true);
console.log('size', size, minSize);
if ($showControls && pane.isExpanded()) {
if (size < minSize) {
pane.resize(minSize);
}
if (size < minSize) {
localStorage.chatControlsSize = 0;
} else {
localStorage.chatControlsSize = size;
}
localStorage.chatControlsSize = size;
}
}}
onCollapse={() => {
showControls.set(false);
}}
collapsible={true}
class="pt-8"
>
{#if $showControls}
@@ -187,7 +230,7 @@
<div
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
? ' '
: 'px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800'} rounded-lg z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800'} rounded-lg z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
>
{#if $showCallOverlay}
<div class="w-full h-full flex justify-center">

View File

@@ -16,7 +16,7 @@
</script>
<div class=" dark:text-white">
<div class=" flex justify-between dark:text-gray-100 mb-2">
<div class=" flex items-center justify-between dark:text-gray-100 mb-2">
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Controls')}</div>
<button
class="self-center"
@@ -24,11 +24,11 @@
dispatch('close');
}}
>
<XMark className="size-4" />
<XMark className="size-3.5" />
</button>
</div>
<div class=" dark:text-gray-200 text-sm font-primary py-0.5">
<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}>
<div class="flex flex-col gap-1 mt-1.5" slot="content">

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, tick, getContext, createEventDispatcher } from 'svelte';
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
const dispatch = createEventDispatcher();
import {
@@ -175,57 +175,59 @@
});
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Escape');
dragged = false;
}
};
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) {
const inputFiles = Array.from(e.dataTransfer?.files);
if (inputFiles && inputFiles.length > 0) {
console.log(inputFiles);
inputFilesHandler(inputFiles);
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
onMount(() => {
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
const dropZone = document.querySelector('body');
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Escape');
dragged = false;
}
};
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) {
const inputFiles = Array.from(e.dataTransfer?.files);
if (inputFiles && inputFiles.length > 0) {
console.log(inputFiles);
inputFilesHandler(inputFiles);
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
window.addEventListener('keydown', handleKeyDown);
const dropZone = document.getElementById('chat-container');
dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave);
});
return () => {
window.removeEventListener('keydown', handleKeyDown);
onDestroy(() => {
window.removeEventListener('keydown', handleKeyDown);
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
};
const dropZone = document.getElementById('chat-container');
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
});
</script>
@@ -300,6 +302,9 @@
bind:this={commandsElement}
bind:prompt
bind:files
on:upload={(e) => {
dispatch('upload', e.detail);
}}
on:select={(e) => {
const data = e.detail;
@@ -791,25 +796,27 @@
{/if}
{:else}
<div class=" flex items-center mb-1.5">
<button
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
on:click={() => {
stopResponse();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
<Tooltip content={$i18n.t('Stop')}>
<button
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
on:click={() => {
stopResponse();
}}
>
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
clip-rule="evenodd"
/>
</svg>
</button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
</div>

View File

@@ -26,71 +26,6 @@
let command = '';
$: command = (prompt?.trim() ?? '').split(' ')?.at(-1) ?? '';
const uploadWeb = async (url) => {
console.log(url);
const fileItem = {
type: 'doc',
name: url,
collection_name: '',
status: 'uploading',
url: url,
error: ''
};
try {
files = [...files, fileItem];
const res = await processWeb(localStorage.token, '', url);
if (res) {
fileItem.status = 'uploaded';
fileItem.collection_name = res.collection_name;
fileItem.file = {
...res.file,
...fileItem.file
};
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(JSON.stringify(e));
}
};
const uploadYoutubeTranscription = async (url) => {
console.log(url);
const fileItem = {
type: 'doc',
name: url,
collection_name: '',
status: 'uploading',
url: url,
error: ''
};
try {
files = [...files, fileItem];
const res = await processYoutubeVideo(localStorage.token, url);
if (res) {
fileItem.status = 'uploaded';
fileItem.collection_name = res.collection_name;
fileItem.file = {
...res.file,
...fileItem.file
};
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
}
};
</script>
{#if ['/', '#', '@'].includes(command?.charAt(0))}
@@ -103,18 +38,23 @@
{command}
on:youtube={(e) => {
console.log(e);
uploadYoutubeTranscription(e.detail);
dispatch('upload', {
type: 'youtube',
data: e.detail
});
}}
on:url={(e) => {
console.log(e);
uploadWeb(e.detail);
dispatch('upload', {
type: 'web',
data: e.detail
});
}}
on:select={(e) => {
console.log(e);
files = [
...files,
{
type: e?.detail?.meta?.document ? 'file' : 'collection',
...e.detail,
status: 'processed'
}

View File

@@ -2,6 +2,10 @@
import { toast } from 'svelte-sonner';
import Fuse from 'fuse.js';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
import { createEventDispatcher, tick, getContext, onMount } from 'svelte';
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
import { knowledge } from '$lib/stores';
@@ -72,7 +76,13 @@
};
onMount(() => {
let legacy_documents = $knowledge.filter((item) => item?.meta?.document);
let legacy_documents = $knowledge
.filter((item) => item?.meta?.document)
.map((item) => ({
...item,
type: 'file'
}));
let legacy_collections =
legacy_documents.length > 0
? [
@@ -101,12 +111,44 @@
]
: [];
items = [...$knowledge, ...legacy_collections].map((item) => {
return {
let collections = $knowledge
.filter((item) => !item?.meta?.document)
.map((item) => ({
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
});
type: 'collection'
}));
let collection_files =
$knowledge.length > 0
? [
...$knowledge
.reduce((a, item) => {
return [
...new Set([
...a,
...(item?.files ?? []).map((file) => ({
...file,
collection: { name: item.name, description: item.description }
}))
])
];
}, [])
.map((file) => ({
...file,
name: file?.meta?.name,
description: `${file?.collection?.name} - ${file?.collection?.description}`,
type: 'file'
}))
]
: [];
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
(item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
}
);
fuse = new Fuse(items, {
keys: ['name', 'description']
@@ -117,20 +159,17 @@
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<div
id="commands-container"
class="pl-2 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
class="pl-8 pr-16 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-medium mt-2">#</div>
</div>
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
<div
class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-900 dark:text-gray-100"
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden">
{#each filteredItems as item, idx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {idx === selectedIdx
class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center {idx ===
selectedIdx
? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button'
: ''}"
type="button"
@@ -141,38 +180,87 @@
on:mousemove={() => {
selectedIdx = idx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
{#if item.legacy}
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1"
>
Legacy
</div>
{:else if item?.meta?.document}
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1"
>
Document
</div>
{:else}
<div
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1"
>
Collection
</div>
{/if}
<div>
<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
{#if item.legacy}
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
>
Legacy
</div>
{:else if item?.meta?.document}
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
>
Document
</div>
{:else if item?.type === 'file'}
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
>
File
</div>
{:else}
<div
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
>
Collection
</div>
{/if}
<div class="line-clamp-1">
{item.name}
<div class="line-clamp-1">
{item?.name}
</div>
</div>
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
{item?.description}
</div>
</div>
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
{item?.description}
</div>
</button>
<!-- <div slot="content" class=" pl-2 pt-1 flex flex-col gap-0.5">
{#if !item.legacy && (item?.files ?? []).length > 0}
{#each item?.files ?? [] as file, fileIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center hover:bg-gray-50 dark:hover:bg-gray-850 dark:hover:text-gray-100 selected-command-option-button"
type="button"
on:click={() => {
console.log(file);
}}
on:mousemove={() => {
selectedIdx = idx;
}}
>
<div>
<div
class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
>
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
>
File
</div>
<div class="line-clamp-1">
{file?.meta?.name}
</div>
</div>
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
{$i18n.t('Updated')}
{dayjs(file.updated_at * 1000).fromNow()}
</div>
</div>
</button>
{/each}
{:else}
<div class=" text-gray-500 text-xs mt-1 mb-2">
{$i18n.t('No files found.')}
</div>
{/if}
</div> -->
{/each}
{#if prompt

View File

@@ -68,15 +68,11 @@
{#if filteredItems.length > 0}
<div
id="commands-container"
class="pl-2 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
class="pl-8 pr-16 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-medium mt-2">@</div>
</div>
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
<div
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredItems as model, modelIdx}

View File

@@ -132,17 +132,13 @@
{#if filteredPrompts.length > 0}
<div
id="commands-container"
class="pl-2 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
class="pl-8 pr-16 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-medium mt-2">/</div>
</div>
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
<div
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
<div class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden">
{#each filteredPrompts as prompt, promptIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
@@ -169,7 +165,7 @@
</div>
<div
class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-br-xl flex items-center space-x-1"
class=" px-2 pt-0.5 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-b-xl flex items-center space-x-1"
>
<div>
<svg

View File

@@ -302,6 +302,7 @@ __builtins__.input = input`);
<SvgPanZoom
className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
svg={mermaidHtml}
content={_token.text}
/>
{:else}
<pre class="mermaid">{code}</pre>

View File

@@ -479,7 +479,7 @@
id={message.id}
content={message.content}
floatingButtons={message?.done}
save={true}
save={!readOnly}
{model}
on:update={(e) => {
const { raw, oldContent, newContent } = e.detail;

View File

@@ -94,7 +94,7 @@
: (user?.profile_image_url ?? '/user.png')}
/>
{/if}
<div class="w-full w-0 pl-1">
<div class="flex-auto w-0 max-w-full pl-1">
{#if !($settings?.chatBubble ?? true)}
<div>
<Name>

View File

@@ -15,6 +15,7 @@
import CustomNode from './Overview/Node.svelte';
import Flow from './Overview/Flow.svelte';
import XMark from '../icons/XMark.svelte';
import ArrowLeft from '../icons/ArrowLeft.svelte';
const { width, height } = useStore();
@@ -159,16 +160,26 @@
</script>
<div class="w-full h-full relative">
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-5 py-4">
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3.5">
<div class="flex items-center gap-2.5">
<button
class="self-center p-0.5"
on:click={() => {
showOverview.set(false);
}}
>
<ArrowLeft className="size-3.5" />
</button>
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
</div>
<button
class="self-center"
class="self-center p-0.5"
on:click={() => {
dispatch('close');
showOverview.set(false);
}}
>
<XMark className="size-4" />
<XMark className="size-3.5" />
</button>
</div>

View File

@@ -89,7 +89,7 @@
</script>
{#key mounted}
<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 text-center">
<div class="m-auto w-full max-w-6xl px-2 xl:px-20 translate-y-6 py-24 text-center">
{#if $temporaryChatEnabled}
<Tooltip
content="This chat won't appear in history and your messages will not be saved."
@@ -186,7 +186,7 @@
</div>
<div
class="text-base font-normal xl:translate-x-6 lg:max-w-3xl w-full py-3 {atSelectedModel
class="text-base font-normal xl:translate-x-6 md:max-w-3xl w-full py-3 {atSelectedModel
? 'mt-2'
: ''}"
>
@@ -204,6 +204,9 @@
{stopResponse}
{createMessagePair}
placeholder={$i18n.t('How can I help you today?')}
on:upload={(e) => {
dispatch('upload', e.detail);
}}
on:submit={(e) => {
dispatch('submit', e.detail);
}}

View File

@@ -25,51 +25,32 @@
let tags = [];
const getTags = async () => {
return (
await getTagsById(localStorage.token, chatId).catch(async (error) => {
return [];
})
).filter((tag) => tag.name !== 'pinned');
return await getTagsById(localStorage.token, chatId).catch(async (error) => {
return [];
});
};
const addTag = async (tagName) => {
const res = await addTagById(localStorage.token, chatId, tagName);
tags = await getTags();
await updateChatById(localStorage.token, chatId, {
tags: tags
});
_tags.set(await getAllChatTags(localStorage.token));
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
};
const deleteTag = async (tagName) => {
const res = await deleteTagById(localStorage.token, chatId, tagName);
tags = await getTags();
await updateChatById(localStorage.token, chatId, {
tags: tags
});
await _tags.set(await getAllChatTags(localStorage.token));
if ($_tags.map((t) => t.name).includes(tagName)) {
if (tagName === 'pinned') {
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
} else {
await chats.set(await getChatListByTagName(localStorage.token, tagName));
}
if ($chats.find((chat) => chat.id === chatId)) {
dispatch('close');
}
} else {
// if the tag we deleted is no longer a valid tag, return to main chat list view
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
await scrollPaginationEnabled.set(true);
}
dispatch('delete', {
name: tagName
});
};
onMount(async () => {

View File

@@ -1,11 +1,19 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
import panzoom from 'panzoom';
import DOMPurify from 'dompurify';
import DocumentDuplicate from '../icons/DocumentDuplicate.svelte';
import { copyToClipboard } from '$lib/utils';
import { toast } from 'svelte-sonner';
import Tooltip from './Tooltip.svelte';
import Clipboard from '../icons/Clipboard.svelte';
export let className = '';
export let svg = '';
export let content = '';
let instance;
@@ -22,8 +30,24 @@
}
</script>
<div bind:this={sceneParentElement} class={className}>
<div bind:this={sceneParentElement} class="relative {className}">
<div bind:this={sceneElement} class="flex h-full max-h-full justify-center items-center">
{@html svg}
</div>
{#if content}
<div class=" absolute top-1 right-1">
<Tooltip content={$i18n.t('Copy to clipboard')}>
<button
class="p-1.5 rounded-lg border border-gray-100 dark:border-none dark:bg-gray-850 hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={() => {
copyToClipboard(content);
toast.success($i18n.t('Copied to clipboard'));
}}
>
<Clipboard className=" size-4" strokeWidth="1.5" />
</button>
</Tooltip>
</div>
{/if}
</div>

View File

@@ -1,166 +0,0 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount, getContext } from 'svelte';
import { getDocs } from '$lib/apis/documents';
import Modal from '../common/Modal.svelte';
import { documents } from '$lib/stores';
import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
import Tags from '../common/Tags.svelte';
const i18n = getContext('i18n');
export let show = false;
export let uploadDoc: Function;
let uploadDocInputElement: HTMLInputElement;
let inputFiles;
let tags = [];
let doc = {
name: '',
title: '',
content: null
};
const submitHandler = async () => {
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
console.log(file, file.name.split('.').at(-1));
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file, tags);
} else {
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
);
uploadDoc(file, tags);
}
}
inputFiles = null;
uploadDocInputElement.value = '';
} else {
toast.error($i18n.t(`File not found.`));
}
show = false;
documents.set(await getDocs(localStorage.token));
};
const addTagHandler = async (tagName) => {
if (!tags.find((tag) => tag.name === tagName) && tagName !== '') {
tags = [...tags, { name: tagName }];
} else {
console.log('tag already exists');
}
};
const deleteTagHandler = async (tagName) => {
tags = tags.filter((tag) => tag.name !== tagName);
};
onMount(() => {});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class="mb-3 w-full">
<input
id="upload-doc-input"
bind:this={uploadDocInputElement}
hidden
bind:files={inputFiles}
type="file"
multiple
/>
<button
class="w-full text-sm font-medium py-3 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 text-center rounded-xl"
type="button"
on:click={() => {
uploadDocInputElement.click();
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else}
{$i18n.t('Click here to select documents.')}
{/if}
</button>
</div>
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
</div>
</div>
<div class="flex justify-end pt-5 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>

View File

@@ -1,181 +0,0 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount, getContext } from 'svelte';
import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
import Modal from '../common/Modal.svelte';
import { documents } from '$lib/stores';
import TagInput from '../common/Tags/TagInput.svelte';
import Tags from '../common/Tags.svelte';
import { addTagById } from '$lib/apis/chats';
const i18n = getContext('i18n');
export let show = false;
export let selectedDoc;
let tags = [];
let doc = {
name: '',
title: '',
content: null
};
const submitHandler = async () => {
const res = await updateDocByName(localStorage.token, selectedDoc.name, {
title: doc.title,
name: doc.name
}).catch((error) => {
toast.error(error);
});
if (res) {
show = false;
documents.set(await getDocs(localStorage.token));
}
};
const addTagHandler = async (tagName) => {
if (!tags.find((tag) => tag.name === tagName) && tagName !== '') {
tags = [...tags, { name: tagName }];
await tagDocByName(localStorage.token, doc.name, {
name: doc.name,
tags: tags
});
documents.set(await getDocs(localStorage.token));
} else {
console.log('tag already exists');
}
};
const deleteTagHandler = async (tagName) => {
tags = tags.filter((tag) => tag.name !== tagName);
await tagDocByName(localStorage.token, doc.name, {
name: doc.name,
tags: tags
});
documents.set(await getDocs(localStorage.token));
};
onMount(() => {
if (selectedDoc) {
doc = JSON.parse(JSON.stringify(selectedDoc));
tags = doc?.content?.tags ?? [];
}
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name Tag')}</div>
<div class="flex flex-1">
<div
class="bg-gray-200 dark:bg-gray-800 font-semibold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center"
>
#
</div>
<input
class="w-full rounded-r-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="text"
bind:value={doc.name}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Title')}</div>
<div class="flex-1">
<input
class="w-full rounded-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
bind:value={doc.title}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-2 text-xs text-gray-500">{$i18n.t('Tags')}</div>
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
</div>
</div>
<div class="flex justify-end pt-5 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>

View File

@@ -10,6 +10,7 @@
showArchivedChats,
showControls,
showSidebar,
temporaryChatEnabled,
user
} from '$lib/stores';
@@ -23,6 +24,7 @@
import MenuLines from '../icons/MenuLines.svelte';
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
import Map from '../icons/Map.svelte';
import { stringify } from 'postcss';
const i18n = getContext('i18n');
@@ -74,8 +76,7 @@
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
{#if shareEnabled && chat && chat.id}
{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
<Menu
{chat}
{shareEnabled}

View File

@@ -9,7 +9,13 @@
import { downloadChatAsPDF } from '$lib/apis/utils';
import { copyToClipboard, createMessagesList } from '$lib/utils';
import { showOverview, showControls, showArtifacts, mobile } from '$lib/stores';
import {
showOverview,
showControls,
showArtifacts,
mobile,
temporaryChatEnabled
} from '$lib/stores';
import { flyAndScale } from '$lib/utils/transitions';
import Dropdown from '$lib/components/common/Dropdown.svelte';
@@ -18,6 +24,7 @@
import Clipboard from '$lib/components/icons/Clipboard.svelte';
import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte';
import Cube from '$lib/components/icons/Cube.svelte';
import { getChatById } from '$lib/apis/chats';
const i18n = getContext('i18n');
@@ -31,9 +38,8 @@
export let onClose: Function = () => {};
const getChatAsText = async () => {
const _chat = chat.chat;
const messages = createMessagesList(_chat.history, _chat.history.currentId);
const history = chat.chat.history;
const messages = createMessagesList(history, history.currentId);
const chatText = messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
@@ -52,12 +58,9 @@
};
const downloadPdf = async () => {
const _chat = chat.chat;
const messages = createMessagesList(_chat.history, _chat.history.currentId);
console.log('download', chat);
const blob = await downloadChatAsPDF(_chat.title, messages);
const history = chat.chat.history;
const messages = createMessagesList(history, history.currentId);
const blob = await downloadChatAsPDF(chat.chat.title, messages);
// Create a URL for the blob
const url = window.URL.createObjectURL(blob);
@@ -65,7 +68,7 @@
// Create a link element to trigger the download
const a = document.createElement('a');
a.href = url;
a.download = `chat-${_chat.title}.pdf`;
a.download = `chat-${chat.chat.title}.pdf`;
// Append the link to the body and click it programmatically
document.body.appendChild(a);
@@ -79,6 +82,9 @@
};
const downloadJSONExport = async () => {
if (chat.id) {
chat = await getChatById(localStorage.token, chat.id);
}
let blob = new Blob([JSON.stringify([chat])], {
type: 'application/json'
});
@@ -189,27 +195,29 @@
<div class="flex items-center">{$i18n.t('Copy')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-share-button"
on:click={() => {
shareHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
{#if !$temporaryChatEnabled}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-share-button"
on:click={() => {
shareHandler();
}}
>
<path
fill-rule="evenodd"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
@@ -265,11 +273,13 @@
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
{#if !$temporaryChatEnabled}
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags chatId={chat.id} />
</div>
<div class="flex p-1">
<Tags chatId={chat.id} />
</div>
{/if}
</DropdownMenu.Content>
</div>
</Dropdown>

View File

@@ -19,7 +19,7 @@
showOverview,
showControls
} from '$lib/stores';
import { onMount, getContext, tick } from 'svelte';
import { onMount, getContext, tick, onDestroy } from 'svelte';
const i18n = getContext('i18n');
@@ -32,7 +32,10 @@
updateChatById,
getAllChatTags,
archiveChatById,
cloneChatById
cloneChatById,
getChatListBySearchText,
createNewChat,
getPinnedChatList
} from '$lib/apis/chats';
import { WEBUI_BASE_URL } from '$lib/constants';
@@ -42,6 +45,9 @@
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Spinner from '../common/Spinner.svelte';
import Loader from '../common/Loader.svelte';
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
import { select } from 'd3-selection';
const BREAKPOINT = 768;
@@ -58,33 +64,11 @@
let selectedTagName = null;
let filteredChatList = [];
// Pagination variables
let chatListLoading = false;
let allChatsLoaded = false;
$: filteredChatList = $chats.filter((chat) => {
if (search === '') {
return true;
} else {
let title = chat.title.toLowerCase();
const query = search.toLowerCase();
let contentMatches = false;
// Access the messages within chat.chat.messages
if (chat.chat && chat.chat.messages && Array.isArray(chat.chat.messages)) {
contentMatches = chat.chat.messages.some((message) => {
// Check if message.content exists and includes the search query
return message.content && message.content.toLowerCase().includes(query);
});
}
return title.includes(query) || contentMatches;
}
});
const enablePagination = async () => {
const initChatList = async () => {
// Reset pagination variables
currentChatPage.set(1);
allChatsLoaded = false;
@@ -98,7 +82,14 @@
chatListLoading = true;
currentChatPage.set($currentChatPage + 1);
const newChatList = await getChatList(localStorage.token, $currentChatPage);
let newChatList = [];
if (search) {
newChatList = await getChatListBySearchText(localStorage.token, search, $currentChatPage);
} else {
newChatList = await getChatList(localStorage.token, $currentChatPage);
}
// once the bottom of the list has been reached (no results) there is no need to continue querying
allChatsLoaded = newChatList.length === 0;
@@ -107,110 +98,26 @@
chatListLoading = false;
};
onMount(async () => {
mobile.subscribe((e) => {
if ($showSidebar && e) {
showSidebar.set(false);
}
let searchDebounceTimeout;
if (!$showSidebar && !e) {
showSidebar.set(true);
}
});
const searchDebounceHandler = async () => {
console.log('search', search);
chats.set(null);
selectedTagName = null;
showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
showSidebar.subscribe((value) => {
localStorage.sidebar = value;
});
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
await enablePagination();
let touchstart;
let touchend;
function checkDirection() {
const screenWidth = window.innerWidth;
const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
if (touchend.screenX < touchstart.screenX) {
showSidebar.set(false);
}
if (touchend.screenX > touchstart.screenX) {
showSidebar.set(true);
}
}
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
}
const onTouchStart = (e) => {
touchstart = e.changedTouches[0];
console.log(touchstart.clientX);
};
const onTouchEnd = (e) => {
touchend = e.changedTouches[0];
checkDirection();
};
const onKeyDown = (e) => {
if (e.key === 'Shift') {
shiftKey = true;
}
};
const onKeyUp = (e) => {
if (e.key === 'Shift') {
shiftKey = false;
}
};
const onFocus = () => {};
const onBlur = () => {
shiftKey = false;
selectedChatId = null;
};
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
};
});
// Helper function to fetch and add chat content to each chat
const enrichChatsWithContent = async (chatList) => {
const enrichedChats = await Promise.all(
chatList.map(async (chat) => {
const chatDetails = await getChatById(localStorage.token, chat.id).catch((error) => null); // Handle error or non-existent chat gracefully
if (chatDetails) {
chat.chat = chatDetails.chat; // Assuming chatDetails.chat contains the chat content
}
return chat;
})
);
await chats.set(enrichedChats);
};
const saveSettings = async (updated) => {
await settings.set({ ...$settings, ...updated });
await updateUserSettings(localStorage.token, { ui: $settings });
location.href = '/';
if (search === '') {
await initChatList();
return;
} else {
searchDebounceTimeout = setTimeout(async () => {
currentChatPage.set(1);
await chats.set(await getChatListBySearchText(localStorage.token, search));
}, 1000);
}
};
const deleteChatHandler = async (id) => {
@@ -230,9 +137,175 @@
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
await pinnedChats.set(await getPinnedChatList(localStorage.token));
}
};
const inputFilesHandler = async (files) => {
console.log(files);
for (const file of files) {
const reader = new FileReader();
reader.onload = async (e) => {
const content = e.target.result;
try {
const items = JSON.parse(content);
for (const item of items) {
if (item.chat) {
await createNewChat(localStorage.token, item.chat);
}
}
} catch {
toast.error($i18n.t(`Invalid file format.`));
}
initChatList();
};
reader.readAsText(file);
}
};
const tagEventHandler = async (type, tagName, chatId) => {
console.log(type, tagName, chatId);
if (type === 'delete') {
if (selectedTagName === tagName) {
if ($tags.map((t) => t.name).includes(tagName)) {
await chats.set(await getChatListByTagName(localStorage.token, tagName));
} else {
selectedTagName = null;
await initChatList();
}
}
}
};
let dragged = false;
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) {
const inputFiles = Array.from(e.dataTransfer?.files);
if (inputFiles && inputFiles.length > 0) {
console.log(inputFiles);
inputFilesHandler(inputFiles);
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
let touchstart;
let touchend;
function checkDirection() {
const screenWidth = window.innerWidth;
const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
if (touchend.screenX < touchstart.screenX) {
showSidebar.set(false);
}
if (touchend.screenX > touchstart.screenX) {
showSidebar.set(true);
}
}
}
const onTouchStart = (e) => {
touchstart = e.changedTouches[0];
console.log(touchstart.clientX);
};
const onTouchEnd = (e) => {
touchend = e.changedTouches[0];
checkDirection();
};
const onKeyDown = (e) => {
if (e.key === 'Shift') {
shiftKey = true;
}
};
const onKeyUp = (e) => {
if (e.key === 'Shift') {
shiftKey = false;
}
};
const onFocus = () => {};
const onBlur = () => {
shiftKey = false;
selectedChatId = null;
};
onMount(async () => {
mobile.subscribe((e) => {
if ($showSidebar && e) {
showSidebar.set(false);
}
if (!$showSidebar && !e) {
showSidebar.set(true);
}
});
showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
showSidebar.subscribe((value) => {
localStorage.sidebar = value;
});
await pinnedChats.set(await getPinnedChatList(localStorage.token));
await initChatList();
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
const dropZone = document.getElementById('sidebar');
dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave);
});
onDestroy(() => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
const dropZone = document.getElementById('sidebar');
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
});
</script>
<ArchivedChatsModal
@@ -274,6 +347,18 @@
"
data-state={$showSidebar}
>
{#if dragged}
<div
class="absolute w-full h-full max-h-full backdrop-blur bg-gray-800/40 flex justify-center z-[999] touch-none pointer-events-none"
>
<div class="m-auto pt-64 flex flex-col justify-center">
<AddFilesPlaceholder
title={$i18n.t('Drop Chat Export')}
content={$i18n.t('Drop a chat export file here to import it.')}
/>
</div>
</div>
{/if}
<div
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
? ''
@@ -419,30 +504,27 @@
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
placeholder={$i18n.t('Search')}
bind:value={search}
on:focus={async () => {
// TODO: migrate backend for more scalable search mechanism
scrollPaginationEnabled.set(false);
await chats.set(await getChatList(localStorage.token)); // when searching, load all chats
enrichChatsWithContent($chats);
on:input={() => {
searchDebounceHandler();
}}
/>
</div>
</div>
{#if $tags.filter((t) => t.name !== 'pinned').length > 0}
<div class="px-3.5 mb-1 flex gap-0.5 flex-wrap">
{#if $tags.length > 0}
<div class="px-3.5 mb-2.5 flex gap-0.5 flex-wrap">
<button
class="px-2.5 py-[1px] text-xs transition {selectedTagName === null
? 'bg-gray-100 dark:bg-gray-900'
: ' '} rounded-md font-medium"
on:click={async () => {
selectedTagName = null;
await enablePagination();
await initChatList();
}}
>
{$i18n.t('all')}
</button>
{#each $tags.filter((t) => t.name !== 'pinned') as tag}
{#each $tags as tag}
<button
class="px-2.5 py-[1px] text-xs transition {selectedTagName === tag.name
? 'bg-gray-100 dark:bg-gray-900'
@@ -450,15 +532,15 @@
on:click={async () => {
selectedTagName = tag.name;
scrollPaginationEnabled.set(false);
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
let taggedChatList = await getChatListByTagName(localStorage.token, tag.name);
if (taggedChatList.length === 0) {
await tags.set(await getAllChatTags(localStorage.token));
// if the tag we deleted is no longer a valid tag, return to main chat list view
await enablePagination();
await initChatList();
} else {
await chats.set(taggedChatList);
}
await chats.set(chatIds);
chatListLoading = false;
}}
>
@@ -469,7 +551,7 @@
{/if}
{#if !search && $pinnedChats.length > 0}
<div class="pl-2 py-2 flex flex-col space-y-1">
<div class="pl-2 pb-2 flex flex-col space-y-1">
<div class="">
<div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5">
{$i18n.t('Pinned')}
@@ -494,22 +576,27 @@
showDeleteConfirm = true;
}
}}
on:tag={(e) => {
const { type, name } = e.detail;
tagEventHandler(type, name, chat.id);
}}
/>
{/each}
</div>
</div>
{/if}
<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
{#each filteredChatList as chat, idx}
{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
<div
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
? ''
: 'pt-5'} pb-0.5"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
<div class="pl-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
{#if $chats}
{#each $chats as chat, idx}
{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
<div
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
? ''
: 'pt-5'} pb-0.5"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
{$i18n.t('Today')}
{$i18n.t('Yesterday')}
{$i18n.t('Previous 7 days')}
@@ -527,43 +614,53 @@
{$i18n.t('November')}
{$i18n.t('December')}
-->
</div>
</div>
{/if}
<ChatItem
{chat}
{shiftKey}
selected={selectedChatId === chat.id}
on:select={() => {
selectedChatId = chat.id;
}}
on:unselect={() => {
selectedChatId = null;
}}
on:delete={(e) => {
if ((e?.detail ?? '') === 'shift') {
deleteChatHandler(chat.id);
} else {
deleteChat = chat;
showDeleteConfirm = true;
}
}}
on:tag={(e) => {
const { type, name } = e.detail;
tagEventHandler(type, name, chat.id);
}}
/>
{/each}
{#if $scrollPaginationEnabled && !allChatsLoaded}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadMoreChats();
}
}}
>
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{/if}
<ChatItem
{chat}
{shiftKey}
selected={selectedChatId === chat.id}
on:select={() => {
selectedChatId = chat.id;
}}
on:unselect={() => {
selectedChatId = null;
}}
on:delete={(e) => {
if ((e?.detail ?? '') === 'shift') {
deleteChatHandler(chat.id);
} else {
deleteChat = chat;
showDeleteConfirm = true;
}
}}
/>
{/each}
{#if $scrollPaginationEnabled && !allChatsLoaded}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadMoreChats();
}
}}
>
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{:else}
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
{/if}
</div>
</div>

View File

@@ -12,6 +12,7 @@
deleteChatById,
getChatList,
getChatListByTagName,
getPinnedChatList,
updateChatById
} from '$lib/apis/chats';
import {
@@ -55,7 +56,7 @@
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
await pinnedChats.set(await getPinnedChatList(localStorage.token));
}
};
@@ -70,7 +71,7 @@
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
await pinnedChats.set(await getPinnedChatList(localStorage.token));
}
};
@@ -79,7 +80,7 @@
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
await pinnedChats.set(await getPinnedChatList(localStorage.token));
};
const focusEdit = async (node: HTMLInputElement) => {
@@ -256,7 +257,10 @@
dispatch('unselect');
}}
on:change={async () => {
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
await pinnedChats.set(await getPinnedChatList(localStorage.token));
}}
on:tag={(e) => {
dispatch('tag', e.detail);
}}
>
<button

View File

@@ -15,7 +15,13 @@
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import Bookmark from '$lib/components/icons/Bookmark.svelte';
import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte';
import { addTagById, deleteTagById, getTagsById } from '$lib/apis/chats';
import {
addTagById,
deleteTagById,
getChatPinnedStatusById,
getTagsById,
toggleChatPinnedStatusById
} from '$lib/apis/chats';
const i18n = getContext('i18n');
@@ -32,20 +38,12 @@
let pinned = false;
const pinHandler = async () => {
if (pinned) {
await deleteTagById(localStorage.token, chatId, 'pinned');
} else {
await addTagById(localStorage.token, chatId, 'pinned');
}
await toggleChatPinnedStatusById(localStorage.token, chatId);
dispatch('change');
};
const checkPinned = async () => {
pinned = (
await getTagsById(localStorage.token, chatId).catch(async (error) => {
return [];
})
).find((tag) => tag.name === 'pinned');
pinned = await getChatPinnedStatusById(localStorage.token, chatId);
};
$: if (show) {
@@ -143,6 +141,13 @@
<div class="flex p-1">
<Tags
{chatId}
on:delete={(e) => {
dispatch('tag', {
type: 'delete',
name: e.detail.name
});
show = false;
}}
on:close={() => {
show = false;
onClose();

View File

@@ -1,627 +0,0 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, documents, showSidebar } from '$lib/stores';
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
import { processFile } from '$lib/apis/retrieval';
import { blobToFile, transformFileName } from '$lib/utils';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
import { transcribeAudio } from '$lib/apis/audio';
import { uploadFile } from '$lib/apis/files';
const i18n = getContext('i18n');
let importFiles = '';
let inputFiles = '';
let query = '';
let documentsImportInputElement: HTMLInputElement;
let tags = [];
let showSettingsModal = false;
let showAddDocModal = false;
let showEditDocModal = false;
let selectedDoc;
let selectedTag = '';
let dragged = false;
const deleteDoc = async (name) => {
await deleteDocByName(localStorage.token, name);
await documents.set(await getDocs(localStorage.token));
};
const deleteDocs = async (docs) => {
const res = await Promise.all(
docs.map(async (doc) => {
return await deleteDocByName(localStorage.token, doc.name);
})
);
await documents.set(await getDocs(localStorage.token));
};
const uploadDoc = async (file, tags?: object) => {
console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (transcribeRes) {
console.log(transcribeRes);
const blob = new Blob([transcribeRes.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
}
// Upload the file to the server
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
const res = await processFile(localStorage.token, uploadedFile.id).catch((error) => {
toast.error(error);
return null;
});
if (res) {
await createNewDoc(
localStorage.token,
res.collection_name,
res.filename,
transformFileName(res.filename),
res.filename,
tags?.length > 0
? {
tags: tags
}
: null
).catch((error) => {
toast.error(error);
return null;
});
await documents.set(await getDocs(localStorage.token));
}
};
onMount(() => {
documents.subscribe((docs) => {
tags = docs.reduce((a, e, i, arr) => {
return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
}, []);
});
const dropZone = document.querySelector('body');
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
if (e.dataTransfer?.files) {
let reader = new FileReader();
reader.onload = (event) => {
files = [
...files,
{
type: 'image',
url: `${event.target.result}`
}
];
};
const inputFiles = e.dataTransfer?.files;
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
console.log(file, file.name.split('.').at(-1));
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else {
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
);
uploadDoc(file);
}
}
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave);
return () => {
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
};
});
let filteredDocs;
$: filteredDocs = $documents.filter(
(doc) =>
(selectedTag === '' ||
(doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) &&
(query === '' || doc.name.includes(query))
);
</script>
<svelte:head>
<title>
{$i18n.t('Documents')} | {$WEBUI_NAME}
</title>
</svelte:head>
{#if dragged}
<div
class="fixed {$showSidebar
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
>
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<AddFilesPlaceholder>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents
</div>
</AddFilesPlaceholder>
</div>
</div>
</div>
</div>
{/if}
{#key selectedDoc}
<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
{/key}
<AddDocModal bind:show={showAddDocModal} {uploadDoc} />
<div class="mb-3">
<div class="flex justify-between items-center">
<div class="flex md:self-center text-lg font-medium px-0.5">
{$i18n.t('Documents')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$documents.length}</span>
</div>
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Documents')}
/>
</div>
<div>
<button
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
aria-label={$i18n.t('Add Docs')}
on:click={() => {
showAddDocModal = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
</div>
<!-- <div>
<div
class="my-3 py-16 rounded-lg border-2 border-dashed dark:border-gray-600 {dragged &&
' dark:bg-gray-700'} "
role="region"
on:drop={onDrop}
on:dragover={onDragOver}
on:dragleave={onDragLeave}
>
<div class=" pointer-events-none">
<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents
</div>
</div>
</div>
</div> -->
<hr class=" dark:border-gray-850 my-2.5" />
{#if tags.length > 0}
<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
<div class="ml-0.5 pr-3 my-auto flex items-center">
<Checkbox
state={filteredDocs.filter((doc) => doc?.selected === 'checked').length ===
filteredDocs.length
? 'checked'
: 'unchecked'}
indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 &&
filteredDocs.filter((doc) => doc?.selected === 'checked').length !== filteredDocs.length}
on:change={(e) => {
if (e.detail === 'checked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' }));
} else if (e.detail === 'unchecked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' }));
}
}}
/>
</div>
{#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('all')}</div>
</button>
{#each tags as tag}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = tag;
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
#{tag}
</div>
</button>
{/each}
{:else}
<div class="flex-1 flex w-full justify-between items-center">
<div class="text-xs font-medium py-0.5 self-center mr-1">
{filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected
</div>
<div class="flex gap-1">
<!-- <button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">add tags</div>
</button> -->
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked'));
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
{$i18n.t('delete')}
</div>
</button>
</div>
</div>
{/if}
</div>
{/if}
<div class="my-3 mb-5">
{#each filteredDocs as doc}
<button
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
on:click={() => {
if (doc?.selected === 'checked') {
doc.selected = 'unchecked';
} else {
doc.selected = 'checked';
}
}}
>
<div class="my-auto flex items-center">
<Checkbox state={doc?.selected ?? 'unchecked'} />
</div>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<div class=" flex items-center space-x-3">
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if doc}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else}
<svg
class=" w-6 h-6 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
{/if}
</div>
<div class=" self-center flex-1">
<div class=" font-semibold line-clamp-1">#{doc.name} ({doc.filename})</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{doc.title}
</div>
</div>
</div>
</div>
<div class="flex flex-row space-x-1 self-center">
<button
class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
aria-label={$i18n.t('Edit Doc')}
on:click={async (e) => {
e.stopPropagation();
showEditDocModal = !showEditDocModal;
selectedDoc = doc;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<!-- <button
class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
type="button"
on:click={() => {
console.log('download file');
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</button> -->
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
aria-label={$i18n.t('Delete Doc')}
on:click={(e) => {
e.stopPropagation();
deleteDoc(doc.name);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div>
</button>
{/each}
</div>
<div class=" text-gray-500 text-xs mt-1 mb-2">
{$i18n.t("Use '#' in the prompt input to load and select your documents.")}
</div>
<div class=" flex justify-end w-full mb-2">
<div class="flex space-x-2">
<input
id="documents-import-input"
bind:this={documentsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
const reader = new FileReader();
reader.onload = async (event) => {
const savedDocs = JSON.parse(event.target.result);
console.log(savedDocs);
for (const doc of savedDocs) {
await createNewDoc(
localStorage.token,
doc.collection_name,
doc.filename,
doc.name,
doc.title,
doc.content
).catch((error) => {
toast.error(error);
return null;
});
}
await documents.set(await getDocs(localStorage.token));
};
reader.readAsText(importFiles[0]);
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
documentsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Import Documents Mapping')}
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
let blob = new Blob([JSON.stringify($documents)], {
type: 'application/json'
});
saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Documents Mapping')}
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
</div>

View File

@@ -181,7 +181,8 @@
{/if}
</div>
<div class=" text-xs text-gray-500">
Updated {dayjs(item.updated_at * 1000).fromNow()}
{$i18n.t('Updated')}
{dayjs(item.updated_at * 1000).fromNow()}
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import Fuse from 'fuse.js';
import { toast } from 'svelte-sonner';
import { v4 as uuidv4 } from 'uuid';
import { onMount, getContext, onDestroy, tick } from 'svelte';
const i18n = getContext('i18n');
@@ -101,6 +102,7 @@
const uploadFileHandler = async (file) => {
console.log(file);
const tempItemId = uuidv4();
const fileItem = {
type: 'file',
file: '',
@@ -109,7 +111,8 @@
name: file.name,
size: file.size,
status: 'uploading',
error: ''
error: '',
itemId: tempItemId
};
knowledge.files = [...(knowledge.files ?? []), fileItem];
@@ -131,10 +134,20 @@
try {
const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
toast.error(e);
return null;
});
if (uploadedFile) {
console.log(uploadedFile);
knowledge.files = knowledge.files.map((item) => {
if (item.itemId === tempItemId) {
item.id = uploadedFile.id;
}
// Remove temporary item id
delete item.itemId;
return item;
});
await addFileHandler(uploadedFile.id);
} else {
toast.error($i18n.t('Failed to upload file.'));
@@ -329,12 +342,16 @@
const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
(e) => {
toast.error(e);
return null;
}
);
if (updatedKnowledge) {
knowledge = updatedKnowledge;
toast.success($i18n.t('File added successfully.'));
} else {
toast.error($i18n.t('Failed to add file.'));
knowledge.files = knowledge.files.filter((file) => file.id !== fileId);
}
};
@@ -517,10 +534,10 @@
type="file"
multiple
hidden
on:change={() => {
on:change={async () => {
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
uploadFileHandler(file);
await uploadFileHandler(file);
}
inputFiles = null;
@@ -536,65 +553,38 @@
/>
<div class="flex flex-col w-full max-h-[100dvh] h-full">
<button
class="flex space-x-1 w-fit"
on:click={() => {
goto('/workspace/knowledge');
}}
>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button>
<div class="flex items-center justify-between">
<button
class="flex space-x-1 w-fit"
on:click={() => {
goto('/workspace/knowledge');
}}
>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button>
<div class=" flex-shrink-0">
<div>
<Badge type="success" content="Collection" />
</div>
</div>
</div>
<div class="flex flex-col my-2 flex-1 overflow-auto h-0">
{#if id && knowledge}
<div class=" flex w-full mt-1 mb-3.5">
<div class="flex-1">
<div class="flex items-center justify-between w-full px-0.5 mb-1">
<div class="w-full">
<input
type="text"
class="w-full font-medium text-2xl font-primary bg-transparent outline-none"
bind:value={knowledge.name}
on:input={() => {
changeDebounceHandler();
}}
/>
</div>
<div class=" flex-shrink-0">
<div>
<Badge type="success" content="Collection" />
</div>
</div>
</div>
<div class="flex w-full px-1">
<input
type="text"
class="w-full text-gray-500 text-sm bg-transparent outline-none"
bind:value={knowledge.description}
on:input={() => {
changeDebounceHandler();
}}
/>
</div>
</div>
</div>
<div class="flex flex-row h-0 flex-1 overflow-auto">
<div
class=" {largeScreen
@@ -623,6 +613,9 @@
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Collection')}
on:focus={() => {
selectedFileId = null;
}}
/>
<div>
@@ -652,7 +645,7 @@
files={filteredItems}
{selectedFileId}
on:click={(e) => {
selectedFileId = e.detail;
selectedFileId = selectedFileId === e.detail ? null : e.detail;
}}
on:delete={(e) => {
console.log(e.detail);
@@ -663,7 +656,7 @@
/>
</div>
{:else}
<div class="m-auto text-gray-500 text-xs">No content found</div>
<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
{/if}
</div>
</div>
@@ -699,12 +692,40 @@
</div>
</div>
{:else}
<div class="m-auto">
<AddFilesPlaceholder title={$i18n.t('Select/Add Files')}>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Select a file to view or drag and drop a file to upload
<div class="m-auto pb-32">
<div>
<div class=" flex w-full mt-1 mb-3.5">
<div class="flex-1">
<div class="flex items-center justify-between w-full px-0.5 mb-1">
<div class="w-full">
<input
type="text"
class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
bind:value={knowledge.name}
on:input={() => {
changeDebounceHandler();
}}
/>
</div>
</div>
<div class="flex w-full px-1">
<input
type="text"
class="text-center w-full text-gray-500 bg-transparent outline-none"
bind:value={knowledge.description}
on:input={() => {
changeDebounceHandler();
}}
/>
</div>
</div>
</div>
</AddFilesPlaceholder>
</div>
<div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
{$i18n.t('Select a file to view or drag and drop a file to upload')}
</div>
</div>
{/if}
</div>

View File

@@ -59,7 +59,7 @@
<div class="mb-3 w-full">
<div class="w-full flex flex-col gap-2.5">
<div class="w-full">
<div class=" text-sm mb-2">Title</div>
<div class=" text-sm mb-2">{$i18n.t('Title')}</div>
<div class="w-full mt-1">
<input
@@ -73,7 +73,7 @@
</div>
<div>
<div class="text-sm mb-2">Content</div>
<div class="text-sm mb-2">{$i18n.t('Content')}</div>
<div class=" w-full mt-1">
<textarea

View File

@@ -414,7 +414,9 @@
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
{!!model?.info?.meta?.description
? model?.info?.meta?.description
: (model?.ollama?.digest ?? model.id)}
: model?.ollama?.digest
? `${model.id} (${model?.ollama?.digest})`
: model.id}
</div>
</div>
</a>