Merge branch 'upstream-dev' into dev
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user