feat: chat_file table

This commit is contained in:
Timothy Jaeryang Baek
2025-12-21 23:17:53 +04:00
parent a3458f492c
commit f1bf4f20c5
14 changed files with 382 additions and 101 deletions
@@ -116,7 +116,6 @@
// Check for known image types
for (const type of item.types) {
if (type.startsWith('image/')) {
// get as file
const blob = await item.getType(type);
const file = new File([blob], `clipboard-image.${type.split('/')[1]}`, {
type: type
@@ -486,7 +485,7 @@
fileItem.collection_name =
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
fileItem.content_type = uploadedFile.meta?.content_type || uploadedFile.content_type;
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
fileItem.url = `${uploadedFile.id}`;
files = files;
} else {
@@ -802,10 +801,14 @@
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
{#each files as file, fileIdx}
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
{@const fileUrl =
file.url.startsWith('data') || file.url.startsWith('http')
? file.url
: `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`}
<div class=" relative group">
<div class="relative">
<Image
src={`${file.url}${file?.content_type ? '/content' : ''}`}
src={fileUrl}
alt=""
imageClassName=" size-10 rounded-xl object-cover"
/>
@@ -928,8 +931,7 @@
for (const item of clipboardData.items) {
const file = item.getAsFile();
if (file) {
const _files = [file];
await inputFilesHandler(_files);
await inputFilesHandler([file]);
e.preventDefault();
}
}
@@ -343,19 +343,15 @@
dir={$settings?.chatDirection ?? 'auto'}
>
{#each message?.data?.files as file}
{@const fileUrl =
file.url.startsWith('data') || file.url.startsWith('http')
? file.url
: `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`}
<div>
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
<Image
src={`${file.url}${file?.content_type ? '/content' : ''}`}
alt={file.name}
imageClassName=" max-h-96 rounded-lg"
/>
<Image src={fileUrl} alt={file.name} imageClassName=" max-h-96 rounded-lg" />
{:else if file.type === 'video' || (file?.content_type ?? '').startsWith('video/')}
<video
src={`${file.url}${file?.content_type ? '/content' : ''}`}
controls
class=" max-h-96 rounded-lg"
></video>
<video src={fileUrl} controls class=" max-h-96 rounded-lg"></video>
{:else}
<FileItem
item={file}
+35 -25
View File
@@ -759,7 +759,7 @@
fileItem.id = uploadedFile.id;
fileItem.size = file.size;
fileItem.collection_name = uploadedFile?.meta?.collection_name;
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
fileItem.url = `${uploadedFile.id}`;
files = files;
toast.success($i18n.t('File uploaded successfully'));
@@ -1601,8 +1601,10 @@
const _files = JSON.parse(JSON.stringify(files));
chatFiles.push(
..._files.filter((item) =>
['doc', 'text', 'file', 'note', 'chat', 'folder', 'collection'].includes(item.type)
..._files.filter(
(item) =>
['doc', 'text', 'note', 'chat', 'folder', 'collection'].includes(item.type) ||
(item.type === 'file' && !item?.content_type?.startsWith('image/'))
)
);
chatFiles = chatFiles.filter(
@@ -1730,7 +1732,9 @@
if (model) {
// If there are image files, check if model is vision capable
const hasImages = createMessagesList(_history, parentId).some((message) =>
message.files?.some((file) => file.type === 'image')
message.files?.some(
(file) => file.type === 'image' || file?.content_type?.startsWith('image/')
)
);
if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) {
@@ -1824,8 +1828,10 @@
let files = JSON.parse(JSON.stringify(chatFiles));
files.push(
...(userMessage?.files ?? []).filter((item) =>
['doc', 'text', 'file', 'note', 'chat', 'collection'].includes(item.type)
...(userMessage?.files ?? []).filter(
(item) =>
['doc', 'text', 'note', 'chat', 'collection'].includes(item.type) ||
(item.type === 'file' && !item?.content_type.startsWith('image/'))
)
);
// Remove duplicates
@@ -1872,30 +1878,33 @@
].filter((message) => message);
messages = messages
.map((message, idx, arr) => ({
role: message.role,
...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
message.role === 'user'
? {
content: [
{
type: 'text',
text: message?.merged?.content ?? message.content
},
...message.files
.filter((file) => file.type === 'image')
.map((file) => ({
.map((message, idx, arr) => {
const imageFiles = (message?.files ?? []).filter(
(file) => file.type === 'image' || (file?.content_type ?? '').startsWith('image/')
);
return {
role: message.role,
...(message.role === 'user' && imageFiles.length > 0
? {
content: [
{
type: 'text',
text: message?.merged?.content ?? message.content
},
...imageFiles.map((file) => ({
type: 'image_url',
image_url: {
url: file.url
}
}))
]
}
: {
content: message?.merged?.content ?? message.content
})
}))
]
}
: {
content: message?.merged?.content ?? message.content
})
};
})
.filter((message) => message?.role === 'user' || message?.content?.trim());
const toolIds = [];
@@ -1950,6 +1959,7 @@
id: responseMessageId,
parent_id: userMessage?.id ?? null,
parent_message: userMessage,
background_tasks: {
...(!$temporaryChatEnabled &&
+37 -51
View File
@@ -191,17 +191,11 @@
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
const reader = new FileReader();
reader.onload = (event) => {
files = [
...files,
{
type: 'image',
url: event.target.result as string
}
];
};
reader.readAsDataURL(blob);
const file = new File([blob], `clipboard-image.${type.split('/')[1]}`, {
type: type
});
inputFilesHandler([file]);
}
}
}
@@ -527,8 +521,9 @@
// Convert the canvas to a Base64 image URL
const imageUrl = canvas.toDataURL('image/png');
// Add the captured image to the files array to render it
files = [...files, { type: 'image', url: imageUrl }];
const blob = await (await fetch(imageUrl)).blob();
const file = new File([blob], `screen-capture-${Date.now()}.png`, { type: 'image/png' });
inputFilesHandler([file]);
// Clean memory: Clear video srcObject
video.srcObject = null;
} catch (error) {
@@ -537,7 +532,7 @@
}
};
const uploadFileHandler = async (file, fullContext: boolean = false) => {
const uploadFileHandler = async (file, process = true, itemData = {}) => {
if ($_user?.role !== 'admin' && !($_user?.permissions?.chat?.file_upload ?? true)) {
toast.error($i18n.t('You do not have permission to upload files.'));
return null;
@@ -560,7 +555,7 @@
size: file.size,
error: '',
itemId: tempItemId,
...(fullContext ? { context: 'full' } : {})
...itemData
};
if (fileItem.size == 0) {
@@ -584,7 +579,7 @@
}
// During the file upload, file content is automatically extracted.
const uploadedFile = await uploadFile(localStorage.token, file, metadata);
const uploadedFile = await uploadFile(localStorage.token, file, metadata, process);
if (uploadedFile) {
console.log('File upload completed:', {
@@ -603,7 +598,8 @@
fileItem.id = uploadedFile.id;
fileItem.collection_name =
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
fileItem.content_type = uploadedFile.meta?.content_type || uploadedFile.content_type;
fileItem.url = `${uploadedFile.id}`;
files = files;
} else {
@@ -726,19 +722,21 @@
};
let reader = new FileReader();
reader.onload = async (event) => {
let imageUrl = event.target.result;
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
// Compress the image if settings or config require it
if ($settings?.imageCompression && $settings?.imageCompressionInChannels) {
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
}
files = [
...files,
{
type: 'image',
url: `${imageUrl}`
}
];
const blob = await (await fetch(imageUrl)).blob();
const compressedFile = new File([blob], file.name, { type: file.type });
uploadFileHandler(compressedFile, false);
};
reader.readAsDataURL(file['type'] === 'image/heic' ? await convertHeicToJpeg(file) : file);
} else {
uploadFileHandler(file);
@@ -1146,11 +1144,15 @@
dir={$settings?.chatDirection ?? 'auto'}
>
{#each files as file, fileIdx}
{#if file.type === 'image'}
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
{@const fileUrl =
file.url.startsWith('data') || file.url.startsWith('http')
? file.url
: `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`}
<div class=" relative group">
<div class="relative flex items-center">
<Image
src={file.url}
src={fileUrl}
alt=""
imageClassName=" size-10 rounded-xl object-cover"
/>
@@ -1392,29 +1394,7 @@
if (clipboardData && clipboardData.items) {
for (const item of clipboardData.items) {
if (item.type.indexOf('image') !== -1) {
const blob = item.getAsFile();
const reader = new FileReader();
reader.onload = function (e) {
files = [
...files,
{
type: 'image',
url: `${e.target.result}`
}
];
};
reader.readAsDataURL(blob);
} else if (item?.kind === 'file') {
const file = item.getAsFile();
if (file) {
const _files = [file];
await inputFilesHandler(_files);
e.preventDefault();
}
} else if (item.type === 'text/plain') {
if (item.type === 'text/plain') {
if (($settings?.largeTextAsFile ?? false) && !shiftKey) {
const text = clipboardData.getData('text/plain');
@@ -1429,9 +1409,15 @@
}
);
await uploadFileHandler(file, true);
await uploadFileHandler(file, true, { context: 'full' });
}
}
} else {
const file = item.getAsFile();
if (file) {
await inputFilesHandler([file]);
e.preventDefault();
}
}
}
}
@@ -193,9 +193,13 @@
dir={$settings?.chatDirection ?? 'auto'}
>
{#each message.files as file}
{@const fileUrl =
file.url.startsWith('data') || file.url.startsWith('http')
? file.url
: `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`}
<div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
{#if file.type === 'image'}
<Image src={file.url} imageClassName=" max-h-96 rounded-lg" />
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
<Image src={fileUrl} imageClassName=" max-h-96 rounded-lg" />
{:else}
<FileItem
item={file}
+7 -1
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { formatFileSize } from '$lib/utils';
import { settings } from '$lib/stores';
@@ -60,7 +62,11 @@
} else {
if (url) {
if (type === 'file') {
window.open(`${url}/content`, '_blank').focus();
if (url.startsWith('http')) {
window.open(`${url}/content`, '_blank').focus();
} else {
window.open(`${WEBUI_API_BASE_URL}/files/${url}/content`, '_blank').focus();
}
} else {
window.open(`${url}`, '_blank').focus();
}
@@ -197,7 +197,11 @@
on:click|preventDefault={() => {
if (!isPDF && item.url) {
window.open(
item.type === 'file' ? `${item.url}/content` : `${item.url}`,
item.type === 'file'
? item?.url?.startsWith('http')
? item.url
: `${WEBUI_API_BASE_URL}/files/${item.url}/content`
: item.url,
'_blank'
);
}
+1 -1
View File
@@ -442,7 +442,7 @@ ${content}
fileItem.collection_name =
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
fileItem.url = `${uploadedFile.id}`;
files = files;
} else {
@@ -80,7 +80,7 @@
fileItem.id = uploadedFile.id;
fileItem.collection_name =
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
fileItem.url = `${uploadedFile.id}`;
selectedItems = selectedItems;
} else {