enh: files chat control

This commit is contained in:
Timothy J. Baek 2024-07-17 11:39:37 +02:00
parent a33b0abbe0
commit 4eecdbadd3
6 changed files with 216 additions and 261 deletions

View File

@ -98,6 +98,8 @@
let title = ''; let title = '';
let prompt = ''; let prompt = '';
let chatFiles = [];
let files = []; let files = [];
let messages = []; let messages = [];
let history = { let history = {
@ -333,6 +335,7 @@
} }
params = chatContent?.params ?? {}; params = chatContent?.params ?? {};
chatFiles = chatContent?.files ?? {};
autoScroll = true; autoScroll = true;
await tick(); await tick();
@ -408,7 +411,8 @@
models: selectedModels, models: selectedModels,
messages: messages, messages: messages,
history: history, history: history,
params: params params: params,
files: chatFiles
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
@ -453,7 +457,8 @@
models: selectedModels, models: selectedModels,
messages: messages, messages: messages,
history: history, history: history,
params: params params: params,
files: chatFiles
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
@ -514,6 +519,13 @@
} }
const _files = JSON.parse(JSON.stringify(files)); const _files = JSON.parse(JSON.stringify(files));
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
chatFiles = chatFiles.filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
files = []; files = [];
prompt = ''; prompt = '';
@ -754,25 +766,10 @@
} }
}); });
let files = []; let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
files = model.info.meta.knowledge; files.push(...model.info.meta.knowledge);
} }
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? []),
...(responseMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
eventTarget.dispatchEvent( eventTarget.dispatchEvent(
new CustomEvent('chat:start', { new CustomEvent('chat:start', {
@ -936,7 +933,8 @@
messages: messages, messages: messages,
history: history, history: history,
models: selectedModels, models: selectedModels,
params: params params: params,
files: chatFiles
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
@ -1003,24 +1001,10 @@
let _response = null; let _response = null;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
let files = []; let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) { if (model?.info?.meta?.knowledge ?? false) {
files = model.info.meta.knowledge; files.push(...model.info.meta.knowledge);
} }
const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
files = [
...files,
...(lastUserMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? []),
...(responseMessage?.files?.filter((item) =>
['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
) ?? [])
].filter(
// Remove duplicates
(item, index, array) =>
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
);
scrollToBottom(); scrollToBottom();
@ -1214,7 +1198,8 @@
models: selectedModels, models: selectedModels,
messages: messages, messages: messages,
history: history, history: history,
params: params params: params,
files: chatFiles
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
@ -1632,6 +1617,7 @@
return a; return a;
}, [])} }, [])}
bind:show={showControls} bind:show={showControls}
bind:chatFiles
bind:params bind:params
bind:valves bind:valves
/> />

View File

@ -9,8 +9,9 @@
export let models = []; export let models = [];
export let chatId = null; export let chatId = null;
export let valves = {};
export let chatFiles = [];
export let valves = {};
export let params = {}; export let params = {};
let largeScreen = false; let largeScreen = false;
@ -48,6 +49,7 @@
show = false; show = false;
}} }}
{models} {models}
bind:chatFiles
bind:valves bind:valves
bind:params bind:params
/> />
@ -63,6 +65,7 @@
show = false; show = false;
}} }}
{models} {models}
bind:chatFiles
bind:valves bind:valves
bind:params bind:params
/> />

View File

@ -6,8 +6,11 @@
import XMark from '$lib/components/icons/XMark.svelte'; import XMark from '$lib/components/icons/XMark.svelte';
import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte'; import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
import Valves from '$lib/components/common/Valves.svelte'; import Valves from '$lib/components/common/Valves.svelte';
import FileItem from '$lib/components/common/FileItem.svelte';
export let models = []; export let models = [];
export let chatFiles = [];
export let valves = {}; export let valves = {};
export let params = {}; export let params = {};
</script> </script>
@ -26,9 +29,33 @@
</div> </div>
<div class=" dark:text-gray-200 text-sm font-primary"> <div class=" dark:text-gray-200 text-sm font-primary">
{#if chatFiles.length > 0}
<div>
<div class="mb-1.5 font-medium">{$i18n.t('Files')}</div>
<div>
{#each chatFiles as file}
<FileItem
className="w-full"
url={`${file?.url}`}
name={file.name}
type={file.type}
dismissible={true}
on:dismiss={() => {
// Remove the file from the chatFiles array
chatFiles = chatFiles.filter((f) => f.id !== file.id);
}}
/>
{/each}
</div>
</div>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
{/if}
{#if models.length === 1 && models[0]?.pipe?.valves_spec} {#if models.length === 1 && models[0]?.pipe?.valves_spec}
<div> <div>
<div class=" font-medium">Valves</div> <div class=" font-medium">{$i18n.t('Valves')}</div>
<div> <div>
<Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves /> <Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves />

View File

@ -40,6 +40,7 @@
import Headphone from '../icons/Headphone.svelte'; import Headphone from '../icons/Headphone.svelte';
import VoiceRecording from './MessageInput/VoiceRecording.svelte'; import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import { transcribeAudio } from '$lib/apis/audio'; import { transcribeAudio } from '$lib/apis/audio';
import FileItem from '../common/FileItem.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -502,8 +503,8 @@
{#if files.length > 0} {#if files.length > 0}
<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2"> <div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
{#each files as file, fileIdx} {#each files as file, fileIdx}
<div class=" relative group">
{#if file.type === 'image'} {#if file.type === 'image'}
<div class=" relative group">
<div class="relative"> <div class="relative">
<img <img
src={file.url} src={file.url}
@ -534,115 +535,6 @@
</Tooltip> </Tooltip>
{/if} {/if}
</div> </div>
{:else if ['doc', 'file'].includes(file.type)}
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-none"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if file.status === 'processed'}
<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="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div>
</div>
{:else if file.type === 'collection'}
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-none"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
/>
<path
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</div>
{/if}
<div class=" absolute -top-1 -right-1"> <div class=" absolute -top-1 -right-1">
<button <button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition" class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
@ -665,6 +557,17 @@
</button> </button>
</div> </div>
</div> </div>
{:else}
<FileItem
name={file.name}
type={file.type}
dismissible={true}
on:dismiss={() => {
files.splice(fileIdx, 1);
files = files;
}}
/>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -9,6 +9,7 @@
import { user as _user } from '$lib/stores'; import { user as _user } from '$lib/stores';
import { getFileContentById } from '$lib/apis/files'; import { getFileContentById } from '$lib/apis/files';
import FileItem from '$lib/components/common/FileItem.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -99,106 +100,11 @@
{#if file.type === 'image'} {#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" /> <img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else if file.type === 'file'} {:else if file.type === 'file'}
<button <FileItem url={`${file?.url}/content`} name={file.name} type={$i18n.t('File')} />
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
on:click={async () => {
if (file?.url) {
window.open(`${file?.url}/content`, '_blank').focus();
}
}}
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<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>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('File')}</div>
</div>
</button>
{:else if file.type === 'doc'} {:else if file.type === 'doc'}
<button <FileItem url={`${file?.url}`} name={file.name} type={$i18n.t('Document')} />
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
on:click={() => {
if (file?.url) {
window.open(file?.url, '_blank').focus();
}
}}
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<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>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div>
</button>
{:else if file.type === 'collection'} {:else if file.type === 'collection'}
<button <FileItem name={file?.title ?? `#${file.name}`} type={$i18n.t('Collection')} />
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
/>
<path
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</button>
{/if} {/if}
</div> </div>
{/each} {/each}

View File

@ -0,0 +1,130 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let className = 'w-72';
export let url: string | null = null;
export let dismissible = false;
export let status = 'processed';
export let name: string;
export let type: string;
</script>
<div class="relative group">
<button
class="h-14 {className} flex items-center space-x-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-100 dark:border-gray-800 text-left"
type="button"
on:click={async () => {
if (url) {
if (type === 'file') {
window.open(`${url}/content`, '_blank').focus();
} else {
window.open(`${url}`, '_blank').focus();
}
}
}}
>
<div class="p-4 py-[1.1rem] bg-red-400 text-white rounded-l-lg">
{#if status === 'processed'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class=" size-5"
>
<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=" size-5 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="flex flex-col justify-center -space-y-0.5 pl-1.5 pr-4 w-full">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{name}
</div>
<div class=" text-gray-500 text-xs">
{#if type === 'file'}
{$i18n.t('File')}
{:else if type === 'doc'}
{$i18n.t('Document')}
{:else if type === 'collection'}
{$i18n.t('Collection')}
{:else}
<span class=" capitalize">{type}</span>
{/if}
</div>
</div>
</button>
{#if dismissible}
<div class=" absolute -top-1 -right-1">
<button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
dispatch('dismiss');
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<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>
{/if}
</div>