mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
Merge branch 'upstream-dev' into dev
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
15
src/lib/components/icons/ArrowLeft.svelte
Normal file
15
src/lib/components/icons/ArrowLeft.svelte
Normal 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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user