mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-23 09:45:24 +00:00
feat: chat_file table
This commit is contained in:
parent
a3458f492c
commit
f1bf4f20c5
14 changed files with 382 additions and 101 deletions
|
|
@ -1606,6 +1606,7 @@ async def chat_completion(
|
||||||
"user_id": user.id,
|
"user_id": user.id,
|
||||||
"chat_id": form_data.pop("chat_id", None),
|
"chat_id": form_data.pop("chat_id", None),
|
||||||
"message_id": form_data.pop("id", None),
|
"message_id": form_data.pop("id", None),
|
||||||
|
"parent_message": form_data.pop("parent_message", None),
|
||||||
"parent_message_id": form_data.pop("parent_id", None),
|
"parent_message_id": form_data.pop("parent_id", None),
|
||||||
"session_id": form_data.pop("session_id", None),
|
"session_id": form_data.pop("session_id", None),
|
||||||
"filter_ids": form_data.pop("filter_ids", []),
|
"filter_ids": form_data.pop("filter_ids", []),
|
||||||
|
|
@ -1630,15 +1631,38 @@ async def chat_completion(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.get("chat_id") and (user and user.role != "admin"):
|
if metadata.get("chat_id") and user:
|
||||||
if not metadata["chat_id"].startswith("local:"):
|
if not metadata["chat_id"].startswith(
|
||||||
|
"local:"
|
||||||
|
): # temporary chats are not stored
|
||||||
|
|
||||||
|
# Verify chat ownership
|
||||||
chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
|
chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
|
||||||
if chat is None:
|
if chat is None and user.role != "admin": # admins can access any chat
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(),
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Insert chat files from parent message if any
|
||||||
|
parent_message = metadata.get("parent_message", {})
|
||||||
|
parent_message_files = parent_message.get("files", [])
|
||||||
|
if parent_message_files:
|
||||||
|
try:
|
||||||
|
Chats.insert_chat_files(
|
||||||
|
metadata["chat_id"],
|
||||||
|
parent_message.get("id"),
|
||||||
|
[
|
||||||
|
file_item.get("id")
|
||||||
|
for file_item in parent_message_files
|
||||||
|
if file_item.get("type") == "file"
|
||||||
|
],
|
||||||
|
user.id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Error inserting chat files: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
request.state.metadata = metadata
|
request.state.metadata = metadata
|
||||||
form_data["metadata"] = metadata
|
form_data["metadata"] = metadata
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""Add chat_file table
|
||||||
|
|
||||||
|
Revision ID: c440947495f3
|
||||||
|
Revises: 81cc2ce44d79
|
||||||
|
Create Date: 2025-12-21 20:27:41.694897
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "c440947495f3"
|
||||||
|
down_revision: Union[str, None] = "81cc2ce44d79"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"chat_file",
|
||||||
|
sa.Column("id", sa.Text(), primary_key=True),
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"chat_id",
|
||||||
|
sa.Text(),
|
||||||
|
sa.ForeignKey("chat.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"file_id",
|
||||||
|
sa.Text(),
|
||||||
|
sa.ForeignKey("file.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("message_id", sa.Text(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.BigInteger(), nullable=False),
|
||||||
|
# indexes
|
||||||
|
sa.Index("ix_chat_file_chat_id", "chat_id"),
|
||||||
|
sa.Index("ix_chat_file_file_id", "file_id"),
|
||||||
|
sa.Index("ix_chat_file_message_id", "message_id"),
|
||||||
|
sa.Index("ix_chat_file_user_id", "user_id"),
|
||||||
|
# unique constraints
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"chat_id", "file_id", name="uq_chat_file_chat_file"
|
||||||
|
), # prevent duplicate entries
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("chat_file")
|
||||||
|
pass
|
||||||
|
|
@ -10,7 +10,17 @@ from open_webui.models.folders import Folders
|
||||||
from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db
|
from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, Index
|
from sqlalchemy import (
|
||||||
|
BigInteger,
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
ForeignKey,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
JSON,
|
||||||
|
Index,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
from sqlalchemy import or_, func, select, and_, text
|
from sqlalchemy import or_, func, select, and_, text
|
||||||
from sqlalchemy.sql import exists
|
from sqlalchemy.sql import exists
|
||||||
from sqlalchemy.sql.expression import bindparam
|
from sqlalchemy.sql.expression import bindparam
|
||||||
|
|
@ -74,6 +84,38 @@ class ChatModel(BaseModel):
|
||||||
folder_id: Optional[str] = None
|
folder_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ChatFile(Base):
|
||||||
|
__tablename__ = "chat_file"
|
||||||
|
|
||||||
|
id = Column(Text, unique=True, primary_key=True)
|
||||||
|
user_id = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
chat_id = Column(Text, ForeignKey("chat.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
message_id = Column(Text, nullable=True)
|
||||||
|
file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
|
||||||
|
created_at = Column(BigInteger, nullable=False)
|
||||||
|
updated_at = Column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("chat_id", "file_id", name="uq_chat_file_chat_file"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatFileModel(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
|
||||||
|
chat_id: str
|
||||||
|
message_id: Optional[str] = None
|
||||||
|
file_id: str
|
||||||
|
|
||||||
|
created_at: int
|
||||||
|
updated_at: int
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
####################
|
####################
|
||||||
|
|
@ -1219,5 +1261,81 @@ class ChatTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def insert_chat_files(
|
||||||
|
self, chat_id: str, message_id: str, file_ids: list[str], user_id: str
|
||||||
|
) -> Optional[list[ChatFileModel]]:
|
||||||
|
if not file_ids:
|
||||||
|
return None
|
||||||
|
|
||||||
|
chat_message_file_ids = [
|
||||||
|
item.id
|
||||||
|
for item in self.get_chat_files_by_chat_id_and_message_id(
|
||||||
|
chat_id, message_id
|
||||||
|
)
|
||||||
|
]
|
||||||
|
# Remove duplicates and existing file_ids
|
||||||
|
file_ids = list(
|
||||||
|
set(
|
||||||
|
[
|
||||||
|
file_id
|
||||||
|
for file_id in file_ids
|
||||||
|
if file_id and file_id not in chat_message_file_ids
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not file_ids:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
now = int(time.time())
|
||||||
|
|
||||||
|
chat_files = [
|
||||||
|
ChatFileModel(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
user_id=user_id,
|
||||||
|
chat_id=chat_id,
|
||||||
|
message_id=message_id,
|
||||||
|
file_id=file_id,
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
)
|
||||||
|
for file_id in file_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
results = [
|
||||||
|
ChatFile(**chat_file.model_dump()) for chat_file in chat_files
|
||||||
|
]
|
||||||
|
|
||||||
|
db.add_all(results)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return chat_files
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_chat_files_by_chat_id_and_message_id(
|
||||||
|
self, chat_id: str, message_id: str
|
||||||
|
) -> list[ChatFileModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
all_chat_files = (
|
||||||
|
db.query(ChatFile)
|
||||||
|
.filter_by(chat_id=chat_id, message_id=message_id)
|
||||||
|
.order_by(ChatFile.created_at.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
ChatFileModel.model_validate(chat_file) for chat_file in all_chat_files
|
||||||
|
]
|
||||||
|
|
||||||
|
def delete_chat_file(self, chat_id: str, file_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
db.query(ChatFile).filter_by(chat_id=chat_id, file_id=file_id).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Chats = ChatTable()
|
Chats = ChatTable()
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,43 @@ import base64
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
BASE64_IMAGE_URL_PREFIX = re.compile(r"data:image/\w+;base64,", re.IGNORECASE)
|
BASE64_IMAGE_URL_PREFIX = re.compile(r"data:image/\w+;base64,", re.IGNORECASE)
|
||||||
MARKDOWN_IMAGE_URL_PATTERN = re.compile(r"!\[(.*?)\]\((.+?)\)", re.IGNORECASE)
|
MARKDOWN_IMAGE_URL_PATTERN = re.compile(r"!\[(.*?)\]\((.+?)\)", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_base64_from_url(url: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
if url.startswith("http"):
|
||||||
|
# Download the image from the URL
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
image_data = response.content
|
||||||
|
encoded_string = base64.b64encode(image_data).decode("utf-8")
|
||||||
|
content_type = response.headers.get("Content-Type", "image/png")
|
||||||
|
return f"data:{content_type};base64,{encoded_string}"
|
||||||
|
else:
|
||||||
|
file = Files.get_file_by_id(url)
|
||||||
|
|
||||||
|
if not file:
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_path = Storage.get_file(file.path)
|
||||||
|
file_path = Path(file_path)
|
||||||
|
|
||||||
|
if file_path.is_file():
|
||||||
|
with open(file_path, "rb") as image_file:
|
||||||
|
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
||||||
|
content_type, _ = mimetypes.guess_type(file_path.name)
|
||||||
|
return f"data:{content_type};base64,{encoded_string}"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_image_url_from_base64(request, base64_image_string, metadata, user):
|
def get_image_url_from_base64(request, base64_image_string, metadata, user):
|
||||||
if BASE64_IMAGE_URL_PREFIX.match(base64_image_string):
|
if BASE64_IMAGE_URL_PREFIX.match(base64_image_string):
|
||||||
image_url = ""
|
image_url = ""
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ from open_webui.utils.webhook import post_webhook
|
||||||
from open_webui.utils.files import (
|
from open_webui.utils.files import (
|
||||||
convert_markdown_base64_images,
|
convert_markdown_base64_images,
|
||||||
get_file_url_from_base64,
|
get_file_url_from_base64,
|
||||||
|
get_image_base64_from_url,
|
||||||
get_image_url_from_base64,
|
get_image_url_from_base64,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1108,6 +1109,45 @@ def apply_params_to_form_data(form_data, model):
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
|
async def convert_url_images_to_base64(form_data):
|
||||||
|
messages = form_data.get("messages", [])
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
content = message.get("content")
|
||||||
|
if not isinstance(content, list):
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_content = []
|
||||||
|
|
||||||
|
for item in content:
|
||||||
|
if not isinstance(item, dict) or item.get("type") != "image_url":
|
||||||
|
new_content.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
image_url = item.get("image_url", {}).get("url", "")
|
||||||
|
if image_url.startswith("data:image/"):
|
||||||
|
new_content.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
base64_data = await asyncio.to_thread(
|
||||||
|
get_image_base64_from_url, image_url
|
||||||
|
)
|
||||||
|
new_content.append(
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": base64_data},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Error converting image URL to base64: {e}")
|
||||||
|
new_content.append(item)
|
||||||
|
|
||||||
|
message["content"] = new_content
|
||||||
|
|
||||||
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
async def process_chat_payload(request, form_data, user, metadata, model):
|
async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
# Pipeline Inlet -> Filter Inlet -> Chat Memory -> Chat Web Search -> Chat Image Generation
|
# Pipeline Inlet -> Filter Inlet -> Chat Memory -> Chat Web Search -> Chat Image Generation
|
||||||
# -> Chat Code Interpreter (Form Data Update) -> (Default) Chat Tools Function Calling
|
# -> Chat Code Interpreter (Form Data Update) -> (Default) Chat Tools Function Calling
|
||||||
|
|
@ -1125,6 +1165,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
form_data = await convert_url_images_to_base64(form_data)
|
||||||
|
|
||||||
event_emitter = get_event_emitter(metadata)
|
event_emitter = get_event_emitter(metadata)
|
||||||
event_caller = get_event_call(metadata)
|
event_caller = get_event_call(metadata)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,6 @@
|
||||||
// Check for known image types
|
// Check for known image types
|
||||||
for (const type of item.types) {
|
for (const type of item.types) {
|
||||||
if (type.startsWith('image/')) {
|
if (type.startsWith('image/')) {
|
||||||
// get as file
|
|
||||||
const blob = await item.getType(type);
|
const blob = await item.getType(type);
|
||||||
const file = new File([blob], `clipboard-image.${type.split('/')[1]}`, {
|
const file = new File([blob], `clipboard-image.${type.split('/')[1]}`, {
|
||||||
type: type
|
type: type
|
||||||
|
|
@ -486,7 +485,7 @@
|
||||||
fileItem.collection_name =
|
fileItem.collection_name =
|
||||||
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
||||||
fileItem.content_type = uploadedFile.meta?.content_type || uploadedFile.content_type;
|
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;
|
files = files;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -802,10 +801,14 @@
|
||||||
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
|
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
|
||||||
{#each files as file, fileIdx}
|
{#each files as file, fileIdx}
|
||||||
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('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 group">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Image
|
<Image
|
||||||
src={`${file.url}${file?.content_type ? '/content' : ''}`}
|
src={fileUrl}
|
||||||
alt=""
|
alt=""
|
||||||
imageClassName=" size-10 rounded-xl object-cover"
|
imageClassName=" size-10 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
@ -928,8 +931,7 @@
|
||||||
for (const item of clipboardData.items) {
|
for (const item of clipboardData.items) {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
const _files = [file];
|
await inputFilesHandler([file]);
|
||||||
await inputFilesHandler(_files);
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -343,19 +343,15 @@
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
>
|
>
|
||||||
{#each message?.data?.files as file}
|
{#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>
|
<div>
|
||||||
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
|
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
|
||||||
<Image
|
<Image src={fileUrl} alt={file.name} imageClassName=" max-h-96 rounded-lg" />
|
||||||
src={`${file.url}${file?.content_type ? '/content' : ''}`}
|
|
||||||
alt={file.name}
|
|
||||||
imageClassName=" max-h-96 rounded-lg"
|
|
||||||
/>
|
|
||||||
{:else if file.type === 'video' || (file?.content_type ?? '').startsWith('video/')}
|
{:else if file.type === 'video' || (file?.content_type ?? '').startsWith('video/')}
|
||||||
<video
|
<video src={fileUrl} controls class=" max-h-96 rounded-lg"></video>
|
||||||
src={`${file.url}${file?.content_type ? '/content' : ''}`}
|
|
||||||
controls
|
|
||||||
class=" max-h-96 rounded-lg"
|
|
||||||
></video>
|
|
||||||
{:else}
|
{:else}
|
||||||
<FileItem
|
<FileItem
|
||||||
item={file}
|
item={file}
|
||||||
|
|
|
||||||
|
|
@ -759,7 +759,7 @@
|
||||||
fileItem.id = uploadedFile.id;
|
fileItem.id = uploadedFile.id;
|
||||||
fileItem.size = file.size;
|
fileItem.size = file.size;
|
||||||
fileItem.collection_name = uploadedFile?.meta?.collection_name;
|
fileItem.collection_name = uploadedFile?.meta?.collection_name;
|
||||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
fileItem.url = `${uploadedFile.id}`;
|
||||||
|
|
||||||
files = files;
|
files = files;
|
||||||
toast.success($i18n.t('File uploaded successfully'));
|
toast.success($i18n.t('File uploaded successfully'));
|
||||||
|
|
@ -1601,8 +1601,10 @@
|
||||||
const _files = JSON.parse(JSON.stringify(files));
|
const _files = JSON.parse(JSON.stringify(files));
|
||||||
|
|
||||||
chatFiles.push(
|
chatFiles.push(
|
||||||
..._files.filter((item) =>
|
..._files.filter(
|
||||||
['doc', 'text', 'file', 'note', 'chat', 'folder', 'collection'].includes(item.type)
|
(item) =>
|
||||||
|
['doc', 'text', 'note', 'chat', 'folder', 'collection'].includes(item.type) ||
|
||||||
|
(item.type === 'file' && !item?.content_type?.startsWith('image/'))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
chatFiles = chatFiles.filter(
|
chatFiles = chatFiles.filter(
|
||||||
|
|
@ -1730,7 +1732,9 @@
|
||||||
if (model) {
|
if (model) {
|
||||||
// If there are image files, check if model is vision capable
|
// If there are image files, check if model is vision capable
|
||||||
const hasImages = createMessagesList(_history, parentId).some((message) =>
|
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)) {
|
if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) {
|
||||||
|
|
@ -1824,8 +1828,10 @@
|
||||||
|
|
||||||
let files = JSON.parse(JSON.stringify(chatFiles));
|
let files = JSON.parse(JSON.stringify(chatFiles));
|
||||||
files.push(
|
files.push(
|
||||||
...(userMessage?.files ?? []).filter((item) =>
|
...(userMessage?.files ?? []).filter(
|
||||||
['doc', 'text', 'file', 'note', 'chat', 'collection'].includes(item.type)
|
(item) =>
|
||||||
|
['doc', 'text', 'note', 'chat', 'collection'].includes(item.type) ||
|
||||||
|
(item.type === 'file' && !item?.content_type.startsWith('image/'))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
|
|
@ -1872,30 +1878,33 @@
|
||||||
].filter((message) => message);
|
].filter((message) => message);
|
||||||
|
|
||||||
messages = messages
|
messages = messages
|
||||||
.map((message, idx, arr) => ({
|
.map((message, idx, arr) => {
|
||||||
role: message.role,
|
const imageFiles = (message?.files ?? []).filter(
|
||||||
...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
|
(file) => file.type === 'image' || (file?.content_type ?? '').startsWith('image/')
|
||||||
message.role === 'user'
|
);
|
||||||
? {
|
|
||||||
content: [
|
return {
|
||||||
{
|
role: message.role,
|
||||||
type: 'text',
|
...(message.role === 'user' && imageFiles.length > 0
|
||||||
text: message?.merged?.content ?? message.content
|
? {
|
||||||
},
|
content: [
|
||||||
...message.files
|
{
|
||||||
.filter((file) => file.type === 'image')
|
type: 'text',
|
||||||
.map((file) => ({
|
text: message?.merged?.content ?? message.content
|
||||||
|
},
|
||||||
|
...imageFiles.map((file) => ({
|
||||||
type: 'image_url',
|
type: 'image_url',
|
||||||
image_url: {
|
image_url: {
|
||||||
url: file.url
|
url: file.url
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
content: message?.merged?.content ?? message.content
|
content: message?.merged?.content ?? message.content
|
||||||
})
|
})
|
||||||
}))
|
};
|
||||||
|
})
|
||||||
.filter((message) => message?.role === 'user' || message?.content?.trim());
|
.filter((message) => message?.role === 'user' || message?.content?.trim());
|
||||||
|
|
||||||
const toolIds = [];
|
const toolIds = [];
|
||||||
|
|
@ -1950,6 +1959,7 @@
|
||||||
|
|
||||||
id: responseMessageId,
|
id: responseMessageId,
|
||||||
parent_id: userMessage?.id ?? null,
|
parent_id: userMessage?.id ?? null,
|
||||||
|
parent_message: userMessage,
|
||||||
|
|
||||||
background_tasks: {
|
background_tasks: {
|
||||||
...(!$temporaryChatEnabled &&
|
...(!$temporaryChatEnabled &&
|
||||||
|
|
|
||||||
|
|
@ -191,17 +191,11 @@
|
||||||
for (const type of item.types) {
|
for (const type of item.types) {
|
||||||
if (type.startsWith('image/')) {
|
if (type.startsWith('image/')) {
|
||||||
const blob = await item.getType(type);
|
const blob = await item.getType(type);
|
||||||
const reader = new FileReader();
|
const file = new File([blob], `clipboard-image.${type.split('/')[1]}`, {
|
||||||
reader.onload = (event) => {
|
type: type
|
||||||
files = [
|
});
|
||||||
...files,
|
|
||||||
{
|
inputFilesHandler([file]);
|
||||||
type: 'image',
|
|
||||||
url: event.target.result as string
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -527,8 +521,9 @@
|
||||||
|
|
||||||
// Convert the canvas to a Base64 image URL
|
// Convert the canvas to a Base64 image URL
|
||||||
const imageUrl = canvas.toDataURL('image/png');
|
const imageUrl = canvas.toDataURL('image/png');
|
||||||
// Add the captured image to the files array to render it
|
const blob = await (await fetch(imageUrl)).blob();
|
||||||
files = [...files, { type: 'image', url: imageUrl }];
|
const file = new File([blob], `screen-capture-${Date.now()}.png`, { type: 'image/png' });
|
||||||
|
inputFilesHandler([file]);
|
||||||
// Clean memory: Clear video srcObject
|
// Clean memory: Clear video srcObject
|
||||||
video.srcObject = null;
|
video.srcObject = null;
|
||||||
} catch (error) {
|
} 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)) {
|
if ($_user?.role !== 'admin' && !($_user?.permissions?.chat?.file_upload ?? true)) {
|
||||||
toast.error($i18n.t('You do not have permission to upload files.'));
|
toast.error($i18n.t('You do not have permission to upload files.'));
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -560,7 +555,7 @@
|
||||||
size: file.size,
|
size: file.size,
|
||||||
error: '',
|
error: '',
|
||||||
itemId: tempItemId,
|
itemId: tempItemId,
|
||||||
...(fullContext ? { context: 'full' } : {})
|
...itemData
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fileItem.size == 0) {
|
if (fileItem.size == 0) {
|
||||||
|
|
@ -584,7 +579,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// During the file upload, file content is automatically extracted.
|
// 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) {
|
if (uploadedFile) {
|
||||||
console.log('File upload completed:', {
|
console.log('File upload completed:', {
|
||||||
|
|
@ -603,7 +598,8 @@
|
||||||
fileItem.id = uploadedFile.id;
|
fileItem.id = uploadedFile.id;
|
||||||
fileItem.collection_name =
|
fileItem.collection_name =
|
||||||
uploadedFile?.meta?.collection_name || uploadedFile?.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;
|
files = files;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -726,19 +722,21 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
let imageUrl = event.target.result;
|
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 = [
|
const blob = await (await fetch(imageUrl)).blob();
|
||||||
...files,
|
const compressedFile = new File([blob], file.name, { type: file.type });
|
||||||
{
|
|
||||||
type: 'image',
|
uploadFileHandler(compressedFile, false);
|
||||||
url: `${imageUrl}`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(file['type'] === 'image/heic' ? await convertHeicToJpeg(file) : file);
|
reader.readAsDataURL(file['type'] === 'image/heic' ? await convertHeicToJpeg(file) : file);
|
||||||
} else {
|
} else {
|
||||||
uploadFileHandler(file);
|
uploadFileHandler(file);
|
||||||
|
|
@ -1146,11 +1144,15 @@
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
>
|
>
|
||||||
{#each files as file, fileIdx}
|
{#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 group">
|
||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src={file.url}
|
src={fileUrl}
|
||||||
alt=""
|
alt=""
|
||||||
imageClassName=" size-10 rounded-xl object-cover"
|
imageClassName=" size-10 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1392,29 +1394,7 @@
|
||||||
|
|
||||||
if (clipboardData && clipboardData.items) {
|
if (clipboardData && clipboardData.items) {
|
||||||
for (const item of clipboardData.items) {
|
for (const item of clipboardData.items) {
|
||||||
if (item.type.indexOf('image') !== -1) {
|
if (item.type === 'text/plain') {
|
||||||
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 (($settings?.largeTextAsFile ?? false) && !shiftKey) {
|
if (($settings?.largeTextAsFile ?? false) && !shiftKey) {
|
||||||
const text = clipboardData.getData('text/plain');
|
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'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
>
|
>
|
||||||
{#each message.files as file}
|
{#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' : ''}>
|
<div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
|
||||||
{#if file.type === 'image'}
|
{#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
|
||||||
<Image src={file.url} imageClassName=" max-h-96 rounded-lg" />
|
<Image src={fileUrl} imageClassName=" max-h-96 rounded-lg" />
|
||||||
{:else}
|
{:else}
|
||||||
<FileItem
|
<FileItem
|
||||||
item={file}
|
item={file}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, getContext } from 'svelte';
|
import { createEventDispatcher, getContext } from 'svelte';
|
||||||
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import { formatFileSize } from '$lib/utils';
|
import { formatFileSize } from '$lib/utils';
|
||||||
import { settings } from '$lib/stores';
|
import { settings } from '$lib/stores';
|
||||||
|
|
||||||
|
|
@ -60,7 +62,11 @@
|
||||||
} else {
|
} else {
|
||||||
if (url) {
|
if (url) {
|
||||||
if (type === 'file') {
|
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 {
|
} else {
|
||||||
window.open(`${url}`, '_blank').focus();
|
window.open(`${url}`, '_blank').focus();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,11 @@
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
if (!isPDF && item.url) {
|
if (!isPDF && item.url) {
|
||||||
window.open(
|
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'
|
'_blank'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ ${content}
|
||||||
fileItem.collection_name =
|
fileItem.collection_name =
|
||||||
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
||||||
|
|
||||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
fileItem.url = `${uploadedFile.id}`;
|
||||||
|
|
||||||
files = files;
|
files = files;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
fileItem.id = uploadedFile.id;
|
fileItem.id = uploadedFile.id;
|
||||||
fileItem.collection_name =
|
fileItem.collection_name =
|
||||||
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
||||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
fileItem.url = `${uploadedFile.id}`;
|
||||||
|
|
||||||
selectedItems = selectedItems;
|
selectedItems = selectedItems;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue