This commit is contained in:
Andrew Baek 2025-09-16 01:07:15 +09:00
commit b0720ec2aa
153 changed files with 4449 additions and 2793 deletions

View file

@ -236,7 +236,7 @@ class ChatTable:
return chat.chat.get("title", "New Chat")
def get_messages_by_chat_id(self, id: str) -> Optional[dict]:
def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]:
chat = self.get_chat_by_id(id)
if chat is None:
return None

View file

@ -37,6 +37,7 @@ class Function(Base):
class FunctionMeta(BaseModel):
description: Optional[str] = None
manifest: Optional[dict] = {}
model_config = ConfigDict(extra="allow")
class FunctionModel(BaseModel):
@ -260,6 +261,29 @@ class FunctionsTable:
except Exception:
return None
def update_function_metadata_by_id(
self, id: str, metadata: dict
) -> Optional[FunctionModel]:
with get_db() as db:
try:
function = db.get(Function, id)
if function:
if function.meta:
function.meta = {**function.meta, **metadata}
else:
function.meta = metadata
function.updated_at = int(time.time())
db.commit()
db.refresh(function)
return self.get_function_by_id(id)
else:
return None
except Exception as e:
log.exception(f"Error updating function metadata by id {id}: {e}")
return None
def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[dict]:

View file

@ -97,15 +97,26 @@ class NoteTable:
db.commit()
return note
def get_notes(self) -> list[NoteModel]:
def get_notes(
self, skip: Optional[int] = None, limit: Optional[int] = None
) -> list[NoteModel]:
with get_db() as db:
notes = db.query(Note).order_by(Note.updated_at.desc()).all()
query = db.query(Note).order_by(Note.updated_at.desc())
if skip is not None:
query = query.offset(skip)
if limit is not None:
query = query.limit(limit)
notes = query.all()
return [NoteModel.model_validate(note) for note in notes]
def get_notes_by_user_id(
self, user_id: str, permission: str = "write"
self,
user_id: str,
permission: str = "write",
skip: Optional[int] = None,
limit: Optional[int] = None,
) -> list[NoteModel]:
notes = self.get_notes()
notes = self.get_notes(skip=skip, limit=limit)
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
return [
note

View file

@ -19,10 +19,13 @@ from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.models.users import UserModel
from open_webui.models.files import Files
from open_webui.models.knowledge import Knowledges
from open_webui.models.chats import Chats
from open_webui.models.notes import Notes
from open_webui.retrieval.vector.main import GetResult
from open_webui.utils.access_control import has_access
from open_webui.utils.misc import get_message_list
from open_webui.env import (
@ -491,25 +494,37 @@ def get_sources_from_items(
# Raw Text
# Used during temporary chat file uploads or web page & youtube attachements
if item.get("collection_name"):
# If item has a collection name, use it
collection_names.append(item.get("collection_name"))
elif item.get("file"):
# if item has file data, use it
query_result = {
"documents": [
[item.get("file", {}).get("data", {}).get("content")]
],
"metadatas": [[item.get("file", {}).get("meta", {})]],
}
else:
# Fallback to item content
query_result = {
"documents": [[item.get("content")]],
"metadatas": [
[{"file_id": item.get("id"), "name": item.get("name")}]
],
}
if item.get("context") == "full":
if item.get("file"):
# if item has file data, use it
query_result = {
"documents": [
[item.get("file", {}).get("data", {}).get("content")]
],
"metadatas": [[item.get("file", {}).get("meta", {})]],
}
if query_result is None:
# Fallback
if item.get("collection_name"):
# If item has a collection name, use it
collection_names.append(item.get("collection_name"))
elif item.get("file"):
# If item has file data, use it
query_result = {
"documents": [
[item.get("file", {}).get("data", {}).get("content")]
],
"metadatas": [[item.get("file", {}).get("meta", {})]],
}
else:
# Fallback to item content
query_result = {
"documents": [[item.get("content")]],
"metadatas": [
[{"file_id": item.get("id"), "name": item.get("name")}]
],
}
elif item.get("type") == "note":
# Note Attached
@ -526,6 +541,30 @@ def get_sources_from_items(
"metadatas": [[{"file_id": note.id, "name": note.title}]],
}
elif item.get("type") == "chat":
# Chat Attached
chat = Chats.get_chat_by_id(item.get("id"))
if chat and (user.role == "admin" or chat.user_id == user.id):
messages_map = chat.chat.get("history", {}).get("messages", {})
message_id = chat.chat.get("history", {}).get("currentId")
if messages_map and message_id:
# Reconstruct the message list in order
message_list = get_message_list(messages_map, message_id)
message_history = "\n".join(
[
f"#### {m.get('role', 'user').capitalize()}\n{m.get('content')}\n"
for m in message_list
]
)
# User has access to the chat
query_result = {
"documents": [[message_history]],
"metadatas": [[{"file_id": chat.id, "name": chat.title}]],
}
elif item.get("type") == "file":
if (
item.get("context") == "full"

View file

@ -192,6 +192,9 @@ async def create_new_function(
function_cache_dir = CACHE_DIR / "functions" / form_data.id
function_cache_dir.mkdir(parents=True, exist_ok=True)
if function_type == "filter" and getattr(function_module, "toggle", None):
Functions.update_function_metadata_by_id(id, {"toggle": True})
if function:
return function
else:
@ -308,6 +311,9 @@ async def update_function_by_id(
function = Functions.update_function_by_id(id, updated)
if function_type == "filter" and getattr(function_module, "toggle", None):
Functions.update_function_metadata_by_id(id, {"toggle": True})
if function:
return function
else:

View file

@ -62,8 +62,9 @@ class NoteTitleIdResponse(BaseModel):
@router.get("/list", response_model=list[NoteTitleIdResponse])
async def get_note_list(request: Request, user=Depends(get_verified_user)):
async def get_note_list(
request: Request, page: Optional[int] = None, user=Depends(get_verified_user)
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
):
@ -72,9 +73,15 @@ async def get_note_list(request: Request, user=Depends(get_verified_user)):
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
limit = None
skip = None
if page is not None:
limit = 60
skip = (page - 1) * limit
notes = [
NoteTitleIdResponse(**note.model_dump())
for note in Notes.get_notes_by_user_id(user.id, "write")
for note in Notes.get_notes_by_user_id(user.id, "write", skip=skip, limit=limit)
]
return notes

View file

@ -127,8 +127,10 @@ async def process_filter_functions(
raise e
# Handle file cleanup for inlet
if skip_files and "files" in form_data.get("metadata", {}):
del form_data["files"]
del form_data["metadata"]["files"]
if skip_files:
if "files" in form_data.get("metadata", {}):
del form_data["metadata"]["files"]
if "files" in form_data:
del form_data["files"]
return form_data, {}

View file

@ -1131,11 +1131,11 @@ async def process_chat_response(
request, response, form_data, user, metadata, model, events, tasks
):
async def background_tasks_handler():
message_map = Chats.get_messages_by_chat_id(metadata["chat_id"])
message = message_map.get(metadata["message_id"]) if message_map else None
messages_map = Chats.get_messages_map_by_chat_id(metadata["chat_id"])
message = messages_map.get(metadata["message_id"]) if messages_map else None
if message:
message_list = get_message_list(message_map, metadata["message_id"])
message_list = get_message_list(messages_map, metadata["message_id"])
# Remove details tags and files from the messages.
# as get_message_list creates a new list, it does not affect

View file

@ -26,7 +26,7 @@ def deep_update(d, u):
return d
def get_message_list(messages, message_id):
def get_message_list(messages_map, message_id):
"""
Reconstructs a list of messages in order up to the specified message_id.
@ -36,11 +36,11 @@ def get_message_list(messages, message_id):
"""
# Handle case where messages is None
if not messages:
if not messages_map:
return [] # Return empty list instead of None to prevent iteration errors
# Find the message by its id
current_message = messages.get(message_id)
current_message = messages_map.get(message_id)
if not current_message:
return [] # Return empty list instead of None to prevent iteration errors
@ -53,7 +53,7 @@ def get_message_list(messages, message_id):
0, current_message
) # Insert the message at the beginning of the list
parent_id = current_message.get("parentId") # Use .get() for safety
current_message = messages.get(parent_id) if parent_id else None
current_message = messages_map.get(parent_id) if parent_id else None
return message_list

37
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "0.6.28",
"version": "0.6.29",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.6.28",
"version": "0.6.29",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
@ -37,6 +37,7 @@
"@tiptap/extensions": "^3.0.7",
"@tiptap/pm": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.4.2",
"@xyflow/svelte": "^0.1.19",
"async": "^3.2.5",
"bits-ui": "^0.21.15",
@ -86,7 +87,6 @@
"socket.io-client": "^4.2.0",
"sortablejs": "^1.15.6",
"svelte-sonner": "^0.3.19",
"svelte-tiptap": "^3.0.0",
"tippy.js": "^6.3.7",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
@ -3856,18 +3856,17 @@
}
},
"node_modules/@tiptap/suggestion": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.0.9.tgz",
"integrity": "sha512-irthqfUybezo3IwR6AXvyyTOtkzwfvvst58VXZtTnR1nN6NEcrs3TQoY3bGKGbN83bdiquKh6aU2nLnZfAhoXg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.4.2.tgz",
"integrity": "sha512-sljtfiDtdAsbPOwrXrFGf64D6sXUjeU3Iz5v3TvN7TVJKozkZ/gaMkPRl+WC1CGwC6BnzQVDBEEa1e+aApV0mA==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.0.9",
"@tiptap/pm": "^3.0.9"
"@tiptap/core": "^3.4.2",
"@tiptap/pm": "^3.4.2"
}
},
"node_modules/@tiptap/y-tiptap": {
@ -12503,26 +12502,6 @@
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
}
},
"node_modules/svelte-tiptap": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/svelte-tiptap/-/svelte-tiptap-3.0.0.tgz",
"integrity": "sha512-digFHOJe16RX0HIU+u8hOaCS9sIgktTpYHSF9yJ6dgxPv/JWJdYCdwoX65lcHitFhhCG7xnolJng6PJa9M9h3w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"license": "MIT",
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
"@tiptap/core": "^3.0.0",
"@tiptap/extension-bubble-menu": "^3.0.0",
"@tiptap/extension-floating-menu": "^3.0.0",
"@tiptap/pm": "^3.0.0",
"svelte": "^5.0.0"
}
},
"node_modules/svelte/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.6.28",
"version": "0.6.29",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@ -81,6 +81,7 @@
"@tiptap/extensions": "^3.0.7",
"@tiptap/pm": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.4.2",
"@xyflow/svelte": "^0.1.19",
"async": "^3.2.5",
"bits-ui": "^0.21.15",
@ -130,7 +131,6 @@
"socket.io-client": "^4.2.0",
"sortablejs": "^1.15.6",
"svelte-sonner": "^0.3.19",
"svelte-tiptap": "^3.0.0",
"tippy.js": "^6.3.7",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",

View file

@ -56,18 +56,11 @@ dependencies = [
"fake-useragent==2.2.0",
"chromadb==1.0.20",
"pymilvus==2.5.0",
"qdrant-client==1.14.3",
"opensearch-py==2.8.0",
"playwright==1.49.1",
"elasticsearch==9.1.0",
"pinecone==6.0.2",
"oracledb==3.2.0",
"transformers",
"sentence-transformers==4.1.0",
"accelerate",
"colbert-ai==0.2.21",
"pyarrow==20.0.0",
"einops==0.8.1",
@ -154,6 +147,15 @@ all = [
"docker~=7.1.0",
"pytest~=8.3.2",
"pytest-docker~=3.1.1",
"playwright==1.49.1",
"elasticsearch==9.1.0",
"qdrant-client==1.14.3",
"pymilvus==2.5.0",
"pinecone==6.0.2",
"oracledb==3.2.0",
"colbert-ai==0.2.21",
]
[project.scripts]

View file

@ -409,17 +409,33 @@ input[type='number'] {
}
}
.tiptap .mention {
.mention {
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
@apply text-blue-900 dark:text-blue-100 bg-blue-300/20 dark:bg-blue-500/20;
}
.tiptap .mention::after {
.mention::after {
content: '\200B';
}
.tiptap .suggestion {
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
@apply bg-purple-100/20 text-purple-900 dark:bg-purple-500/20 dark:text-purple-100;
}
.tiptap .suggestion::after {
content: '\200B';
}
.tiptap .suggestion.is-empty::after {
content: '\00A0';
border-bottom: 1px dotted rgba(31, 41, 55, 0.12);
}
.input-prose .tiptap ul[data-type='taskList'] {
list-style: none;
margin-left: 0;

View file

@ -23,8 +23,6 @@
href="/static/apple-touch-icon.png"
crossorigin="use-credentials"
/>
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
<link
rel="manifest"
href="/manifest.json"
@ -37,14 +35,7 @@
/>
<meta name="theme-color" content="#171717" />
<meta name="robots" content="noindex,nofollow" />
<meta name="description" content="Open WebUI" />
<link
rel="search"
type="application/opensearchdescription+xml"
title="Open WebUI"
href="/opensearch.xml"
crossorigin="use-credentials"
/>
<script src="/static/loader.js" defer crossorigin="use-credentials"></script>
<link rel="stylesheet" href="/static/custom.css" crossorigin="use-credentials" />

View file

@ -91,10 +91,15 @@ export const getNotes = async (token: string = '', raw: boolean = false) => {
return grouped;
};
export const getNoteList = async (token: string = '') => {
export const getNoteList = async (token: string = '', page: number | null = null) => {
let error = null;
const searchParams = new URLSearchParams();
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list`, {
if (page !== null) {
searchParams.append('page', `${page}`);
}
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',

View file

@ -7,8 +7,7 @@
</script>
<div class="px-3">
<div class="text-center text-6xl mb-3">📄</div>
<div class="text-center dark:text-white text-xl font-semibold z-50">
<div class="text-center dark:text-white text-2xl font-medium z-50">
{#if title}
{title}
{:else}
@ -17,7 +16,7 @@
</div>
<slot
><div class="px-2 mt-2 text-center text-sm dark:text-gray-200 w-full">
><div class="px-2 mt-2 text-center text-gray-700 dark:text-gray-200 w-full">
{#if content}
{content}
{:else}

View file

@ -42,7 +42,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"
@ -50,7 +50,7 @@
>
{#if ['filter', 'action'].includes(func.type)}
<div
class="flex gap-2 justify-between items-center px-3 py-2 text-sm font-medium cursor-pointerrounded-md"
class="flex gap-2 justify-between items-center px-3 py-1.5 text-sm font-medium cursor-pointerrounded-md"
>
<div class="flex gap-2 items-center">
<GlobeAlt />
@ -67,7 +67,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
editHandler();
}}
@ -91,7 +91,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
@ -101,7 +101,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
cloneHandler();
}}
@ -112,7 +112,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
exportHandler();
}}
@ -125,7 +125,7 @@
<hr class="border-gray-100 dark:border-gray-850 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
deleteHandler();
}}

View file

@ -233,14 +233,4 @@
{/if}
</div>
</div>
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div> -->
</form>

View file

@ -45,14 +45,14 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
hideHandler();
}}
@ -104,7 +104,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
copyLinkHandler();
}}
@ -115,7 +115,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
exportHandler();
}}

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,8 @@
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
import Camera from '$lib/components/icons/Camera.svelte';
import Clip from '$lib/components/icons/Clip.svelte';
const i18n = getContext('i18n');
@ -44,34 +46,34 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={15}
alignOffset={-8}
side="top"
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
sideOffset={4}
alignOffset={-6}
side="bottom"
align="start"
transition={flyAndScale}
>
{#if !$mobile}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
screenCaptureHandler();
}}
>
<CameraSolid />
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadFilesHandler();
}}
>
<DocumentArrowUpSolid />
<Clip />
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
</DropdownMenu.Item>
{#if !$mobile}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
screenCaptureHandler();
}}
>
<Camera />
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</div>
</Dropdown>

View file

@ -0,0 +1,85 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import { models } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
export let query = '';
export let command: (payload: { id: string; label: string }) => void;
export let selectedIndex = 0;
let items = [];
$: filteredItems = $models.filter((u) => u.name.toLowerCase().includes(query.toLowerCase()));
const select = (index: number) => {
const item = filteredItems[index];
// Add the "A:" prefix to the id to indicate it's an agent/assistant/ai model
if (item) command({ id: `A:${item.id}|${item.name}`, label: item.name });
};
const onKeyDown = (event: KeyboardEvent) => {
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
if (event.key === 'ArrowUp') {
selectedIndex = (selectedIndex + filteredItems.length - 1) % filteredItems.length;
const item = document.querySelector(`[data-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
return true;
}
if (event.key === 'ArrowDown') {
selectedIndex = (selectedIndex + 1) % filteredItems.length;
const item = document.querySelector(`[data-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
return true;
}
if (event.key === 'Enter' || event.key === 'Tab') {
select(selectedIndex);
return true;
}
if (event.key === 'Escape') {
// tell tiptap we handled it (it will close)
return true;
}
return false;
};
// This method will be called from the suggestion renderer
// @ts-ignore
export function _onKeyDown(event: KeyboardEvent) {
return onKeyDown(event);
}
</script>
{#if filteredItems.length}
<div
class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-60 p-1"
id="suggestions-container"
>
<div class="overflow-y-auto scrollbar-thin max-h-60">
<div class="px-2 text-xs text-gray-500 py-1">
{$i18n.t('Models')}
</div>
{#each filteredItems as item, i}
<Tooltip content={item?.id} placement="top-start">
<button
type="button"
on:click={() => select(i)}
on:mousemove={() => {
selectedIndex = i;
}}
class="px-2.5 py-1.5 rounded-xl w-full text-left {i === selectedIndex
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
: ''}"
data-selected={i === selectedIndex}
>
<div class="truncate">
@{item.name}
</div>
</button>
</Tooltip>
{/each}
</div>
</div>
{/if}

View file

@ -138,9 +138,7 @@
id="message-{message.id}"
dir={$settings.chatDirection}
>
<div
class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
>
<div class={`shrink-0 mr-3 w-9`}>
{#if showUserProfile}
<ProfilePreview user={message.user}>
<ProfileImage
@ -178,7 +176,12 @@
class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
>
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
<span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span>
<span class="line-clamp-1">
{$i18n.t(formatDate(message.created_at / 1000000), {
LOCALIZED_TIME: dayjs(message.created_at / 1000000).format('LT'),
LOCALIZED_DATE: dayjs(message.created_at / 1000000).format('L')
})}
</span>
</Tooltip>
</div>
{/if}
@ -198,7 +201,7 @@
name={file.name}
type={file.type}
size={file?.size}
colorClassName="bg-white dark:bg-gray-850 "
small={true}
/>
{/if}
</div>
@ -228,7 +231,7 @@
<div class="flex space-x-1.5">
<button
id="close-edit-message-button"
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
class="px-3.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
on:click={() => {
edit = false;
editedContent = null;
@ -239,7 +242,7 @@
<button
id="confirm-edit-message-button"
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
class="px-3.5 py-1.5 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
on:click={async () => {
onEdit(editedContent);
edit = false;

View file

@ -37,6 +37,7 @@
showArtifacts,
tools,
toolServers,
functions,
selectedFolder,
pinnedChats
} from '$lib/stores';
@ -88,6 +89,7 @@
import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte';
import Sidebar from '../icons/Sidebar.svelte';
import { getFunctions } from '$lib/apis/functions';
export let chatIdProp = '';
@ -236,33 +238,58 @@
};
const resetInput = () => {
console.debug('resetInput');
setToolIds();
selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false;
imageGenerationEnabled = false;
codeInterpreterEnabled = false;
setDefaults();
};
const setToolIds = async () => {
const setDefaults = async () => {
if (!$tools) {
tools.set(await getTools(localStorage.token));
}
if (!$functions) {
functions.set(await getFunctions(localStorage.token));
}
if (selectedModels.length !== 1 && !atSelectedModel) {
return;
}
const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]);
if (model && model?.info?.meta?.toolIds) {
selectedToolIds = [
...new Set(
[...(model?.info?.meta?.toolIds ?? [])].filter((id) => $tools.find((t) => t.id === id))
)
];
} else {
selectedToolIds = [];
if (model) {
// Set Default Tools
if (model?.info?.meta?.toolIds) {
selectedToolIds = [
...new Set(
[...(model?.info?.meta?.toolIds ?? [])].filter((id) => $tools.find((t) => t.id === id))
)
];
}
// Set Default Filters (Toggleable only)
if (model?.info?.meta?.defaultFilterIds) {
selectedFilterIds = model.info.meta.defaultFilterIds.filter((id) =>
model?.filters?.find((f) => f.id === id)
);
}
// Set Default Features
if (model?.info?.meta?.defaultFeatureIds) {
if (model.info?.meta?.capabilities?.['image_generation']) {
imageGenerationEnabled = model.info.meta.defaultFeatureIds.includes('image_generation');
}
if (model.info?.meta?.capabilities?.['web_search']) {
webSearchEnabled = model.info.meta.defaultFeatureIds.includes('web_search');
}
if (model.info?.meta?.capabilities?.['code_interpreter']) {
codeInterpreterEnabled = model.info.meta.defaultFeatureIds.includes('code_interpreter');
}
}
}
};
@ -1464,19 +1491,11 @@
prompt = '';
const messages = createMessagesList(history, history.currentId);
// Reset chat input textarea
if (!($settings?.richTextInput ?? true)) {
const chatInputElement = document.getElementById('chat-input');
if (chatInputElement) {
await tick();
chatInputElement.style.height = '';
}
}
const _files = JSON.parse(JSON.stringify(files));
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
chatFiles.push(
..._files.filter((item) => ['doc', 'text', 'file', 'collection'].includes(item.type))
);
chatFiles = chatFiles.filter(
// Remove duplicates
(item, index, array) =>
@ -1696,7 +1715,7 @@
let files = JSON.parse(JSON.stringify(chatFiles));
files.push(
...(userMessage?.files ?? []).filter((item) =>
['doc', 'text', 'file', 'note', 'collection'].includes(item.type)
['doc', 'text', 'file', 'note', 'chat', 'collection'].includes(item.type)
)
);
// Remove duplicates
@ -2259,7 +2278,6 @@
bind:selectedModels
shareEnabled={!!history.currentId}
{initNewChat}
showBanners={!showCommands}
archiveChatHandler={() => {}}
{moveChatHandler}
onSaveTempChat={async () => {
@ -2379,11 +2397,7 @@
if (e.detail || files.length > 0) {
await tick();
submitPrompt(
($settings?.richTextInput ?? true)
? e.detail.replaceAll('\n\n', '\n')
: e.detail
);
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
}
}}
/>
@ -2432,11 +2446,7 @@
clearDraft();
if (e.detail || files.length > 0) {
await tick();
submitPrompt(
($settings?.richTextInput ?? true)
? e.detail.replaceAll('\n\n', '\n')
: e.detail
);
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
}
}}
/>

View file

@ -45,6 +45,7 @@
type={file.type}
size={file?.size}
dismissible={true}
small={true}
on:dismiss={() => {
// Remove the file from the chatFiles array

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-9999 bg-white dark:bg-gray-900 dark:text-white shadow-xs"
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-9999 bg-white dark:bg-gray-900 dark:text-white shadow-xs"
sideOffset={6}
side="top"
align="start"
@ -34,7 +34,7 @@
>
{#each devices as device}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
dispatch('change', device.deviceId);
}}

View file

@ -0,0 +1,159 @@
<script lang="ts">
import { knowledge, prompts } from '$lib/stores';
import { getPrompts } from '$lib/apis/prompts';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import Prompts from './Commands/Prompts.svelte';
import Knowledge from './Commands/Knowledge.svelte';
import Models from './Commands/Models.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import { onMount } from 'svelte';
export let char = '';
export let query = '';
export let command: (payload: { id: string; label: string }) => void;
export let onSelect = (e) => {};
export let onUpload = (e) => {};
export let insertTextHandler = (text) => {};
let suggestionElement = null;
let loading = false;
let filteredItems = [];
const init = async () => {
loading = true;
await Promise.all([
(async () => {
prompts.set(await getPrompts(localStorage.token));
})(),
(async () => {
knowledge.set(await getKnowledgeBases(localStorage.token));
})()
]);
loading = false;
};
onMount(() => {
init();
});
const onKeyDown = (event: KeyboardEvent) => {
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
if (event.key === 'ArrowUp') {
suggestionElement?.selectUp();
const item = document.querySelector(`[data-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
return true;
}
if (event.key === 'ArrowDown') {
suggestionElement?.selectDown();
const item = document.querySelector(`[data-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
return true;
}
if (event.key === 'Enter' || event.key === 'Tab') {
suggestionElement?.select();
if (event.key === 'Enter') {
event.preventDefault();
}
return true;
}
if (event.key === 'Escape') {
return true;
}
return false;
};
// This method will be called from the suggestion renderer
// @ts-ignore
export function _onKeyDown(event: KeyboardEvent) {
return onKeyDown(event);
}
</script>
<div
class="{(filteredItems ?? []).length > 0
? ''
: 'hidden'} rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-72 p-1"
id="suggestions-container"
>
<div class="overflow-y-auto scrollbar-thin max-h-60">
{#if !loading}
{#if char === '/'}
<Prompts
bind:this={suggestionElement}
{query}
bind:filteredItems
prompts={$prompts ?? []}
onSelect={(e) => {
const { type, data } = e;
if (type === 'prompt') {
insertTextHandler(data.content);
}
}}
/>
{:else if char === '#'}
<Knowledge
bind:this={suggestionElement}
{query}
bind:filteredItems
knowledge={$knowledge ?? []}
onSelect={(e) => {
const { type, data } = e;
if (type === 'knowledge') {
insertTextHandler('');
onUpload({
type: 'file',
data: data
});
} else if (type === 'youtube') {
insertTextHandler('');
onUpload({
type: 'youtube',
data: data
});
} else if (type === 'web') {
insertTextHandler('');
onUpload({
type: 'web',
data: data
});
}
}}
/>
{:else if char === '@'}
<Models
bind:this={suggestionElement}
{query}
bind:filteredItems
onSelect={(e) => {
const { type, data } = e;
if (type === 'model') {
insertTextHandler('');
onSelect({
type: 'model',
data: data
});
}
}}
/>
{/if}
{:else}
<div class="py-4 flex flex-col w-full rounded-xl text-gray-700 dark:text-gray-300">
<Spinner />
</div>
{/if}
</div>
</div>

View file

@ -1,129 +0,0 @@
<script>
import { knowledge, prompts } from '$lib/stores';
import { removeLastWordFromString } from '$lib/utils';
import { getPrompts } from '$lib/apis/prompts';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import Prompts from './Commands/Prompts.svelte';
import Knowledge from './Commands/Knowledge.svelte';
import Models from './Commands/Models.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
export let show = false;
export let files = [];
export let command = '';
export let onSelect = (e) => {};
export let onUpload = (e) => {};
export let insertTextHandler = (text) => {};
let loading = false;
let commandElement = null;
export const selectUp = () => {
commandElement?.selectUp();
};
export const selectDown = () => {
commandElement?.selectDown();
};
$: if (show) {
init();
}
const init = async () => {
loading = true;
await Promise.all([
(async () => {
prompts.set(await getPrompts(localStorage.token));
})(),
(async () => {
knowledge.set(await getKnowledgeBases(localStorage.token));
})()
]);
loading = false;
};
</script>
{#if show}
{#if !loading}
{#if command?.charAt(0) === '/'}
<Prompts
bind:this={commandElement}
{command}
onSelect={(e) => {
const { type, data } = e;
if (type === 'prompt') {
insertTextHandler(data.content);
}
}}
/>
{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
<Knowledge
bind:this={commandElement}
command={command.includes('\\#') ? command.slice(2) : command}
onSelect={(e) => {
const { type, data } = e;
if (type === 'knowledge') {
insertTextHandler('');
onUpload({
type: 'file',
data: data
});
} else if (type === 'youtube') {
insertTextHandler('');
onUpload({
type: 'youtube',
data: data
});
} else if (type === 'web') {
insertTextHandler('');
onUpload({
type: 'web',
data: data
});
}
}}
/>
{:else if command?.charAt(0) === '@'}
<Models
bind:this={commandElement}
{command}
onSelect={(e) => {
const { type, data } = e;
if (type === 'model') {
insertTextHandler('');
onSelect({
type: 'model',
data: data
});
}
}}
/>
{/if}
{:else}
<div
id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<Spinner />
</div>
</div>
</div>
{/if}
{/if}

View file

@ -8,29 +8,48 @@
import { tick, getContext, onMount, onDestroy } from 'svelte';
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
import { knowledge } from '$lib/stores';
import { getNoteList, getNotes } from '$lib/apis/notes';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
import Database from '$lib/components/icons/Database.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Youtube from '$lib/components/icons/Youtube.svelte';
const i18n = getContext('i18n');
export let command = '';
export let query = '';
export let onSelect = (e) => {};
export let knowledge = [];
let selectedIdx = 0;
let items = [];
let fuse = null;
let filteredItems = [];
export let filteredItems = [];
$: if (fuse) {
filteredItems = command.slice(1)
? fuse.search(command).map((e) => {
return e.item;
})
: items;
filteredItems = [
...(query
? fuse.search(query).map((e) => {
return e.item;
})
: items),
...(query.startsWith('http')
? query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')
? [{ type: 'youtube', name: query, description: query }]
: [
{
type: 'web',
name: query,
description: query
}
]
: [])
];
}
$: if (command) {
$: if (query) {
selectedIdx = 0;
}
@ -42,32 +61,14 @@
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
};
let container;
let adjustHeightDebounce;
const adjustHeight = () => {
if (container) {
if (adjustHeightDebounce) {
clearTimeout(adjustHeightDebounce);
}
adjustHeightDebounce = setTimeout(() => {
if (!container) return;
// Ensure the container is visible before adjusting height
const rect = container.getBoundingClientRect();
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
}, 100);
export const select = async () => {
// find item with data-selected=true
const item = document.querySelector(`[data-selected="true"]`);
if (item) {
// click the item
item.click();
}
};
const confirmSelect = async (type, data) => {
onSelect({
type: type,
data: data
});
};
const decodeString = (str: string) => {
try {
return decodeURIComponent(str);
@ -77,22 +78,7 @@
};
onMount(async () => {
window.addEventListener('resize', adjustHeight);
let notes = await getNoteList(localStorage.token).catch(() => {
return [];
});
notes = notes.map((note) => {
return {
...note,
type: 'note',
name: note.title,
description: dayjs(note.updated_at / 1000000).fromNow()
};
});
let legacy_documents = $knowledge
let legacy_documents = knowledge
.filter((item) => item?.meta?.document)
.map((item) => ({
...item,
@ -127,16 +113,16 @@
]
: [];
let collections = $knowledge
let collections = knowledge
.filter((item) => !item?.meta?.document)
.map((item) => ({
...item,
type: 'collection'
}));
let collection_files =
$knowledge.length > 0
knowledge.length > 0
? [
...$knowledge
...knowledge
.reduce((a, item) => {
return [
...new Set([
@ -158,196 +144,145 @@
]
: [];
items = [
...notes,
...collections,
...collection_files,
...legacy_collections,
...legacy_documents
].map((item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
});
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']
});
await tick();
adjustHeight();
});
const onKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
select();
}
};
onMount(() => {
window.addEventListener('keydown', onKeyDown);
});
onDestroy(() => {
window.removeEventListener('resize', adjustHeight);
window.removeEventListener('keydown', onKeyDown);
});
</script>
{#if filteredItems.length > 0 || command?.substring(1).startsWith('http')}
<div
id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div class="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 max-h-60"
id="command-options-container"
bind:this={container}
>
{#each filteredItems as item, idx}
<button
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"
on:click={() => {
console.log(item);
confirmSelect('knowledge', item);
}}
on:mousemove={() => {
selectedIdx = idx;
}}
>
<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-sm uppercase text-xs font-bold px-1 shrink-0"
>
Legacy
</div>
{:else if item?.meta?.document}
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
>
Document
</div>
{:else if item?.type === 'file'}
<div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
>
File
</div>
{:else if item?.type === 'note'}
<div
class="bg-blue-500/20 text-blue-700 dark:text-blue-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
>
Note
</div>
{:else}
<div
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
>
Collection
</div>
{/if}
<div class="px-2 text-xs text-gray-500 py-1">
{$i18n.t('Knowledge')}
</div>
<div class="line-clamp-1">
{decodeString(item?.name)}
</div>
</div>
{#if filteredItems.length > 0 || query.startsWith('http')}
{#each filteredItems as item, idx}
{#if !['youtube', 'web'].includes(item.type)}
<button
class=" px-2 py-1 rounded-xl w-full text-left flex justify-between items-center {idx ===
selectedIdx
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
console.log(item);
onSelect({
type: 'knowledge',
data: item
});
}}
on:mousemove={() => {
selectedIdx = idx;
}}
data-selected={idx === selectedIdx}
>
<div class=" text-black dark:text-gray-100 flex items-center gap-1">
<Tooltip
content={item?.legacy
? $i18n.t('Legacy')
: item?.type === 'file'
? $i18n.t('File')
: item?.type === 'collection'
? $i18n.t('Collection')
: ''}
placement="top"
>
{#if item?.type === 'collection'}
<Database className="size-4" />
{:else}
<DocumentPage className="size-4" />
{/if}
</Tooltip>
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
{item?.description}
</div>
</div>
</button>
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
<div class="line-clamp-1 flex-1">
{decodeString(item?.name)}
</div>
</Tooltip>
</div>
</button>
{/if}
{/each}
<!-- <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-sm uppercase text-xs font-bold px-1 shrink-0"
>
File
</div>
{#if query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')}
<button
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button"
type="button"
data-selected={true}
on:click={() => {
if (isValidHttpUrl(query)) {
onSelect({
type: 'youtube',
data: query
});
} else {
toast.error(
$i18n.t('Oops! Looks like the URL is invalid. Please double-check and try again.')
);
}
}}
>
<div class=" text-black dark:text-gray-100 line-clamp-1 flex items-center gap-1">
<Tooltip content={$i18n.t('YouTube')} placement="top">
<Youtube className="size-4" />
</Tooltip>
<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('File not found.')}
</div>
{/if}
</div> -->
{/each}
{#if command.substring(1).startsWith('https://www.youtube.com') || command
.substring(1)
.startsWith('https://youtu.be')}
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
type="button"
on:click={() => {
if (isValidHttpUrl(command.substring(1))) {
confirmSelect('youtube', command.substring(1));
} else {
toast.error(
$i18n.t(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
)
);
}
}}
>
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
{command.substring(1)}
</div>
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
</button>
{:else if command.substring(1).startsWith('http')}
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
type="button"
on:click={() => {
if (isValidHttpUrl(command.substring(1))) {
confirmSelect('web', command.substring(1));
} else {
toast.error(
$i18n.t(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
)
);
}
}}
>
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
{command}
</div>
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
</button>
{/if}
<div class="truncate flex-1">
{query}
</div>
</div>
</div>
</div>
</button>
{:else if query.startsWith('http')}
<button
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button"
type="button"
data-selected={true}
on:click={() => {
if (isValidHttpUrl(query)) {
onSelect({
type: 'web',
data: query
});
} else {
toast.error(
$i18n.t('Oops! Looks like the URL is invalid. Please double-check and try again.')
);
}
}}
>
<div class=" text-black dark:text-gray-100 line-clamp-1 flex items-center gap-1">
<Tooltip content={$i18n.t('Web')} placement="top">
<GlobeAlt className="size-4" />
</Tooltip>
<div class="truncate flex-1">
{query}
</div>
</div>
</button>
{/if}
{/if}

View file

@ -6,14 +6,15 @@
import { models } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let command = '';
export let query = '';
export let onSelect = (e) => {};
let selectedIdx = 0;
let filteredItems = [];
export let filteredItems = [];
let fuse = new Fuse(
$models
@ -33,13 +34,13 @@
}
);
$: filteredItems = command.slice(1)
? fuse.search(command.slice(1)).map((e) => {
$: filteredItems = query
? fuse.search(query).map((e) => {
return e.item;
})
: $models.filter((model) => !model?.info?.meta?.hidden);
$: if (command) {
$: if (query) {
selectedIdx = 0;
}
@ -51,85 +52,46 @@
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
};
let container;
let adjustHeightDebounce;
const adjustHeight = () => {
if (container) {
if (adjustHeightDebounce) {
clearTimeout(adjustHeightDebounce);
}
adjustHeightDebounce = setTimeout(() => {
if (!container) return;
// Ensure the container is visible before adjusting height
const rect = container.getBoundingClientRect();
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
}, 100);
export const select = async () => {
const model = filteredItems[selectedIdx];
if (model) {
onSelect({ type: 'model', data: model });
}
};
const confirmSelect = async (model) => {
onSelect({ type: 'model', data: model });
};
onMount(async () => {
window.addEventListener('resize', adjustHeight);
await tick();
const chatInputElement = document.getElementById('chat-input');
await tick();
chatInputElement?.focus();
await tick();
adjustHeight();
});
onDestroy(() => {
window.removeEventListener('resize', adjustHeight);
});
</script>
<div class="px-2 text-xs text-gray-500 py-1">
{$i18n.t('Models')}
</div>
{#if filteredItems.length > 0}
<div
id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div class="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 max-h-60"
id="command-options-container"
bind:this={container}
>
{#each filteredItems as model, modelIdx}
<button
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmSelect(model);
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
>
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
<img
src={model?.info?.meta?.profile_image_url ??
`${WEBUI_BASE_URL}/static/favicon.png`}
alt={model?.name ?? model.id}
class="rounded-full size-6 items-center mr-2"
/>
{model.name}
</div>
</button>
{/each}
{#each filteredItems as model, modelIdx}
<Tooltip content={model.id} placement="top-start">
<button
class="px-2.5 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
onSelect({ type: 'model', data: model });
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
data-selected={modelIdx === selectedIdx}
>
<div class="flex text-black dark:text-gray-100 line-clamp-1">
<img
src={model?.info?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
alt={model?.name ?? model.id}
class="rounded-full size-5 items-center mr-2"
/>
<div class="truncate">
{model.name}
</div>
</div>
</div>
</div>
</div>
</button>
</Tooltip>
{/each}
{/if}

View file

@ -1,140 +1,71 @@
<script lang="ts">
import { prompts, settings, user } from '$lib/stores';
import {
extractCurlyBraceWords,
getUserPosition,
getFormattedDate,
getFormattedTime,
getCurrentDateTime,
getUserTimezone,
getWeekday
} from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { tick, getContext, onMount, onDestroy } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let command = '';
export let query = '';
export let prompts = [];
export let onSelect = (e) => {};
let selectedPromptIdx = 0;
let filteredPrompts = [];
export let filteredItems = [];
$: filteredPrompts = $prompts
.filter((p) => p.command.toLowerCase().includes(command.toLowerCase()))
$: filteredItems = prompts
.filter((p) => p.command.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => a.title.localeCompare(b.title));
$: if (command) {
$: if (query) {
selectedPromptIdx = 0;
}
export const selectUp = () => {
selectedPromptIdx = Math.max(0, selectedPromptIdx - 1);
};
export const selectDown = () => {
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredItems.length - 1);
};
let container;
let adjustHeightDebounce;
const adjustHeight = () => {
if (container) {
if (adjustHeightDebounce) {
clearTimeout(adjustHeightDebounce);
}
adjustHeightDebounce = setTimeout(() => {
if (!container) return;
// Ensure the container is visible before adjusting height
const rect = container.getBoundingClientRect();
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 80), 100) + 'px';
}, 100);
export const select = async () => {
const command = filteredItems[selectedPromptIdx];
if (command) {
onSelect({ type: 'prompt', data: command });
}
};
const confirmPrompt = async (command) => {
onSelect({ type: 'prompt', data: command });
};
onMount(async () => {
window.addEventListener('resize', adjustHeight);
await tick();
adjustHeight();
});
onDestroy(() => {
window.removeEventListener('resize', adjustHeight);
});
</script>
{#if filteredPrompts.length > 0}
<div
id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div class="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 space-y-0.5 scrollbar-hidden max-h-60"
id="command-options-container"
bind:this={container}
<div class="px-2 text-xs text-gray-500 py-1">
{$i18n.t('Prompts')}
</div>
{#if filteredItems.length > 0}
<div class=" space-y-0.5 scrollbar-hidden">
{#each filteredItems as promptItem, promptIdx}
<Tooltip content={promptItem.title} placement="top-start">
<button
class=" px-3 py-1 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
? ' bg-gray-50 dark:bg-gray-800 selected-command-option-button'
: ''} truncate"
type="button"
on:click={() => {
onSelect({ type: 'prompt', data: promptItem });
}}
on:mousemove={() => {
selectedPromptIdx = promptIdx;
}}
on:focus={() => {}}
data-selected={promptIdx === selectedPromptIdx}
>
{#each filteredPrompts as promptItem, promptIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmPrompt(promptItem);
}}
on:mousemove={() => {
selectedPromptIdx = promptIdx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black dark:text-gray-100">
{promptItem.command}
</div>
<span class=" font-medium text-black dark:text-gray-100">
{promptItem.command}
</span>
<div class=" text-xs text-gray-600 dark:text-gray-100">
{promptItem.title}
</div>
</button>
{/each}
</div>
<div
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
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-3 h-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</div>
<div class="line-clamp-1">
{$i18n.t(
'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
)}
</div>
</div>
</div>
</div>
<span class=" text-xs text-gray-600 dark:text-gray-100">
{promptItem.title}
</span>
</button>
</Tooltip>
{/each}
</div>
{/if}

View file

@ -24,8 +24,10 @@
role="region"
aria-label="Drag and Drop Container"
>
<div class="absolute w-full h-full backdrop-blur-sm bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div
class="absolute w-full h-full backdrop-blur-sm bg-gray-100/50 dark:bg-gray-900/80 flex justify-center"
>
<div class="m-auto flex flex-col justify-center">
<div class="max-w-md">
<AddFilesPlaceholder />
</div>

View file

@ -1,27 +1,33 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext, onMount, tick } from 'svelte';
import { fly } from 'svelte/transition';
import { flyAndScale } from '$lib/utils/transitions';
import { config, user, tools as _tools, mobile } from '$lib/stores';
import { config, user, tools as _tools, mobile, knowledge, chats } from '$lib/stores';
import { createPicker } from '$lib/utils/google-drive-picker';
import { getTools } from '$lib/apis/tools';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
import PhotoSolid from '$lib/components/icons/PhotoSolid.svelte';
import CommandLineSolid from '$lib/components/icons/CommandLineSolid.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
import Camera from '$lib/components/icons/Camera.svelte';
import Note from '$lib/components/icons/Note.svelte';
import Clip from '$lib/components/icons/Clip.svelte';
import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
import Refresh from '$lib/components/icons/Refresh.svelte';
import Agile from '$lib/components/icons/Agile.svelte';
import ClockRotateRight from '$lib/components/icons/ClockRotateRight.svelte';
import Database from '$lib/components/icons/Database.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
import PageEdit from '$lib/components/icons/PageEdit.svelte';
import Chats from './InputMenu/Chats.svelte';
import Notes from './InputMenu/Notes.svelte';
import Knowledge from './InputMenu/Knowledge.svelte';
const i18n = getContext('i18n');
export let selectedToolIds: string[] = [];
export let files = [];
export let selectedModels: string[] = [];
export let fileUploadCapableModels: string[] = [];
@ -35,46 +41,41 @@
export let onClose: Function;
let tools = null;
let show = false;
let showAllTools = false;
$: if (show) {
init();
}
let tab = '';
let fileUploadEnabled = true;
$: fileUploadEnabled =
fileUploadCapableModels.length === selectedModels.length &&
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
const init = async () => {
await _tools.set(await getTools(localStorage.token));
if ($_tools) {
tools = $_tools.reduce((a, tool, i, arr) => {
a[tool.id] = {
name: tool.name,
description: tool.meta.description,
enabled: selectedToolIds.includes(tool.id)
};
return a;
}, {});
selectedToolIds = selectedToolIds.filter((id) => $_tools?.some((tool) => tool.id === id));
}
};
const detectMobile = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
};
function handleFileChange(event) {
const handleFileChange = (event) => {
const inputFiles = Array.from(event.target?.files);
if (inputFiles && inputFiles.length > 0) {
console.log(inputFiles);
inputFilesHandler(inputFiles);
}
}
};
const onSelect = (item) => {
if (files.find((f) => f.id === item.id)) {
return;
}
files = [
...files,
{
...item,
status: 'processed'
}
];
show = false;
};
</script>
<!-- Hidden file input used to open the camera on mobile -->
@ -101,299 +102,381 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[240px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={10}
alignOffset={-8}
side="top"
class="w-full max-w-70 rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin transition"
sideOffset={4}
alignOffset={-6}
side="bottom"
align="start"
transition={flyAndScale}
>
{#if tools}
{#if Object.keys(tools).length > 0}
<div class="{showAllTools ? ' max-h-96' : 'max-h-28'} overflow-y-auto scrollbar-thin">
{#each Object.keys(tools) as toolId}
{#if tab === ''}
<div in:fly={{ x: -20, duration: 150 }}>
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
if (fileUploadEnabled) {
uploadFilesHandler();
}
}}
>
<Clip />
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
</DropdownMenu.Item>
</Tooltip>
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
if (fileUploadEnabled) {
if (!detectMobile()) {
screenCaptureHandler();
} else {
const cameraInputElement = document.getElementById('camera-input');
if (cameraInputElement) {
cameraInputElement.click();
}
}
}
}}
>
<Camera />
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
</DropdownMenu.Item>
</Tooltip>
{#if $config?.features?.enable_notes ?? false}
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full"
>
<button
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
tools[toolId].enabled = !tools[toolId].enabled;
tab = 'notes';
}}
>
<div class="flex-1 truncate">
<Tooltip
content={tools[toolId]?.description ?? ''}
placement="top-start"
className="flex flex-1 gap-2 items-center"
>
<div class="shrink-0">
<WrenchSolid />
</div>
<PageEdit />
<div class=" truncate">{tools[toolId].name}</div>
</Tooltip>
</div>
<div class="flex items-center w-full justify-between">
<div class=" line-clamp-1">
{$i18n.t('Attach Notes')}
</div>
<div class=" shrink-0">
<Switch
state={tools[toolId].enabled}
on:change={async (e) => {
const state = e.detail;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}}
/>
<div class="text-gray-500">
<ChevronRight />
</div>
</div>
</button>
{/each}
</div>
{#if Object.keys(tools).length > 3}
<button
class="flex w-full justify-center items-center text-sm font-medium cursor-pointer rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
showAllTools = !showAllTools;
}}
title={showAllTools ? $i18n.t('Show Less') : $i18n.t('Show All')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2.5"
stroke="currentColor"
class="size-3 transition-transform duration-200 {showAllTools
? 'rotate-180'
: ''} text-gray-300 dark:text-gray-600"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"
></path>
</svg>
</button>
</Tooltip>
{/if}
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full"
>
<button
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
tab = 'knowledge';
}}
>
<Database />
<div class="flex items-center w-full justify-between">
<div class=" line-clamp-1">
{$i18n.t('Attach Knowledge')}
</div>
<div class="text-gray-500">
<ChevronRight />
</div>
</div>
</button>
</Tooltip>
{#if ($chats ?? []).length > 0}
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full"
>
<button
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
tab = 'chats';
}}
>
<ClockRotateRight />
<div class="flex items-center w-full justify-between">
<div class=" line-clamp-1">
{$i18n.t('Reference Chats')}
</div>
<div class="text-gray-500">
<ChevronRight />
</div>
</div>
</button>
</Tooltip>
{/if}
{#if fileUploadEnabled}
{#if $config?.features?.enable_google_drive_integration}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadGoogleDriveHandler();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
<path
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
fill="#0066da"
/>
<path
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
fill="#00ac47"
/>
<path
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
fill="#ea4335"
/>
<path
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
fill="#00832d"
/>
<path
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
fill="#2684fc"
/>
<path
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
fill="#ffba00"
/>
</svg>
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
</DropdownMenu.Item>
{/if}
{#if $config?.features?.enable_onedrive_integration}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
class="w-5 h-5"
fill="none"
>
<mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
side={$mobile ? 'bottom' : 'right'}
sideOffset={$mobile ? 5 : 0}
alignOffset={$mobile ? 0 : -8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('personal');
}}
>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('organizations');
}}
>
<div class="flex flex-col">
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
<div class="text-xs text-gray-500">{$i18n.t('Includes SharePoint')}</div>
</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
{/if}
<hr class="border-black/5 dark:border-white/5 my-1" />
{/if}
{:else}
<div class="py-4">
<Spinner />
</div>
<hr class="border-black/5 dark:border-white/5 my-1" />
{/if}
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
if (fileUploadEnabled) {
if (!detectMobile()) {
screenCaptureHandler();
} else {
const cameraInputElement = document.getElementById('camera-input');
if (cameraInputElement) {
cameraInputElement.click();
}
}
}
}}
>
<CameraSolid />
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
</DropdownMenu.Item>
</Tooltip>
<Tooltip
content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
? 'opacity-50'
: ''}"
on:click={() => {
if (fileUploadEnabled) {
uploadFilesHandler();
}
}}
>
<DocumentArrowUpSolid />
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
</DropdownMenu.Item>
</Tooltip>
{#if fileUploadEnabled}
{#if $config?.features?.enable_google_drive_integration}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
{:else if tab === 'knowledge'}
<div in:fly={{ x: 20, duration: 150 }}>
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
uploadGoogleDriveHandler();
tab = '';
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
<path
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
fill="#0066da"
/>
<path
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
fill="#00ac47"
/>
<path
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
fill="#ea4335"
/>
<path
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
fill="#00832d"
/>
<path
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
fill="#2684fc"
/>
<path
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
fill="#ffba00"
/>
</svg>
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
</DropdownMenu.Item>
{/if}
<ChevronLeft />
{#if $config?.features?.enable_onedrive_integration}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
class="w-5 h-5"
fill="none"
>
<mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
side={$mobile ? 'bottom' : 'right'}
sideOffset={$mobile ? 5 : 0}
alignOffset={$mobile ? 0 : -8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('personal');
}}
>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('organizations');
}}
>
<div class="flex flex-col">
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
<div class="text-xs text-gray-500">{$i18n.t('Includes SharePoint')}</div>
</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
<div class="flex items-center w-full justify-between">
<div>
{$i18n.t('Knowledge')}
</div>
</div>
</button>
<Knowledge {onSelect} />
</div>
{:else if tab === 'notes'}
<div in:fly={{ x: 20, duration: 150 }}>
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
tab = '';
}}
>
<ChevronLeft />
<div class="flex items-center w-full justify-between">
<div>
{$i18n.t('Notes')}
</div>
</div>
</button>
<Notes {onSelect} />
</div>
{:else if tab === 'chats'}
<div in:fly={{ x: 20, duration: 150 }}>
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
tab = '';
}}
>
<ChevronLeft />
<div class="flex items-center w-full justify-between">
<div>
{$i18n.t('Chats')}
</div>
</div>
</button>
<Chats {onSelect} />
</div>
{/if}
</DropdownMenu.Content>
</div>

View file

@ -0,0 +1,125 @@
<script lang="ts">
import dayjs from 'dayjs';
import { onMount, tick, getContext } from 'svelte';
import { decodeString } from '$lib/utils';
import { getChatList } from '$lib/apis/chats';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Loader from '$lib/components/common/Loader.svelte';
import { chatId } from '$lib/stores';
const i18n = getContext('i18n');
export let onSelect = (e) => {};
let loaded = false;
let items = [];
let selectedIdx = 0;
let page = 1;
let itemsLoading = false;
let allItemsLoaded = false;
const loadMoreItems = async () => {
if (allItemsLoaded) return;
page += 1;
await getItemsPage();
};
const getItemsPage = async () => {
itemsLoading = true;
let res = await getChatList(localStorage.token, page).catch(() => {
return [];
});
if ((res ?? []).length === 0) {
allItemsLoaded = true;
} else {
allItemsLoaded = false;
}
items = [
...items,
...res
.filter((item) => item?.id !== $chatId)
.map((item) => {
return {
...item,
type: 'chat',
name: item.title,
description: dayjs(item.updated_at * 1000).fromNow()
};
})
];
itemsLoading = false;
return res;
};
onMount(async () => {
await getItemsPage();
await tick();
loaded = true;
});
</script>
{#if loaded}
{#if items.length === 0}
<div class="text-center text-xs text-gray-500 py-3">{$i18n.t('No chats found')}</div>
{:else}
<div class="flex flex-col gap-0.5">
{#each items as item, idx}
<button
class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm {idx ===
selectedIdx
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
onSelect(item);
}}
on:mousemove={() => {
selectedIdx = idx;
}}
on:mouseleave={() => {
if (idx === 0) {
selectedIdx = -1;
}
}}
data-selected={idx === selectedIdx}
>
<div class="text-black dark:text-gray-100 flex items-center gap-1.5">
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
<div class="line-clamp-1 flex-1">
{decodeString(item?.name)}
</div>
</Tooltip>
</div>
</button>
{/each}
{#if !allItemsLoaded}
<Loader
on:visible={(e) => {
if (!itemsLoading) {
loadMoreItems();
}
}}
>
<div class="w-full flex justify-center py-4 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">{$i18n.t('Loading...')}</div>
</div>
</Loader>
{/if}
</div>
{/if}
{:else}
<div class="py-4.5">
<Spinner />
</div>
{/if}

View file

@ -0,0 +1,163 @@
<script lang="ts">
import { onMount, tick, getContext } from 'svelte';
import { decodeString } from '$lib/utils';
import { knowledge } from '$lib/stores';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Database from '$lib/components/icons/Database.svelte';
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n = getContext('i18n');
export let onSelect = (e) => {};
let loaded = false;
let items = [];
let selectedIdx = 0;
onMount(async () => {
if ($knowledge === null) {
await knowledge.set(await getKnowledgeBases(localStorage.token));
}
let legacy_documents = $knowledge
.filter((item) => item?.meta?.document)
.map((item) => ({
...item,
type: 'file'
}));
let legacy_collections =
legacy_documents.length > 0
? [
{
name: 'All Documents',
legacy: true,
type: 'collection',
description: 'Deprecated (legacy collection), please create a new knowledge base.',
title: $i18n.t('All Documents'),
collection_names: legacy_documents.map((item) => item.id)
},
...legacy_documents
.reduce((a, item) => {
return [...new Set([...a, ...(item?.meta?.tags ?? []).map((tag) => tag.name)])];
}, [])
.map((tag) => ({
name: tag,
legacy: true,
type: 'collection',
description: 'Deprecated (legacy collection), please create a new knowledge base.',
collection_names: legacy_documents
.filter((item) => (item?.meta?.tags ?? []).map((tag) => tag.name).includes(tag))
.map((item) => item.id)
}))
]
: [];
let collections = $knowledge
.filter((item) => !item?.meta?.document)
.map((item) => ({
...item,
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 } // DO NOT REMOVE, USED IN FILE DESCRIPTION/ATTACHMENT
}))
])
];
}, [])
.map((file) => ({
...file,
name: file?.meta?.name,
description: `${file?.collection?.name} - ${file?.collection?.description}`,
knowledge: true, // DO NOT REMOVE, USED TO INDICATE KNOWLEDGE BASE FILE
type: 'file'
}))
]
: [];
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
(item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
}
);
await tick();
loaded = true;
});
</script>
{#if loaded}
<div class="flex flex-col gap-0.5">
{#each items as item, idx}
<button
class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm {idx ===
selectedIdx
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
console.log(item);
onSelect(item);
}}
on:mousemove={() => {
selectedIdx = idx;
}}
on:mouseleave={() => {
if (idx === 0) {
selectedIdx = -1;
}
}}
data-selected={idx === selectedIdx}
>
<div class=" text-black dark:text-gray-100 flex items-center gap-1">
<Tooltip
content={item?.legacy
? $i18n.t('Legacy')
: item?.type === 'file'
? $i18n.t('File')
: item?.type === 'collection'
? $i18n.t('Collection')
: ''}
placement="top"
>
{#if item?.type === 'collection'}
<Database className="size-4" />
{:else}
<DocumentPage className="size-4" />
{/if}
</Tooltip>
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
<div class="line-clamp-1 flex-1">
{decodeString(item?.name)}
</div>
</Tooltip>
</div>
</button>
{/each}
</div>
{:else}
<div class="py-4.5">
<Spinner />
</div>
{/if}

View file

@ -0,0 +1,128 @@
<script lang="ts">
import dayjs from 'dayjs';
import { onMount, tick, getContext } from 'svelte';
import { decodeString } from '$lib/utils';
import { getNoteList } from '$lib/apis/notes';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import PageEdit from '$lib/components/icons/PageEdit.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Loader from '$lib/components/common/Loader.svelte';
const i18n = getContext('i18n');
export let onSelect = (e) => {};
let loaded = false;
let items = [];
let selectedIdx = 0;
let page = 1;
let itemsLoading = false;
let allItemsLoaded = false;
const loadMoreItems = async () => {
if (allItemsLoaded) return;
page += 1;
await getItemsPage();
};
const getItemsPage = async () => {
itemsLoading = true;
let res = await getNoteList(localStorage.token, page).catch(() => {
return [];
});
if ((res ?? []).length === 0) {
allItemsLoaded = true;
} else {
allItemsLoaded = false;
}
items = [
...items,
...res.map((note) => {
return {
...note,
type: 'note',
name: note.title,
description: dayjs(note.updated_at / 1000000).fromNow()
};
})
];
itemsLoading = false;
return res;
};
onMount(async () => {
await getItemsPage();
await tick();
loaded = true;
});
</script>
{#if loaded}
{#if items.length === 0}
<div class="text-center text-xs text-gray-500 py-3">{$i18n.t('No notes found')}</div>
{:else}
<div class="flex flex-col gap-0.5">
{#each items as item, idx}
<button
class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm {idx ===
selectedIdx
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
onSelect(item);
}}
on:mousemove={() => {
selectedIdx = idx;
}}
on:mouseleave={() => {
if (idx === 0) {
selectedIdx = -1;
}
}}
data-selected={idx === selectedIdx}
>
<div class="text-black dark:text-gray-100 flex items-center gap-1.5">
<Tooltip content={$i18n.t('Note')} placement="top">
<PageEdit className="size-4" />
</Tooltip>
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
<div class="line-clamp-1 flex-1">
{decodeString(item?.name)}
</div>
</Tooltip>
</div>
</button>
{/each}
{#if !allItemsLoaded}
<Loader
on:visible={(e) => {
if (!itemsLoading) {
loadMoreItems();
}
}}
>
<div class="w-full flex justify-center py-4 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">{$i18n.t('Loading...')}</div>
</div>
</Loader>
{/if}
</div>
{/if}
{:else}
<div class="py-4.5">
<Spinner />
</div>
{/if}

View file

@ -0,0 +1,328 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { getContext, onMount, tick } from 'svelte';
import { fly } from 'svelte/transition';
import { flyAndScale } from '$lib/utils/transitions';
import { config, user, tools as _tools, mobile, settings } from '$lib/stores';
import { getTools } from '$lib/apis/tools';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Wrench from '$lib/components/icons/Wrench.svelte';
import Sparkles from '$lib/components/icons/Sparkles.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Photo from '$lib/components/icons/Photo.svelte';
import Terminal from '$lib/components/icons/Terminal.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
const i18n = getContext('i18n');
export let selectedToolIds: string[] = [];
export let selectedModels: string[] = [];
export let fileUploadCapableModels: string[] = [];
export let toggleFilters: { id: string; name: string; description?: string; icon?: string }[] =
[];
export let selectedFilterIds: string[] = [];
export let showWebSearchButton = false;
export let webSearchEnabled = false;
export let showImageGenerationButton = false;
export let imageGenerationEnabled = false;
export let showCodeInterpreterButton = false;
export let codeInterpreterEnabled = false;
export let onClose: Function;
let show = false;
let tab = '';
let tools = null;
$: if (show) {
init();
}
let fileUploadEnabled = true;
$: fileUploadEnabled =
fileUploadCapableModels.length === selectedModels.length &&
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
const init = async () => {
await _tools.set(await getTools(localStorage.token));
if ($_tools) {
tools = $_tools.reduce((a, tool, i, arr) => {
a[tool.id] = {
name: tool.name,
description: tool.meta.description,
enabled: selectedToolIds.includes(tool.id)
};
return a;
}, {});
selectedToolIds = selectedToolIds.filter((id) => $_tools?.some((tool) => tool.id === id));
}
};
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<Tooltip content={$i18n.t('Integrations')} placement="top">
<slot />
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-70 rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin"
sideOffset={4}
alignOffset={-6}
side="bottom"
align="start"
transition={flyAndScale}
>
{#if tab === ''}
<div in:fly={{ x: -20, duration: 150 }}>
{#if tools}
{#if Object.keys(tools).length > 0}
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
tab = 'tools';
}}
>
<Wrench />
<div class="flex items-center w-full justify-between">
<div class=" line-clamp-1">
{$i18n.t('Tools')}
<span class="ml-0.5 text-gray-500">{Object.keys(tools).length}</span>
</div>
<div class="text-gray-500">
<ChevronRight />
</div>
</div>
</button>
{/if}
{:else}
<div class="py-4">
<Spinner />
</div>
{/if}
{#if toggleFilters && toggleFilters.length > 0}
{#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)}
<Tooltip content={filter?.description} placement="top-start">
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
if (selectedFilterIds.includes(filter.id)) {
selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
} else {
selectedFilterIds = [...selectedFilterIds, filter.id];
}
}}
>
<div class="flex-1 truncate">
<div class="flex flex-1 gap-2 items-center">
<div class="shrink-0">
{#if filter?.icon}
<div class="size-4 items-center flex justify-center">
<img
src={filter.icon}
class="size-3.5 {filter.icon.includes('svg')
? 'dark:invert-[80%]'
: ''}"
style="fill: currentColor;"
alt={filter.name}
/>
</div>
{:else}
<Sparkles className="size-4" strokeWidth="1.75" />
{/if}
</div>
<div class=" truncate">{filter?.name}</div>
</div>
</div>
<div class=" shrink-0">
<Switch
state={selectedFilterIds.includes(filter.id)}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div>
</button>
</Tooltip>
{/each}
{/if}
{#if showWebSearchButton}
<Tooltip content={$i18n.t('Search the internet')} placement="top-start">
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
webSearchEnabled = !webSearchEnabled;
}}
>
<div class="flex-1 truncate">
<div class="flex flex-1 gap-2 items-center">
<div class="shrink-0">
<GlobeAlt />
</div>
<div class=" truncate">{$i18n.t('Web Search')}</div>
</div>
</div>
<div class=" shrink-0">
<Switch
state={webSearchEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div>
</button>
</Tooltip>
{/if}
{#if showImageGenerationButton}
<Tooltip content={$i18n.t('Generate an image')} placement="top-start">
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
imageGenerationEnabled = !imageGenerationEnabled;
}}
>
<div class="flex-1 truncate">
<div class="flex flex-1 gap-2 items-center">
<div class="shrink-0">
<Photo className="size-4" strokeWidth="1.5" />
</div>
<div class=" truncate">{$i18n.t('Image')}</div>
</div>
</div>
<div class=" shrink-0">
<Switch
state={imageGenerationEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div>
</button>
</Tooltip>
{/if}
{#if showCodeInterpreterButton}
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top-start">
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
aria-pressed={codeInterpreterEnabled}
aria-label={codeInterpreterEnabled
? $i18n.t('Disable Code Interpreter')
: $i18n.t('Enable Code Interpreter')}
on:click={() => {
codeInterpreterEnabled = !codeInterpreterEnabled;
}}
>
<div class="flex-1 truncate">
<div class="flex flex-1 gap-2 items-center">
<div class="shrink-0">
<Terminal className="size-3.5" strokeWidth="1.75" />
</div>
<div class=" truncate">{$i18n.t('Code Interpreter')}</div>
</div>
</div>
<div class=" shrink-0">
<Switch
state={codeInterpreterEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div>
</button>
</Tooltip>
{/if}
</div>
{:else if tab === 'tools' && tools}
<div in:fly={{ x: 20, duration: 150 }}>
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
tab = '';
}}
>
<ChevronLeft />
<div class="flex items-center w-full justify-between">
<div>
{$i18n.t('Tools')}
<span class="ml-0.5 text-gray-500">{Object.keys(tools).length}</span>
</div>
</div>
</button>
{#each Object.keys(tools) as toolId}
<button
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
tools[toolId].enabled = !tools[toolId].enabled;
}}
>
<div class="flex-1 truncate">
<div class="flex flex-1 gap-2 items-center">
<Tooltip content={tools[toolId]?.name ?? ''} placement="top">
<div class="shrink-0">
<Wrench />
</div>
</Tooltip>
<Tooltip content={tools[toolId]?.description ?? ''} placement="top-start">
<div class=" truncate">{tools[toolId].name}</div>
</Tooltip>
</div>
</div>
<div class=" shrink-0">
<Switch
state={tools[toolId].enabled}
on:change={async (e) => {
const state = e.detail;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}}
/>
</div>
</button>
{/each}
</div>
{/if}
</DropdownMenu.Content>
</div>
</Dropdown>

View file

@ -158,7 +158,7 @@
selectedCitation = citation;
}}
>
<div class=" font-medium bg-gray-50 rounded-md px-1.5">
<div class=" font-medium bg-gray-50 dark:bg-gray-850 rounded-md px-1">
{idx + 1}
</div>
<div

View file

@ -60,19 +60,21 @@
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">
<div class=" flex justify-between dark:text-gray-300 px-4.5 pt-3 pb-2">
<div class=" text-lg font-medium self-center flex items-center">
{#if citation?.source?.name}
{@const document = mergedDocuments?.[0]}
{#if document?.metadata?.file_id || document.source?.url?.includes('http')}
<Tooltip
className="w-fit"
content={$i18n.t('Open file')}
content={document.source?.url?.includes('http')
? $i18n.t('Open link')
: $i18n.t('Open file')}
placement="top-start"
tippyOptions={{ duration: [500, 0] }}
>
<a
class="hover:text-gray-500 dark:hover:text-gray-100 underline grow"
class="hover:text-gray-500 dark:hover:text-gray-100 underline grow line-clamp-1"
href={document?.metadata?.file_id
? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
: document.source?.url?.includes('http')
@ -100,9 +102,9 @@
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
<div class="flex flex-col md:flex-row w-full px-5 pb-5 md:space-x-4">
<div
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden gap-1"
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-thin gap-1"
>
{#each mergedDocuments as document, documentIdx}
<div class="flex flex-col w-full gap-2">

View file

@ -5,6 +5,7 @@
import markedExtension from '$lib/utils/marked/extension';
import markedKatexExtension from '$lib/utils/marked/katex-extension';
import { mentionExtension } from '$lib/utils/marked/mention-extension';
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
@ -37,6 +38,7 @@
marked.use(markedKatexExtension(options));
marked.use(markedExtension(options));
marked.use({ extensions: [mentionExtension({ triggerChar: '@' })] });
$: (async () => {
if (content) {

View file

@ -16,6 +16,7 @@
import HtmlToken from './HTMLToken.svelte';
import TextToken from './MarkdownInlineTokens/TextToken.svelte';
import CodespanToken from './MarkdownInlineTokens/CodespanToken.svelte';
import MentionToken from './MarkdownInlineTokens/MentionToken.svelte';
export let id: string;
export let done = true;
@ -60,6 +61,8 @@
frameborder="0"
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
></iframe>
{:else if token.type === 'mention'}
<MentionToken {token} />
{:else if token.type === 'text'}
<TextToken {token} {done} />
{/if}

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { Token } from 'marked';
import Tooltip from '$lib/components/common/Tooltip.svelte';
export let token: Token;
</script>
<Tooltip as="span" className="mention" content={token.id} placement="top">
{token?.triggerChar ?? '@'}{token?.label ?? token?.id}
</Tooltip>

View file

@ -634,7 +634,12 @@
: 'invisible group-hover:visible transition text-gray-400'}"
>
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
<span class="line-clamp-1"
>{$i18n.t(formatDate(message.timestamp * 1000), {
LOCALIZED_TIME: dayjs(message.timestamp * 1000).format('LT'),
LOCALIZED_DATE: dayjs(message.timestamp * 1000).format('L')
})}</span
>
</Tooltip>
</div>
{/if}
@ -663,7 +668,7 @@
name={file.name}
type={file.type}
size={file?.size}
colorClassName="bg-white dark:bg-gray-850 "
small={true}
/>
{/if}
</div>
@ -700,7 +705,7 @@
<div>
<button
id="save-new-message-button"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border border-gray-100 dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
class="px-3.5 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border border-gray-100 dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
on:click={() => {
saveAsCopyHandler();
}}
@ -712,7 +717,7 @@
<div class="flex space-x-1.5">
<button
id="close-edit-message-button"
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
class="px-3.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
@ -722,7 +727,7 @@
<button
id="confirm-edit-message-button"
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
class="px-3.5 py-1.5 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}

View file

@ -32,19 +32,26 @@
{#if showHistory}
<div class="flex flex-row">
{#if history.length > 1}
<div class="w-1 border-r border-gray-50 dark:border-gray-800 mt-3 -mb-2.5" />
<div class="w-full -translate-x-[7.5px]">
<div class="w-full">
{#each history as status, idx}
{#if idx !== history.length - 1}
<div class="flex items-start gap-2 mb-1">
<div class="pt-3 px-1">
<span class="relative flex size-2">
<div class="flex items-stretch gap-2 mb-1">
<div class=" ">
<div class="pt-3 px-1 mb-1.5">
<span
class="relative inline-flex size-1.5 rounded-full bg-gray-200 dark:bg-gray-700"
></span>
</span>
class="relative flex size-1.5 rounded-full justify-center items-center"
>
<span
class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-300"
></span>
</span>
</div>
<div
class="w-[0.5px] ml-[6.5px] h-[calc(100%-14px)] bg-gray-300 dark:bg-gray-700"
/>
</div>
<StatusItem {status} done={true} />
</div>
{/if}
@ -55,20 +62,20 @@
{/if}
<button
class="w-full -translate-x-[3.5px]"
class="w-full"
on:click={() => {
showHistory = !showHistory;
}}
>
<div class="flex items-start gap-2">
<div class="pt-3 px-1">
<span class="relative flex size-2">
<span class="relative flex size-1.5 rounded-full justify-center items-center">
{#if status?.done === false}
<span
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-400 dark:bg-gray-700 opacity-75"
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-500 dark:bg-gray-300 opacity-75"
></span>
{/if}
<span class="relative inline-flex size-1.5 rounded-full bg-gray-200 dark:bg-gray-700"
<span class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-300"
></span>
</span>
</div>

View file

@ -153,7 +153,16 @@
: 'invisible group-hover:visible transition'}"
>
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
<!-- $i18n.t('Today at {{LOCALIZED_TIME}}') -->
<!-- $i18n.t('Yesterday at {{LOCALIZED_TIME}}') -->
<!-- $i18n.t('{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}') -->
<span class="line-clamp-1"
>{$i18n.t(formatDate(message.timestamp * 1000), {
LOCALIZED_TIME: dayjs(message.timestamp * 1000).format('LT'),
LOCALIZED_DATE: dayjs(message.timestamp * 1000).format('L')
})}</span
>
</Tooltip>
</div>
{/if}
@ -168,7 +177,12 @@
: 'invisible group-hover:visible transition text-gray-400'}"
>
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
<span class="line-clamp-1"
>{$i18n.t(formatDate(message.timestamp * 1000), {
LOCALIZED_TIME: dayjs(message.timestamp * 1000).format('LT'),
LOCALIZED_DATE: dayjs(message.timestamp * 1000).format('L')
})}</span
>
</Tooltip>
</div>
</div>
@ -189,7 +203,7 @@
name={file.name}
type={file.type}
size={file?.size}
colorClassName="bg-white dark:bg-gray-850 "
small={true}
/>
{/if}
</div>
@ -290,7 +304,7 @@
<div>
<button
id="save-edit-message-button"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border border-gray-100 dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
class="px-3.5 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border border-gray-100 dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler(false);
}}
@ -302,7 +316,7 @@
<div class="flex space-x-1.5">
<button
id="close-edit-message-button"
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
class="px-3.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
@ -312,7 +326,7 @@
<button
id="confirm-edit-message-button"
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
class="px-3.5 py-1.5 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}

View file

@ -44,7 +44,7 @@
<DropdownMenu.Content
strategy="fixed"
class="w-full max-w-[180px] text-sm rounded-xl px-1 py-1.5 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg"
class="w-full max-w-[180px] text-sm rounded-2xl px-1 py-1.5 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800"
sideOffset={-2}
side="bottom"
align="end"
@ -53,7 +53,7 @@
<DropdownMenu.Item
type="button"
aria-pressed={($settings?.pinnedModels ?? []).includes(model?.id)}
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
@ -79,7 +79,7 @@
<DropdownMenu.Item
type="button"
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();

View file

@ -383,7 +383,7 @@
>
<slot>
{#if searchEnabled}
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-1.5">
<div class="flex items-center gap-2.5 px-4 mt-3.5 mb-1.5">
<Search className="size-4" strokeWidth="2.5" />
<input
@ -416,7 +416,7 @@
</div>
{/if}
<div class="px-3">
<div class="px-2">
{#if tags && items.filter((item) => !(item.model?.info?.meta?.hidden ?? false)).length > 0}
<div
class=" flex w-full bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none mb-0.5"
@ -511,7 +511,7 @@
{/if}
</div>
<div class="px-3 max-h-64 overflow-y-auto group relative">
<div class="px-2 max-h-64 overflow-y-auto group relative">
{#each filteredItems as item, index}
<ModelItem
{selectedModelIdx}

View file

@ -47,7 +47,6 @@
export let history;
export let selectedModels;
export let showModelSelector = true;
export let showBanners = true;
export let onSaveTempChat: () => {};
export let archiveChatHandler: (id: string) => void;
@ -282,30 +281,28 @@
/>
{/if}
{#if showBanners}
{#each $banners.filter((b) => ![...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]'), ...closedBannerIds].includes(b.id)) as banner (banner.id)}
<Banner
{banner}
on:dismiss={(e) => {
const bannerId = e.detail;
{#each $banners.filter((b) => ![...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]'), ...closedBannerIds].includes(b.id)) as banner (banner.id)}
<Banner
{banner}
on:dismiss={(e) => {
const bannerId = e.detail;
if (banner.dismissible) {
localStorage.setItem(
'dismissedBannerIds',
JSON.stringify(
[
bannerId,
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
].filter((id) => $banners.find((b) => b.id === id))
)
);
} else {
closedBannerIds = [...closedBannerIds, bannerId];
}
}}
/>
{/each}
{/if}
if (banner.dismissible) {
localStorage.setItem(
'dismissedBannerIds',
JSON.stringify(
[
bannerId,
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
].filter((id) => $banners.find((b) => b.id === id))
)
);
} else {
closedBannerIds = [...closedBannerIds, bannerId];
}
}}
/>
{/each}
</div>
</div>
{/if}

View file

@ -76,7 +76,7 @@
<div class="flex justify-end pt-1 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-3xl flex flex-row space-x-1 items-center {loading
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full {loading
? ' cursor-not-allowed'
: ''}"
type="submit"

View file

@ -86,7 +86,7 @@
<div class="flex justify-end pt-1 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-3xl flex flex-row space-x-1 items-center {loading
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full {loading
? ' cursor-not-allowed'
: ''}"
type="submit"

View file

@ -18,7 +18,7 @@
dismissible: true,
timestamp: Math.floor(Date.now() / 1000)
};
export let className = 'mx-4';
export let className = 'mx-2 px-2 rounded-lg';
export let dismissed = false;
@ -46,7 +46,7 @@
{#if !dismissed}
{#if mounted}
<div
class="{className} top-0 left-0 right-0 py-0.5 flex justify-center items-center relative border border-transparent text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-30"
class="{className} top-0 left-0 right-0 py-1 flex justify-center items-center relative border border-transparent text-gray-800 dark:text-gary-100 bg-transparent backdrop-blur-xl z-30"
transition:fade={{ delay: 100, duration: 300 }}
>
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">

View file

@ -13,7 +13,8 @@
const dispatch = createEventDispatcher();
export let className = 'w-60';
export let colorClassName = 'bg-white dark:bg-gray-850 border border-gray-50 dark:border-white/5';
export let colorClassName =
'bg-white dark:bg-gray-850 border border-gray-50 dark:border-gray-800';
export let url: string | null = null;
export let dismissible = false;
@ -28,8 +29,10 @@
export let type: string;
export let size: number;
import { deleteFileById } from '$lib/apis/files';
import DocumentPage from '../icons/DocumentPage.svelte';
import Database from '../icons/Database.svelte';
import PageEdit from '../icons/PageEdit.svelte';
import ChatBubble from '../icons/ChatBubble.svelte';
let showModal = false;
const decodeString = (str: string) => {
@ -47,7 +50,7 @@
<button
class="relative group p-1.5 {className} flex items-center gap-1 {colorClassName} {small
? 'rounded-xl'
? 'rounded-xl p-2'
: 'rounded-2xl'} text-left"
type="button"
on:click={async () => {
@ -91,6 +94,35 @@
<Spinner />
{/if}
</div>
{:else}
<div class="pl-1.5">
{#if !loading}
<Tooltip
content={type === 'collection'
? $i18n.t('Collection')
: type === 'note'
? $i18n.t('Note')
: type === 'chat'
? $i18n.t('Chat')
: type === 'file'
? $i18n.t('File')
: $i18n.t('Document')}
placement="top"
>
{#if type === 'collection'}
<Database />
{:else if type === 'note'}
<PageEdit />
{:else if type === 'chat'}
<ChatBubble />
{:else}
<DocumentPage />
{/if}
</Tooltip>
{:else}
<Spinner />
{/if}
</div>
{/if}
{#if !small}
@ -106,6 +138,8 @@
>
{#if type === 'file'}
{$i18n.t('File')}
{:else if type === 'note'}
{$i18n.t('Note')}
{:else if type === 'doc'}
{$i18n.t('Document')}
{:else if type === 'collection'}
@ -120,15 +154,14 @@
</div>
{:else}
<Tooltip content={decodeString(name)} className="flex flex-col w-full" placement="top-start">
<div class="flex flex-col justify-center -space-y-0.5 px-2.5 w-full">
<div class="flex flex-col justify-center -space-y-0.5 px-1 w-full">
<div class=" dark:text-gray-100 text-sm flex justify-between items-center">
{#if loading}
<div class=" shrink-0 mr-2">
<Spinner className="size-4" />
</div>
<div class="font-medium line-clamp-1 flex-1 pr-1">{decodeString(name)}</div>
{#if size}
<div class="text-gray-500 text-xs capitalize shrink-0">{formatFileSize(size)}</div>
{:else}
<div class="text-gray-500 text-xs capitalize shrink-0">{type}</div>
{/if}
<div class="font-medium line-clamp-1 flex-1">{decodeString(name)}</div>
<div class="text-gray-500 text-xs capitalize shrink-0">{formatFileSize(size)}</div>
</div>
</div>
</Tooltip>

View file

@ -82,7 +82,7 @@
</script>
<Modal bind:show size="lg">
<div class="font-primary px-6 py-5 w-full flex flex-col justify-center dark:text-gray-400">
<div class="font-primary px-4.5 py-3.5 w-full flex flex-col justify-center dark:text-gray-400">
<div class=" pb-2">
<div class="flex items-start justify-between">
<div>

View file

@ -91,6 +91,18 @@
}
});
// Convert TipTap mention spans -> <@id>
turndownService.addRule('mentions', {
filter: (node) => node.nodeName === 'SPAN' && node.getAttribute('data-type') === 'mention',
replacement: (_content, node: HTMLElement) => {
const id = node.getAttribute('data-id') || '';
// TipTap stores the trigger char in data-mention-suggestion-char (usually "@")
const ch = node.getAttribute('data-mention-suggestion-char') || '@';
// Emit <@id> style, e.g. <@llama3.2:latest>
return `<${ch}${id}>`;
}
});
import { onMount, onDestroy, tick, getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
@ -100,7 +112,7 @@
import { Fragment, DOMParser } from 'prosemirror-model';
import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
import { Editor, Extension } from '@tiptap/core';
import { Editor, Extension, mergeAttributes } from '@tiptap/core';
// Yjs imports
import * as Y from 'yjs';
@ -137,13 +149,10 @@
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import Mention from '@tiptap/extension-mention';
import { all, createLowlight } from 'lowlight';
import FormattingButtons from './RichTextInput/FormattingButtons.svelte';
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
import FormattingButtons from './RichTextInput/FormattingButtons.svelte';
import { duration } from 'dayjs';
import { all, createLowlight } from 'lowlight';
export let oncompositionstart = (e) => {};
export let oncompositionend = (e) => {};
@ -162,9 +171,12 @@
export let className = 'input-prose';
export let placeholder = 'Type here...';
export let richText = true;
export let link = false;
export let image = false;
export let fileHandler = false;
export let suggestions = null;
export let onFileDrop = (currentEditor, files, pos) => {
files.forEach((file) => {
@ -951,6 +963,7 @@
}
console.log(bubbleMenuElement, floatingMenuElement);
console.log(suggestions);
editor = new Editor({
element: element,
@ -961,26 +974,32 @@
Placeholder.configure({ placeholder }),
SelectionDecoration,
CodeBlockLowlight.configure({
lowlight
}),
Highlight,
Typography,
...(richText
? [
CodeBlockLowlight.configure({
lowlight
}),
Highlight,
Typography,
TableKit.configure({
table: { resizable: true }
}),
ListKit.configure({
taskItem: {
nested: true
}
})
]
: []),
...(suggestions
? [
Mention.configure({
HTMLAttributes: { class: 'mention' },
suggestions: suggestions
})
]
: []),
Mention.configure({
HTMLAttributes: {
class: 'mention'
}
}),
TableKit.configure({
table: { resizable: true }
}),
ListKit.configure({
taskItem: {
nested: true
}
}),
CharacterCount.configure({}),
...(image ? [Image] : []),
...(fileHandler
@ -991,8 +1010,7 @@
})
]
: []),
...(autocomplete
...(richText && autocomplete
? [
AIAutocompletion.configure({
generateCompletion: async (text) => {
@ -1010,8 +1028,7 @@
})
]
: []),
...(showFormattingToolbar
...(richText && showFormattingToolbar
? [
BubbleMenu.configure({
element: bubbleMenuElement,
@ -1046,7 +1063,6 @@
htmlValue = editor.getHTML();
jsonValue = editor.getJSON();
mdValue = turndownService
.turndown(
htmlValue
@ -1086,6 +1102,22 @@
},
editorProps: {
attributes: { id },
handlePaste: (view, event) => {
// Force plain-text pasting when richText === false
if (!richText) {
const text = (event.clipboardData?.getData('text/plain') ?? '').replace(/\r\n/g, '\n');
// swallow HTML completely
event.preventDefault();
// Insert as pure text (no HTML parsing)
const { state, dispatch } = view;
const { from, to } = state.selection;
dispatch(state.tr.insertText(text, from, to).scrollIntoView());
return true; // handled
}
return false;
},
handleDOMEvents: {
compositionstart: (view, event) => {
oncompositionstart(event);
@ -1143,12 +1175,13 @@
if (event.key === 'Enter') {
const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac
const { state } = view;
const { $from } = state.selection;
const lineStart = $from.before($from.depth);
const lineEnd = $from.after($from.depth);
const lineText = state.doc.textBetween(lineStart, lineEnd, '\n', '\0').trim();
if (event.shiftKey && !isCtrlPressed) {
const { state } = view;
const { $from } = state.selection;
const lineStart = $from.before($from.depth);
const lineEnd = $from.after($from.depth);
const lineText = state.doc.textBetween(lineStart, lineEnd, '\n', '\0').trim();
if (lineText.startsWith('```')) {
// Fix GitHub issue #16337: prevent backtick removal for lines starting with ```
return false; // Let ProseMirror handle the Enter key normally
@ -1163,10 +1196,18 @@
const isInList = isInside(['listItem', 'bulletList', 'orderedList', 'taskList']);
const isInHeading = isInside(['heading']);
console.log({ isInCodeBlock, isInList, isInHeading });
if (isInCodeBlock || isInList || isInHeading) {
// Let ProseMirror handle the normal Enter behavior
return false;
}
const suggestionsElement = document.getElementById('suggestions-container');
if (lineText.startsWith('#') && suggestionsElement) {
console.log('Letting heading suggestion handle Enter key');
return true;
}
}
}
@ -1263,7 +1304,9 @@
editor.storage.files = files;
}
},
onSelectionUpdate: onSelectionUpdate
onSelectionUpdate: onSelectionUpdate,
enableInputRules: richText,
enablePasteRules: richText
});
if (messageInput) {
@ -1334,7 +1377,7 @@
};
</script>
{#if showFormattingToolbar}
{#if richText && showFormattingToolbar}
<div bind:this={bubbleMenuElement} id="bubble-menu" class="p-0">
<FormattingButtons {editor} />
</div>

View file

@ -22,7 +22,7 @@
</script>
<div
class="flex gap-0.5 p-0.5 rounded-lg shadow-lg bg-white text-gray-800 dark:text-white dark:bg-gray-800 min-w-fit"
class="flex gap-0.5 p-0.5 rounded-xl shadow-lg bg-white text-gray-800 dark:text-white dark:bg-gray-850 min-w-fit border border-gray-100 dark:border-gray-800"
>
<Tooltip placement="top" content={$i18n.t('H1')}>
<button

View file

@ -0,0 +1,26 @@
import { Extension } from '@tiptap/core';
import Suggestion from '@tiptap/suggestion';
export default Extension.create({
name: 'commands',
addOptions() {
return {
suggestion: {
char: '/',
command: ({ editor, range, props }) => {
props.command({ editor, range });
}
}
};
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion
})
];
}
});

View file

@ -0,0 +1,93 @@
import tippy from 'tippy.js';
export function getSuggestionRenderer(Component: any, ComponentProps = {}) {
return function suggestionRenderer() {
let component = null;
let container: HTMLDivElement | null = null;
let popup: TippyInstance | null = null;
let refEl: HTMLDivElement | null = null; // dummy reference
return {
onStart: (props: any) => {
container = document.createElement('div');
container.className = 'suggestion-list-container';
document.body.appendChild(container);
// mount Svelte component
component = new Component({
target: container,
props: {
char: props?.text,
query: props?.query,
command: (item) => {
props.command({ id: item.id, label: item.label });
},
...ComponentProps
},
context: new Map<string, any>([['i18n', ComponentProps?.i18n]])
});
// Create a tiny reference element so outside taps are truly "outside"
refEl = document.createElement('div');
Object.assign(refEl.style, {
position: 'fixed',
left: '0px',
top: '0px',
width: '0px',
height: '0px'
});
document.body.appendChild(refEl);
popup = tippy(refEl, {
getReferenceClientRect: props.clientRect as any,
appendTo: () => document.body,
content: container,
interactive: true,
trigger: 'manual',
theme: 'transparent',
placement: 'top-start',
offset: [-10, -2],
arrow: false
});
popup?.show();
},
onUpdate: (props: any) => {
if (!component) return;
component.$set({
query: props.query,
command: (item) => {
props.command({ id: item.id, label: item.label });
}
});
if (props.clientRect && popup) {
popup.setProps({ getReferenceClientRect: props.clientRect as any });
}
},
onKeyDown: (props: any) => {
// forward to the Svelte components handler
// (expose this from component as `export function onKeyDown(evt)`)
// @ts-ignore
return component?._onKeyDown?.(props.event) ?? false;
},
onExit: () => {
popup?.destroy();
popup = null;
component?.$destroy();
component = null;
if (container?.parentNode) container.parentNode.removeChild(container);
container = null;
if (refEl?.parentNode) refEl.parentNode.removeChild(refEl);
refEl = null;
}
};
};
}

View file

@ -22,15 +22,15 @@
bind:checked={state}
{id}
aria-labelledby={ariaLabelledbyId}
class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition {($settings?.highContrastMode ??
class="flex h-[18px] min-h-[18px] w-8 shrink-0 cursor-pointer items-center rounded-full px-1 mx-[1px] transition {($settings?.highContrastMode ??
false)
? 'focus:outline focus:outline-2 focus:outline-gray-800 focus:dark:outline-gray-200'
: 'outline outline-1 outline-gray-100 dark:outline-gray-800'} {state
? ' bg-emerald-600'
? ' bg-emerald-500 dark:bg-emerald-700'
: 'bg-gray-200 dark:bg-transparent'}"
>
<Switch.Thumb
class="pointer-events-none block size-4 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini "
class="pointer-events-none block size-3 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini "
/>
</Switch.Root>
</Tooltip>

View file

@ -7,10 +7,12 @@
export let elementId = '';
export let as = 'div';
export let className = 'flex';
export let placement = 'top';
export let content = `I'm a tooltip!`;
export let touch = true;
export let className = 'flex';
export let theme = '';
export let offset = [0, 4];
export let allowHTML = true;
@ -59,8 +61,8 @@
});
</script>
<div bind:this={tooltipElement} class={className}>
<svelte:element this={as} bind:this={tooltipElement} class={className}>
<slot />
</div>
</svelte:element>
<slot name="tooltip"></slot>

View file

@ -0,0 +1,27 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
><path
d="M17.5 19H22M22 19L19.5 16.5M22 19L19.5 21.5"
stroke-linecap="round"
stroke-linejoin="round"
></path><path d="M12 2L9.5 4.5L12 7" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M10.5 4.5C14.6421 4.5 18 7.85786 18 12C18 16.1421 14.6421 19.5 10.5 19.5H2"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M6.75583 5.5C4.51086 6.79595 3 9.22154 3 12C3 13.6884 3.55792 15.2465 4.49945 16.5"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,22 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M2 19V9C2 7.89543 2.89543 7 4 7H4.5C5.12951 7 5.72229 6.70361 6.1 6.2L8.32 3.24C8.43331 3.08892 8.61115 3 8.8 3H15.2C15.3889 3 15.5667 3.08892 15.68 3.24L17.9 6.2C18.2777 6.70361 18.8705 7 19.5 7H20C21.1046 7 22 7.89543 22 9V19C22 20.1046 21.1046 21 20 21H4C2.89543 21 2 20.1046 2 19Z"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M12 17C14.2091 17 16 15.2091 16 13C16 10.7909 14.2091 9 12 9C9.79086 9 8 10.7909 8 13C8 15.2091 9.79086 17 12 17Z"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,18 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M21.4383 11.6622L12.2483 20.8522C11.1225 21.9781 9.59552 22.6106 8.00334 22.6106C6.41115 22.6106 4.88418 21.9781 3.75834 20.8522C2.63249 19.7264 2 18.1994 2 16.6072C2 15.015 2.63249 13.4881 3.75834 12.3622L12.9483 3.17222C13.6989 2.42166 14.7169 2 15.7783 2C16.8398 2 17.8578 2.42166 18.6083 3.17222C19.3589 3.92279 19.7806 4.94077 19.7806 6.00222C19.7806 7.06368 19.3589 8.08166 18.6083 8.83222L9.40834 18.0222C9.03306 18.3975 8.52406 18.6083 7.99334 18.6083C7.46261 18.6083 6.95362 18.3975 6.57834 18.0222C6.20306 17.6469 5.99222 17.138 5.99222 16.6072C5.99222 16.0765 6.20306 15.5675 6.57834 15.1922L15.0683 6.71222"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,23 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
><path d="M12 6L12 12L18 12" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M21.8883 10.5C21.1645 5.68874 17.013 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C16.1006 22 19.6248 19.5318 21.1679 16"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M17 16H21.4C21.7314 16 22 16.2686 22 16.6V21"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,22 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M5.21173 15.1113L2.52473 12.4243C2.29041 12.1899 2.29041 11.8101 2.52473 11.5757L5.21173 8.88873C5.44605 8.65442 5.82595 8.65442 6.06026 8.88873L8.74727 11.5757C8.98158 11.8101 8.98158 12.1899 8.74727 12.4243L6.06026 15.1113C5.82595 15.3456 5.44605 15.3456 5.21173 15.1113Z"
></path><path
d="M11.5757 21.475L8.88874 18.788C8.65443 18.5537 8.65443 18.1738 8.88874 17.9395L11.5757 15.2525C11.8101 15.0182 12.19 15.0182 12.4243 15.2525L15.1113 17.9395C15.3456 18.1738 15.3456 18.5537 15.1113 18.788L12.4243 21.475C12.19 21.7094 11.8101 21.7094 11.5757 21.475Z"
></path><path
d="M11.5757 8.7475L8.88874 6.06049C8.65443 5.82618 8.65443 5.44628 8.88874 5.21197L11.5757 2.52496C11.8101 2.29065 12.19 2.29065 12.4243 2.52496L15.1113 5.21197C15.3456 5.44628 15.3456 5.82618 15.1113 6.06049L12.4243 8.7475C12.19 8.98181 11.8101 8.98181 11.5757 8.7475Z"
></path><path
d="M17.9396 15.1113L15.2526 12.4243C15.0183 12.1899 15.0183 11.8101 15.2526 11.5757L17.9396 8.88873C18.174 8.65442 18.5539 8.65442 18.7882 8.88873L21.4752 11.5757C21.7095 11.8101 21.7095 12.1899 21.4752 12.4243L18.7882 15.1113C18.5539 15.3456 18.174 15.3456 17.9396 15.1113Z"
></path></svg
>

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path d="M5 12V18C5 18 5 21 12 21C19 21 19 18 19 18V12"></path><path
d="M5 6V12C5 12 5 15 12 15C19 15 19 12 19 12V6"
></path><path d="M12 3C19 3 19 6 19 6C19 6 19 9 12 9C5 9 5 6 5 6C5 6 5 3 12 3Z"></path></svg
>

View file

@ -0,0 +1,26 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M4 21.4V2.6C4 2.26863 4.26863 2 4.6 2H16.2515C16.4106 2 16.5632 2.06321 16.6757 2.17574L19.8243 5.32426C19.9368 5.43679 20 5.5894 20 5.74853V21.4C20 21.7314 19.7314 22 19.4 22H4.6C4.26863 22 4 21.7314 4 21.4Z"
stroke-linecap="round"
stroke-linejoin="round"
></path><path d="M8 10L16 10" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M8 18L16 18"
stroke-linecap="round"
stroke-linejoin="round"
></path><path d="M8 14L12 14" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M16 2V5.4C16 5.73137 16.2686 6 16.6 6H20"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,22 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M14 20.4V14.6C14 14.2686 14.2686 14 14.6 14H20.4C20.7314 14 21 14.2686 21 14.6V20.4C21 20.7314 20.7314 21 20.4 21H14.6C14.2686 21 14 20.7314 14 20.4Z"
></path><path
d="M3 20.4V14.6C3 14.2686 3.26863 14 3.6 14H9.4C9.73137 14 10 14.2686 10 14.6V20.4C10 20.7314 9.73137 21 9.4 21H3.6C3.26863 21 3 20.7314 3 20.4Z"
></path><path
d="M14 9.4V3.6C14 3.26863 14.2686 3 14.6 3H20.4C20.7314 3 21 3.26863 21 3.6V9.4C21 9.73137 20.7314 10 20.4 10H14.6C14.2686 10 14 9.73137 14 9.4Z"
></path><path
d="M3 9.4V3.6C3 3.26863 3.26863 3 3.6 3H9.4C9.73137 3 10 3.26863 10 3.6V9.4C10 9.73137 9.73137 10 9.4 10H3.6C3.26863 10 3 9.73137 3 9.4Z"
></path></svg
>

View file

@ -6,14 +6,12 @@
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width={strokeWidth}
class={className}
><path
d="M3 19V5C3 3.89543 3.89543 3 5 3H19C20.1046 3 21 3.89543 21 5V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19Z"
></path><path d="M8 14L12 10L16 14" stroke-linecap="round" stroke-linejoin="round"></path></svg
>
<path
fill-rule="evenodd"
d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7Zm5.01 1H5v2.01h2.01V8Zm3 0H8v2.01h2.01V8Zm3 0H11v2.01h2.01V8Zm3 0H14v2.01h2.01V8Zm3 0H17v2.01h2.01V8Zm-12 3H5v2.01h2.01V11Zm3 0H8v2.01h2.01V11Zm3 0H11v2.01h2.01V11Zm3 0H14v2.01h2.01V11Zm3 0H17v2.01h2.01V11Zm-12 3H5v2.01h2.01V14ZM8 14l-.001 2 8.011.01V14H8Zm11.01 0H17v2.01h2.01V14Z"
clip-rule="evenodd"
/>
</svg>

View file

@ -0,0 +1,28 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
><path
d="M20 12V5.74853C20 5.5894 19.9368 5.43679 19.8243 5.32426L16.6757 2.17574C16.5632 2.06321 16.4106 2 16.2515 2H4.6C4.26863 2 4 2.26863 4 2.6V21.4C4 21.7314 4.26863 22 4.6 22H11"
stroke-linecap="round"
stroke-linejoin="round"
></path><path d="M8 10H16M8 6H12M8 14H11" stroke-linecap="round" stroke-linejoin="round"
></path><path
d="M17.9541 16.9394L18.9541 15.9394C19.392 15.5015 20.102 15.5015 20.5399 15.9394V15.9394C20.9778 16.3773 20.9778 17.0873 20.5399 17.5252L19.5399 18.5252M17.9541 16.9394L14.963 19.9305C14.8131 20.0804 14.7147 20.2741 14.6821 20.4835L14.4394 22.0399L15.9957 21.7973C16.2052 21.7646 16.3988 21.6662 16.5487 21.5163L19.5399 18.5252M17.9541 16.9394L19.5399 18.5252"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M16 2V5.4C16 5.73137 16.2686 6 16.6 6H20"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,15 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path d="M6 12H12M18 12H12M12 12V6M12 12V18" stroke-linecap="round" stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,23 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
stroke-width={strokeWidth}
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
><path
d="M21.8883 13.5C21.1645 18.3113 17.013 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C16.1006 2 19.6248 4.46819 21.1679 8"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M17 8H21.4C21.7314 8 22 7.73137 22 7.4V3"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,22 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M9 22C12.866 22 16 18.866 16 15C16 11.134 12.866 8 9 8C5.13401 8 2 11.134 2 15C2 18.866 5.13401 22 9 22Z"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M15 16C18.866 16 22 12.866 22 9C22 5.13401 18.866 2 15 2C11.134 2 8 5.13401 8 9C8 12.866 11.134 16 15 16Z"
stroke-linecap="round"
stroke-linejoin="round"
></path></svg
>

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path d="M14 12L10.5 14V10L14 12Z" stroke-linecap="round" stroke-linejoin="round"></path><path
d="M2 12.7075V11.2924C2 8.39705 2 6.94939 2.90549 6.01792C3.81099 5.08645 5.23656 5.04613 8.08769 4.96549C9.43873 4.92728 10.8188 4.8999 12 4.8999C13.1812 4.8999 14.5613 4.92728 15.9123 4.96549C18.7634 5.04613 20.189 5.08645 21.0945 6.01792C22 6.94939 22 8.39705 22 11.2924V12.7075C22 15.6028 22 17.0505 21.0945 17.9819C20.189 18.9134 18.7635 18.9537 15.9124 19.0344C14.5613 19.0726 13.1812 19.1 12 19.1C10.8188 19.1 9.43867 19.0726 8.0876 19.0344C5.23651 18.9537 3.81097 18.9134 2.90548 17.9819C2 17.0505 2 15.6028 2 12.7075Z"
></path></svg
>

View file

@ -111,7 +111,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[200px] rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
sideOffset={-2}
side="bottom"
align="start"
@ -119,7 +119,7 @@
>
{#if $user?.role === 'admin' || ($user.permissions?.chat?.share ?? true)}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
shareHandler();
}}
@ -131,20 +131,20 @@
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
>
<Download strokeWidth="1.5" />
<div class="flex items-center">{$i18n.t('Download')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800"
transition={flyAndScale}
sideOffset={8}
>
{#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
downloadJSONExport();
}}
@ -154,7 +154,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
downloadTxt();
}}
@ -165,7 +165,7 @@
</DropdownMenu.Sub>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
renameHandler();
}}
@ -177,7 +177,7 @@
<hr class="border-gray-50 dark:border-gray-800 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
pinHandler();
}}
@ -194,20 +194,20 @@
{#if chatId}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
>
<Folder />
<div class="flex items-center">{$i18n.t('Move')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-52 overflow-y-auto scrollbar-hidden"
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white border border-gray-100 dark:border-gray-800 shadow-lg max-h-52 overflow-y-auto scrollbar-hidden"
transition={flyAndScale}
sideOffset={8}
>
{#each $folders.sort((a, b) => b.updated_at - a.updated_at) as folder}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
moveChatHandler(chatId, folder.id);
}}
@ -222,7 +222,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
cloneChatHandler();
}}
@ -232,7 +232,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
archiveChatHandler();
}}
@ -242,7 +242,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
deleteHandler();
}}

View file

@ -64,14 +64,14 @@
<slot name="content">
<DropdownMenu.Content
class="w-full {className} text-sm rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary"
class="w-full {className} rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg text-sm"
sideOffset={4}
side="bottom"
align="start"
transition={(e) => fade(e, { duration: 100 })}
>
<DropdownMenu.Item
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
on:click={async () => {
show = false;
@ -90,7 +90,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
on:click={async () => {
show = false;
@ -113,7 +113,7 @@
<DropdownMenu.Item
as="a"
href="/playground"
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition select-none"
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition select-none"
on:click={async () => {
show = false;
if ($mobile) {
@ -130,7 +130,7 @@
<DropdownMenu.Item
as="a"
href="/admin"
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition select-none"
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition select-none"
on:click={async () => {
show = false;
if ($mobile) {
@ -155,7 +155,7 @@
<DropdownMenu.Item
as="a"
target="_blank"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl transition"
id="chat-share-button"
on:click={() => {
show = false;
@ -170,7 +170,7 @@
<DropdownMenu.Item
as="a"
target="_blank"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl transition"
id="chat-share-button"
on:click={() => {
show = false;
@ -183,7 +183,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition cursor-pointer"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl transition cursor-pointer"
id="chat-share-button"
on:click={async () => {
show = false;
@ -203,7 +203,7 @@
<hr class=" border-gray-50 dark:border-gray-800 my-1 p-0" />
<DropdownMenu.Item
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={async () => {
const res = await userSignOut();
user.set(null);
@ -229,7 +229,7 @@
: ''}
>
<div
class="flex rounded-md py-1 px-3 text-xs gap-2.5 items-center"
class="flex rounded-xl py-1 px-3 text-xs gap-2.5 items-center"
on:mouseenter={() => {
getUsageInfo();
}}

View file

@ -327,7 +327,7 @@ Based on the user's instruction, update and enhance the existing notes or select
});
</script>
<div class="flex items-center mb-1.5 pt-1.5">
<div class="flex items-center mb-1.5 pt-1.5 pl-1.5 pr-2.5">
<div class=" -translate-x-1.5 flex items-center">
<button
class="p-0.5 bg-transparent transition rounded-lg"
@ -358,7 +358,7 @@ Based on the user's instruction, update and enhance the existing notes or select
</div>
</div>
<div class="flex flex-col items-center mb-2 flex-1 @container">
<div class="flex flex-col items-center flex-1 @container">
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
<div class="mx-auto w-full md:px-0 h-full relative">
<div class=" flex flex-col h-full">
@ -375,7 +375,7 @@ Based on the user's instruction, update and enhance the existing notes or select
</div>
</div>
<div class=" pb-2">
<div class=" pb-[1rem] pl-1.5 pr-2.5">
{#if selectedContent}
<div class="text-xs rounded-xl px-3.5 py-3 w-full markdown-prose-xs">
<blockquote>

View file

@ -17,7 +17,7 @@
};
</script>
<div class="flex items-center mb-1.5 pt-1.5">
<div class="flex items-center mb-1.5 pt-1.5 pl-1.5 pr-2.5">
<div class=" -translate-x-1.5 flex items-center">
<button
class="p-0.5 bg-transparent transition rounded-lg"
@ -36,7 +36,7 @@
</div>
</div>
<div class="mt-1">
<div class="mt-1 pl-1.5 pr-2.5">
<div class="pb-10">
{#if files.length > 0}
<div class=" text-xs font-medium pb-1">{$i18n.t('Files')}</div>

View file

@ -98,7 +98,7 @@
{#if show}
<div class="flex max-h-full min-h-full">
<div
class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
class="w-full pt-2 bg-white dark:shadow-lg dark:bg-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
>
<slot />
</div>

View file

@ -374,7 +374,7 @@
>
{#each notes[timeRange] as note, idx (note.id)}
<div
class=" flex space-x-4 cursor-pointer w-full px-4.5 py-4 bg-gray-50 dark:bg-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition"
class=" flex space-x-4 cursor-pointer w-full px-4.5 py-4 border border-gray-50 dark:border-gray-850 bg-transparent dark:hover:bg-gray-850 hover:bg-white rounded-2xl transition"
>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a

View file

@ -49,7 +49,7 @@
>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
>
<Download strokeWidth="2" />
@ -62,7 +62,7 @@
align="end"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
onDownload('txt');
}}
@ -71,7 +71,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
onDownload('md');
}}
@ -80,7 +80,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
onDownload('pdf');
}}
@ -93,7 +93,7 @@
{#if onCopyLink || onCopyToClipboard}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
>
<Share strokeWidth="2" />
@ -107,7 +107,7 @@
>
{#if onCopyLink}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
onCopyLink();
}}
@ -119,7 +119,7 @@
{#if onCopyToClipboard}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
onCopyToClipboard();
}}

View file

@ -51,6 +51,7 @@
import AccessControlModal from '../common/AccessControlModal.svelte';
import Search from '$lib/components/icons/Search.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import FilesOverlay from '$lib/components/chat/MessageInput/FilesOverlay.svelte';
let largeScreen = true;
@ -632,29 +633,7 @@
};
</script>
{#if dragged}
<div
class="fixed {$showSidebar
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
>
<div class="absolute w-full h-full backdrop-blur-sm bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<AddFilesPlaceholder>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents
</div>
</AddFilesPlaceholder>
</div>
</div>
</div>
</div>
{/if}
<FilesOverlay show={dragged} />
<SyncConfirmDialog
bind:show={showSyncConfirmModal}
message={$i18n.t(

View file

@ -22,18 +22,14 @@
});
</script>
<div>
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-sm font-semibold">{$i18n.t('Actions')}</div>
</div>
{#if actions.length > 0}
<div>
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-sm font-semibold">{$i18n.t('Actions')}</div>
</div>
<div class=" text-xs dark:text-gray-500">
{$i18n.t('To select actions here, add them to the "Functions" workspace first.')}
</div>
<div class="flex flex-col">
{#if actions.length > 0}
<div class=" flex items-center mt-2 flex-wrap">
<div class="flex flex-col">
<div class=" flex items-center flex-wrap">
{#each Object.keys(_actions) as action, actionIdx}
<div class=" flex items-center gap-2 mr-3">
<div class="self-center flex items-center">
@ -54,6 +50,6 @@
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/if}

View file

@ -0,0 +1,54 @@
<script lang="ts">
import { getContext } from 'svelte';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { marked } from 'marked';
const i18n = getContext('i18n');
const featureLabels = {
web_search: {
label: $i18n.t('Web Search'),
description: $i18n.t('Model can search the web for information')
},
image_generation: {
label: $i18n.t('Image Generation'),
description: $i18n.t('Model can generate images based on text prompts')
},
code_interpreter: {
label: $i18n.t('Code Interpreter'),
description: $i18n.t('Model can execute code and perform calculations')
}
};
export let availableFeatures = ['web_search', 'image_generation', 'code_interpreter'];
export let featureIds = [];
</script>
<div>
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-sm font-semibold">{$i18n.t('Default Features')}</div>
</div>
<div class="flex items-center mt-2 flex-wrap">
{#each availableFeatures as feature}
<div class=" flex items-center gap-2 mr-3">
<Checkbox
state={featureIds.includes(feature) ? 'checked' : 'unchecked'}
on:change={(e) => {
if (e.detail === 'checked') {
featureIds = [...featureIds, feature];
} else {
featureIds = featureIds.filter((id) => id !== feature);
}
}}
/>
<div class=" py-0.5 text-sm capitalize">
<Tooltip content={marked.parse(featureLabels[feature].description)}>
{$i18n.t(featureLabels[feature].label)}
</Tooltip>
</div>
</div>
{/each}
</div>
</div>

View file

@ -0,0 +1,55 @@
<script lang="ts">
import { getContext, onMount } from 'svelte';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let filters = [];
export let selectedFilterIds = [];
let _filters = {};
onMount(() => {
_filters = filters.reduce((acc, filter) => {
acc[filter.id] = {
...filter,
selected: selectedFilterIds.includes(filter.id)
};
return acc;
}, {});
});
</script>
<div>
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-sm font-semibold">{$i18n.t('Default Filters')}</div>
</div>
<div class="flex flex-col">
{#if filters.length > 0}
<div class=" flex items-center flex-wrap">
{#each Object.keys(_filters) as filter, filterIdx}
<div class=" flex items-center gap-2 mr-3">
<div class="self-center flex items-center">
<Checkbox
state={_filters[filter].selected ? 'checked' : 'unchecked'}
on:change={(e) => {
_filters[filter].selected = e.detail === 'checked';
selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected);
}}
/>
</div>
<div class=" py-0.5 text-sm w-full capitalize font-medium">
<Tooltip content={_filters[filter].meta.description}>
{_filters[filter].name}
</Tooltip>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>

View file

@ -22,19 +22,15 @@
});
</script>
<div>
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-sm font-semibold">{$i18n.t('Filters')}</div>
</div>
{#if filters.length > 0}
<div>
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-sm font-semibold">{$i18n.t('Filters')}</div>
</div>
<div class=" text-xs dark:text-gray-500">
{$i18n.t('To select filters here, add them to the "Functions" workspace first.')}
</div>
<!-- TODO: Filer order matters -->
<div class="flex flex-col">
{#if filters.length > 0}
<div class=" flex items-center mt-2 flex-wrap">
<!-- TODO: Filer order matters -->
<div class="flex flex-col">
<div class=" flex items-center flex-wrap">
{#each Object.keys(_filters) as filter, filterIdx}
<div class=" flex items-center gap-2 mr-3">
<div class="self-center flex items-center">
@ -62,6 +58,6 @@
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/if}

View file

@ -143,7 +143,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-96 rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[99999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg"
class="w-full max-w-96 rounded-xl px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-[99999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={8}
side="bottom"
align="start"

View file

@ -1,8 +1,14 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext, tick } from 'svelte';
import { models, tools, functions, knowledge as knowledgeCollections, user } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants';
import { getTools } from '$lib/apis/tools';
import { getFunctions } from '$lib/apis/functions';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
import Tags from '$lib/components/common/Tags.svelte';
import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte';
@ -11,15 +17,11 @@
import ActionsSelector from '$lib/components/workspace/Models/ActionsSelector.svelte';
import Capabilities from '$lib/components/workspace/Models/Capabilities.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import { getTools } from '$lib/apis/tools';
import { getFunctions } from '$lib/apis/functions';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import AccessControl from '../common/AccessControl.svelte';
import { stringify } from 'postcss';
import { toast } from 'svelte-sonner';
import Spinner from '$lib/components/common/Spinner.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import { getNoteList } from '$lib/apis/notes';
import DefaultFiltersSelector from './DefaultFiltersSelector.svelte';
import DefaultFeatures from './DefaultFeatures.svelte';
const i18n = getContext('i18n');
@ -79,6 +81,13 @@
let params = {
system: ''
};
let knowledge = [];
let toolIds = [];
let filterIds = [];
let defaultFilterIds = [];
let capabilities = {
vision: true,
file_upload: true,
@ -89,12 +98,9 @@
status_updates: true,
usage: undefined
};
let defaultFeatureIds = [];
let knowledge = [];
let toolIds = [];
let filterIds = [];
let actionIds = [];
let accessControl = {};
const addUsage = (base_model_id) => {
@ -172,6 +178,14 @@
}
}
if (defaultFilterIds.length > 0) {
info.meta.defaultFilterIds = defaultFilterIds;
} else {
if (info.meta.defaultFilterIds) {
delete info.meta.defaultFilterIds;
}
}
if (actionIds.length > 0) {
info.meta.actionIds = actionIds;
} else {
@ -180,6 +194,14 @@
}
}
if (defaultFeatureIds.length > 0) {
info.meta.defaultFeatureIds = defaultFeatureIds;
} else {
if (info.meta.defaultFeatureIds) {
delete info.meta.defaultFeatureIds;
}
}
info.params.system = system.trim() === '' ? null : system;
info.params.stop = params.stop ? params.stop.split(',').filter((s) => s.trim()) : null;
Object.keys(info.params).forEach((key) => {
@ -236,9 +258,6 @@
)
: null;
toolIds = model?.meta?.toolIds ?? [];
filterIds = model?.meta?.filterIds ?? [];
actionIds = model?.meta?.actionIds ?? [];
knowledge = (model?.meta?.knowledge ?? []).map((item) => {
if (item?.collection_name && item?.type !== 'file') {
return {
@ -257,7 +276,14 @@
return item;
}
});
toolIds = model?.meta?.toolIds ?? [];
filterIds = model?.meta?.filterIds ?? [];
defaultFilterIds = model?.meta?.defaultFilterIds ?? [];
actionIds = model?.meta?.actionIds ?? [];
capabilities = { ...capabilities, ...(model?.meta?.capabilities ?? {}) };
defaultFeatureIds = model?.meta?.defaultFeatureIds ?? [];
if ('access_control' in model) {
accessControl = model.access_control;
@ -725,6 +751,24 @@
/>
</div>
{#if filterIds.length > 0}
{@const toggleableFilters = $functions.filter(
(func) =>
func.type === 'filter' &&
(filterIds.includes(func.id) || func?.is_global) &&
func?.meta?.toggle
)}
{#if toggleableFilters.length > 0}
<div class="my-2">
<DefaultFiltersSelector
bind:selectedFilterIds={defaultFilterIds}
filters={toggleableFilters}
/>
</div>
{/if}
{/if}
<div class="my-2">
<ActionsSelector
bind:selectedActionIds={actionIds}
@ -736,6 +780,21 @@
<Capabilities bind:capabilities />
</div>
{#if Object.keys(capabilities).filter((key) => capabilities[key]).length > 0}
{@const availableFeatures = Object.entries(capabilities)
.filter(
([key, value]) =>
value && ['web_search', 'code_interpreter', 'image_generation'].includes(key)
)
.map(([key, value]) => key)}
{#if availableFeatures.length > 0}
<div class="my-2">
<DefaultFeatures {availableFeatures} bind:featureIds={defaultFeatureIds} />
</div>
{/if}
{/if}
<div class="my-2 text-gray-300 dark:text-gray-700">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>

View file

@ -48,14 +48,14 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-2xl p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
hideHandler();
}}
@ -107,7 +107,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
copyLinkHandler();
}}
@ -119,7 +119,7 @@
{#if $config?.features.enable_community_sharing}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
shareHandler();
}}
@ -130,7 +130,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
cloneHandler();
}}
@ -141,7 +141,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
exportHandler();
}}
@ -151,10 +151,10 @@
<div class="flex items-center">{$i18n.t('Export')}</div>
</DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-850 my-1" />
<hr class="border-gray-50 dark:border-gray-800 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
deleteHandler();
}}

View file

@ -39,7 +39,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"
@ -47,7 +47,7 @@
>
{#if $config.features.enable_community_sharing}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
@ -58,7 +58,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
cloneHandler();
}}
@ -69,7 +69,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
exportHandler();
}}
@ -82,7 +82,7 @@
<hr class="border-gray-100 dark:border-gray-850 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
deleteHandler();
}}

View file

@ -40,14 +40,14 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
editHandler();
}}
@ -72,7 +72,7 @@
{#if $config.features.enable_community_sharing}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
@ -83,7 +83,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
cloneHandler();
}}
@ -94,7 +94,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
exportHandler();
}}
@ -107,7 +107,7 @@
<hr class="border-gray-100 dark:border-gray-850 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
deleteHandler();
}}

View file

@ -61,7 +61,7 @@
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center"
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit"
>
{$i18n.t('Done')}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "",
"{{user}}'s Chats": "دردشات {{user}}",
"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
@ -146,6 +147,8 @@
"Ask a question": "",
"Assistant": "",
"Attach file from knowledge": "",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "انتبه للتفاصيل",
"Attribute for Mail": "",
"Attribute for Username": "",
@ -851,6 +854,7 @@
"Install from Github URL": "التثبيت من عنوان URL لجيثب",
"Instant Auto-Send After Voice Transcription": "",
"Integration": "",
"Integrations": "",
"Interface": "واجهه المستخدم",
"Invalid file content": "",
"Invalid file format.": "",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"Legacy": "",
"lexical": "",
"License": "",
"Lift List": "",
@ -1221,6 +1226,7 @@
"Rename": "إعادة تسمية",
"Reorder Models": "",
"Reply in Thread": "",
"required": "",
"Reranking Engine": "",
"Reranking Model": "إعادة تقييم النموذج",
"Reset": "",
@ -1369,10 +1375,8 @@
"Show": "عرض",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Formatting Toolbar": "",
"Show image preview": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "إظهار الاختصارات",
"Show your support!": "",
@ -1499,7 +1503,6 @@
"Tika": "",
"Tika Server URL required.": "",
"Tiktoken": "",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.",
"Title": "العنوان",
"Title (e.g. Tell me a fun fact)": "(e.g. Tell me a fun fact) العناون",
"Title Auto-Generation": "توليد تلقائي للعنوان",
@ -1519,6 +1522,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
"Toast notifications for new updates": "",
"Today": "اليوم",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "",
"Toggle settings": "فتح وأغلاق الاعدادات",
"Toggle sidebar": "فتح وأغلاق الشريط الجانبي",
@ -1659,6 +1663,7 @@
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "أمس",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "انت",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "",
@ -1672,7 +1677,7 @@
"Your Account": "",
"Your account status is currently pending activation.": "",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
"Youtube": "Youtube",
"YouTube": "Youtube",
"Youtube Language": "",
"Youtube Proxy URL": ""
}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "{{COUNT}} رد/ردود",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "",
"{{user}}'s Chats": "محادثات المستخدم {{user}}",
"{{webUIName}} Backend Required": "يتطلب الخلفية الخاصة بـ {{webUIName}}",
@ -146,6 +147,8 @@
"Ask a question": "اطرح سؤالاً",
"Assistant": "المساعد",
"Attach file from knowledge": "إرفاق ملف من المعرفة",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "الاهتمام بالتفاصيل",
"Attribute for Mail": "خاصية للبريد",
"Attribute for Username": "خاصية لاسم المستخدم",
@ -851,6 +854,7 @@
"Install from Github URL": "التثبيت من عنوان URL لجيثب",
"Instant Auto-Send After Voice Transcription": "إرسال تلقائي فوري بعد تحويل الصوت إلى نص",
"Integration": "التكامل",
"Integrations": "",
"Interface": "واجهه المستخدم",
"Invalid file content": "",
"Invalid file format.": "تنسيق ملف غير صالح.",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "اتركه فارغًا لتضمين جميع النماذج أو اختر نماذج محددة",
"Leave empty to use the default prompt, or enter a custom prompt": "اتركه فارغًا لاستخدام التوجيه الافتراضي، أو أدخل توجيهًا مخصصًا",
"Leave model field empty to use the default model.": "اترك حقل النموذج فارغًا لاستخدام النموذج الافتراضي.",
"Legacy": "",
"lexical": "",
"License": "الترخيص",
"Lift List": "",
@ -1221,6 +1226,7 @@
"Rename": "إعادة تسمية",
"Reorder Models": "إعادة ترتيب النماذج",
"Reply in Thread": "الرد داخل سلسلة الرسائل",
"required": "",
"Reranking Engine": "",
"Reranking Model": "إعادة تقييم النموذج",
"Reset": "إعادة تعيين",
@ -1369,10 +1375,8 @@
"Show": "عرض",
"Show \"What's New\" modal on login": "عرض نافذة \"ما الجديد\" عند تسجيل الدخول",
"Show Admin Details in Account Pending Overlay": "عرض تفاصيل المشرف في نافذة \"الحساب قيد الانتظار\"",
"Show All": "",
"Show Formatting Toolbar": "",
"Show image preview": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "إظهار الاختصارات",
"Show your support!": "أظهر دعمك!",
@ -1499,7 +1503,6 @@
"Tika": "Tika",
"Tika Server URL required.": "عنوان خادم Tika مطلوب.",
"Tiktoken": "Tiktoken",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.",
"Title": "العنوان",
"Title (e.g. Tell me a fun fact)": "(e.g. Tell me a fun fact) العناون",
"Title Auto-Generation": "توليد تلقائي للعنوان",
@ -1519,6 +1522,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "لاختيار الأدوات هنا، أضفها أولاً إلى مساحة العمل \"الأدوات\".",
"Toast notifications for new updates": "إشعارات منبثقة للتحديثات الجديدة",
"Today": "اليوم",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "",
"Toggle settings": "فتح وأغلاق الاعدادات",
"Toggle sidebar": "فتح وأغلاق الشريط الجانبي",
@ -1659,6 +1663,7 @@
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "أمس",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "انت",
"You are currently using a trial license. Please contact support to upgrade your license.": "أنت تستخدم حالياً ترخيصًا تجريبيًا. يُرجى التواصل مع الدعم للترقية.",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "يمكنك الدردشة مع {{maxCount}} ملف(ات) كحد أقصى في نفس الوقت.",
@ -1672,7 +1677,7 @@
"Your Account": "",
"Your account status is currently pending activation.": "حالة حسابك حالياً بانتظار التفعيل.",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "سيتم توجيه كامل مساهمتك مباشرة إلى مطور المكون الإضافي؛ لا تأخذ Open WebUI أي نسبة. ومع ذلك، قد تفرض منصة التمويل المختارة رسومًا خاصة بها.",
"Youtube": "Youtube",
"YouTube": "Youtube",
"Youtube Language": "لغة YouTube",
"Youtube Proxy URL": "رابط بروكسي YouTube"
}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "{{COUNT}} Отговори",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "",
"{{user}}'s Chats": "{{user}}'s чатове",
"{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд",
@ -146,6 +147,8 @@
"Ask a question": "Задайте въпрос",
"Assistant": "Асистент",
"Attach file from knowledge": "",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "Внимание към детайлите",
"Attribute for Mail": "Атрибут за поща",
"Attribute for Username": "Атрибут за потребителско име",
@ -851,6 +854,7 @@
"Install from Github URL": "Инсталиране от URL адреса на Github",
"Instant Auto-Send After Voice Transcription": "Незабавно автоматично изпращане след гласова транскрипция",
"Integration": "",
"Integrations": "",
"Interface": "Интерфейс",
"Invalid file content": "",
"Invalid file format.": "Невалиден формат на файла.",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "Оставете празно, за да включите всички модели или изберете конкретни модели",
"Leave empty to use the default prompt, or enter a custom prompt": "Оставете празно, за да използвате промпта по подразбиране, или въведете персонализиран промпт",
"Leave model field empty to use the default model.": "Оставете полето за модел празно, за да използвате модела по подразбиране.",
"Legacy": "",
"lexical": "",
"License": "Лиценз",
"Lift List": "",
@ -1221,6 +1226,7 @@
"Rename": "Преименуване",
"Reorder Models": "Преорганизиране на моделите",
"Reply in Thread": "Отговори в тред",
"required": "",
"Reranking Engine": "Двигател за пренареждане",
"Reranking Model": "Модел за преподреждане",
"Reset": "Нулиране",
@ -1365,10 +1371,8 @@
"Show": "Покажи",
"Show \"What's New\" modal on login": "Покажи модалния прозорец \"Какво е ново\" при вписване",
"Show Admin Details in Account Pending Overlay": "Покажи детайлите на администратора в наслагването на изчакващ акаунт",
"Show All": "Покажи всички",
"Show Formatting Toolbar": "",
"Show image preview": "",
"Show Less": "Покажи по-малко",
"Show Model": "Покажи модел",
"Show shortcuts": "Покажи преки пътища",
"Show your support!": "Покажете вашата подкрепа!",
@ -1495,7 +1499,6 @@
"Tika": "Тика",
"Tika Server URL required.": "Изисква се URL адрес на Тика сървъра.",
"Tiktoken": "Tiktoken",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Съвет: Актуализирайте няколко слота за променливи последователно, като натискате клавиша Tab в чат входа след всяка подмяна.",
"Title": "Заглавие",
"Title (e.g. Tell me a fun fact)": "Заглавие (напр. Кажете ми нещо забавно)",
"Title Auto-Generation": "Автоматично генериране на заглавие",
@ -1515,6 +1518,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "За да изберете инструменти тук, първо ги добавете към работното пространство \"Инструменти\".",
"Toast notifications for new updates": "Изскачащи известия за нови актуализации",
"Today": "Днес",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "",
"Toggle settings": "Превключване на настройките",
"Toggle sidebar": "Превключване на страничната лента",
@ -1655,6 +1659,7 @@
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "вчера",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "Вие",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Можете да чатите с максимум {{maxCount}} файл(а) наведнъж.",
@ -1668,7 +1673,7 @@
"Your Account": "",
"Your account status is currently pending activation.": "Статусът на вашия акаунт в момента очаква активиране.",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Цялата ви вноска ще отиде директно при разработчика на плъгина; Open WebUI не взима никакъв процент. Въпреки това, избраната платформа за финансиране може да има свои собствени такси.",
"Youtube": "Youtube",
"YouTube": "Youtube",
"Youtube Language": "Youtube език",
"Youtube Proxy URL": "Youtube Прокси URL"
}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "",
"{{user}}'s Chats": "{{user}}র চ্যাটস",
"{{webUIName}} Backend Required": "{{webUIName}} ব্যাকএন্ড আবশ্যক",
@ -146,6 +147,8 @@
"Ask a question": "",
"Assistant": "",
"Attach file from knowledge": "",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "বিস্তারিত বিশেষতা",
"Attribute for Mail": "",
"Attribute for Username": "",
@ -851,6 +854,7 @@
"Install from Github URL": "Github URL থেকে ইনস্টল করুন",
"Instant Auto-Send After Voice Transcription": "",
"Integration": "",
"Integrations": "",
"Interface": "ইন্টারফেস",
"Invalid file content": "",
"Invalid file format.": "",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"Legacy": "",
"lexical": "",
"License": "",
"Lift List": "",
@ -1221,6 +1226,7 @@
"Rename": "রেনেম",
"Reorder Models": "",
"Reply in Thread": "",
"required": "",
"Reranking Engine": "",
"Reranking Model": "রির্যাক্টিং মডেল",
"Reset": "",
@ -1365,10 +1371,8 @@
"Show": "দেখান",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Formatting Toolbar": "",
"Show image preview": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "শর্টকাটগুলো দেখান",
"Show your support!": "",
@ -1495,7 +1499,6 @@
"Tika": "",
"Tika Server URL required.": "",
"Tiktoken": "",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "পরামর্শ: একাধিক ভেরিয়েবল স্লট একের পর এক রিপ্লেস করার জন্য চ্যাট ইনপুটে কিবোর্ডের Tab বাটন ব্যবহার করুন।",
"Title": "শিরোনাম",
"Title (e.g. Tell me a fun fact)": "শিরোনাম (একটি উপস্থিতি বিবরণ জানান)",
"Title Auto-Generation": "স্বয়ংক্রিয় শিরোনামগঠন",
@ -1515,6 +1518,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
"Toast notifications for new updates": "",
"Today": "আজ",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "",
"Toggle settings": "সেটিংস টোগল",
"Toggle sidebar": "সাইডবার টোগল",
@ -1655,6 +1659,7 @@
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "আগামী",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "আপনি",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "",
@ -1668,7 +1673,7 @@
"Your Account": "",
"Your account status is currently pending activation.": "",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
"Youtube": "YouTube",
"YouTube": "YouTube",
"Youtube Language": "",
"Youtube Proxy URL": ""
}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "ལན་ {{COUNT}}",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "",
"{{user}}'s Chats": "{{user}} ཡི་ཁ་བརྡ།",
"{{webUIName}} Backend Required": "{{webUIName}} རྒྱབ་སྣེ་དགོས།",
@ -146,6 +147,8 @@
"Ask a question": "དྲི་བ་ཞིག་འདྲི་བ།",
"Assistant": "ལག་རོགས་པ།",
"Attach file from knowledge": "ཤེས་བྱའི་ནང་ནས་ཡིག་ཆ་འཇོག་པ།",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "ཞིབ་ཕྲར་དོ་སྣང་།",
"Attribute for Mail": "ཡིག་ཟམ་གྱི་ཁྱད་ཆོས།",
"Attribute for Username": "བེད་སྤྱོད་མིང་གི་ཁྱད་ཆོས།",
@ -851,6 +854,7 @@
"Install from Github URL": "Github URL ནས་སྒྲིག་སྦྱོར་བྱེད་པ།",
"Instant Auto-Send After Voice Transcription": "སྐད་ཆ་ཡིག་འབེབས་བྱས་རྗེས་ལམ་སང་རང་འགུལ་གཏོང་བ།",
"Integration": "མཉམ་འདྲེས།",
"Integrations": "",
"Interface": "ངོས་འཛིན།",
"Invalid file content": "",
"Invalid file format.": "ཡིག་ཆའི་བཀོད་པ་ནུས་མེད།",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "དཔེ་དབྱིབས་ཡོངས་རྫོགས་ཚུད་པར་སྟོང་པ་བཞག་པའམ། ཡང་ན་དཔེ་དབྱིབས་ངེས་ཅན་གདམ་ག་བྱེད་པ།",
"Leave empty to use the default prompt, or enter a custom prompt": "སྔོན་སྒྲིག་འགུལ་སློང་བེད་སྤྱོད་གཏོང་བར་སྟོང་པ་བཞག་པའམ། ཡང་ན་སྲོལ་བཟོས་འགུལ་སློང་འཇུག་པ།",
"Leave model field empty to use the default model.": "སྔོན་སྒྲིག་དཔེ་དབྱིབས་བེད་སྤྱོད་གཏོང་བར་དཔེ་དབྱིབས་ཀྱི་ཁོངས་སྟོང་པ་བཞག་པ།",
"Legacy": "",
"lexical": "",
"License": "ཆོག་མཆན།",
"Lift List": "",
@ -1221,6 +1226,7 @@
"Rename": "མིང་བསྐྱར་འདོགས།",
"Reorder Models": "དཔེ་དབྱིབས་བསྐྱར་སྒྲིག",
"Reply in Thread": "བརྗོད་གཞིའི་ནང་ལན་འདེབས།",
"required": "",
"Reranking Engine": "",
"Reranking Model": "བསྐྱར་སྒྲིག་དཔེ་དབྱིབས།",
"Reset": "སླར་སྒྲིག",
@ -1364,10 +1370,8 @@
"Show": "སྟོན་པ།",
"Show \"What's New\" modal on login": "ནང་འཛུལ་སྐབས་ \"གསར་པ་ཅི་ཡོད\" modal སྟོན་པ།",
"Show Admin Details in Account Pending Overlay": "རྩིས་ཁྲ་སྒུག་བཞིན་པའི་གཏོགས་ངོས་སུ་དོ་དམ་པའི་ཞིབ་ཕྲ་སྟོན་པ།",
"Show All": "",
"Show Formatting Toolbar": "",
"Show image preview": "",
"Show Less": "",
"Show Model": "དཔེ་དབྱིབས་སྟོན་པ།",
"Show shortcuts": "མྱུར་ལམ་སྟོན་པ།",
"Show your support!": "ཁྱེད་ཀྱི་རྒྱབ་སྐྱོར་སྟོན་པ།",
@ -1494,7 +1498,6 @@
"Tika": "Tika",
"Tika Server URL required.": "Tika Server URL དགོས་ངེས།",
"Tiktoken": "Tiktoken",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "བསམ་འཆར།: ཚབ་བྱེད་རེ་རེའི་རྗེས་སུ་ཁ་བརྡའི་ནང་འཇུག་ཏུ་ tab མཐེབ་གནོན་མནན་ནས་འགྱུར་ཚད་ཀྱི་གནས་མང་པོ་རྒྱུན་མཐུད་དུ་གསར་སྒྱུར་བྱེད་པ།",
"Title": "ཁ་བྱང་།",
"Title (e.g. Tell me a fun fact)": "ཁ་བྱང་ (དཔེར་ན། དགོད་བྲོ་བའི་དོན་དངོས་ཤིག་ང་ལ་ཤོད།)",
"Title Auto-Generation": "ཁ་བྱང་རང་འགུལ་བཟོ་སྐྲུན།",
@ -1514,6 +1517,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "ལག་ཆའི་ཚོགས་སྡེ་འདིར་གདམ་ག་བྱེད་པར། ཐོག་མར་དེ་དག་ \"ལག་ཆའི་\" ལས་ཡུལ་དུ་སྣོན་པ།",
"Toast notifications for new updates": "གསར་སྒྱུར་གསར་པའི་ཆེད་དུ་ Toast བརྡ་ཁྱབ།",
"Today": "དེ་རིང་།",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "",
"Toggle settings": "སྒྲིག་འགོད་བརྗེ་བ།",
"Toggle sidebar": "ཟུར་ངོས་བརྗེ་བ།",
@ -1654,6 +1658,7 @@
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "ཁ་ས།",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "ཁྱེད།",
"You are currently using a trial license. Please contact support to upgrade your license.": "ཁྱེད་ཀྱིས་ད་ལྟ་ཚོད་ལྟའི་ཆོག་མཆན་ཞིག་བེད་སྤྱོད་གཏོང་བཞིན་འདུག ཁྱེད་ཀྱི་ཆོག་མཆན་རིམ་སྤོར་བྱེད་པར་རོགས་སྐྱོར་དང་འབྲེལ་གཏུག་བྱེད་རོགས།",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "ཁྱེད་ཀྱིས་ཐེངས་གཅིག་ལ་ཡིག་ཆ་ {{maxCount}} ལས་མང་བ་དང་ཁ་བརྡ་བྱེད་མི་ཐུབ།",
@ -1667,7 +1672,7 @@
"Your Account": "",
"Your account status is currently pending activation.": "ཁྱེད་ཀྱི་རྩིས་ཁྲའི་གནས་སྟངས་ད་ལྟ་སྒུལ་བསྐྱོད་སྒུག་བཞིན་པ།",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "ཁྱེད་ཀྱི་ཞལ་འདེབས་ཆ་ཚང་ཐད་ཀར་ plugin གསར་སྤེལ་བ་ལ་འགྲོ་ངེས། Open WebUI ཡིས་བརྒྱ་ཆ་གང་ཡང་མི་ལེན། འོན་ཀྱང་། གདམ་ཟིན་པའི་མ་དངུལ་གཏོང་བའི་སྟེགས་བུ་ལ་དེའི་རང་གི་འགྲོ་གྲོན་ཡོད་སྲིད།",
"Youtube": "Youtube",
"YouTube": "Youtube",
"Youtube Language": "Youtube སྐད་ཡིག",
"Youtube Proxy URL": "Youtube Proxy URL"
}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "{{COUNT}} respostes",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "{{COUNT}} paraules",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "La descàrrega del model {{model}} s'ha cancel·lat",
"{{user}}'s Chats": "Els xats de {{user}}",
"{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari",
@ -146,6 +147,8 @@
"Ask a question": "Fer una pregunta",
"Assistant": "Assistent",
"Attach file from knowledge": "Associar arxiu del coneixement",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "Atenció al detall",
"Attribute for Mail": "Atribut per al Correu",
"Attribute for Username": "Atribut per al Nom d'usuari",
@ -851,6 +854,7 @@
"Install from Github URL": "Instal·lar des de l'URL de Github",
"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
"Integration": "Integració",
"Integrations": "",
"Interface": "Interfície",
"Invalid file content": "Continguts del fitxer no vàlids",
"Invalid file format.": "Format d'arxiu no vàlid.",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "Deixa-ho en blanc per incloure tots els models o selecciona models específics",
"Leave empty to use the default prompt, or enter a custom prompt": "Deixa-ho en blanc per utilitzar la indicació predeterminada o introdueix una indicació personalitzada",
"Leave model field empty to use the default model.": "Deixa el camp de model buit per utilitzar el model per defecte.",
"Legacy": "",
"lexical": "lèxic",
"License": "Llicència",
"Lift List": "Aixecar la llista",
@ -1221,6 +1226,7 @@
"Rename": "Canviar el nom",
"Reorder Models": "Reordenar els models",
"Reply in Thread": "Respondre al fil",
"required": "",
"Reranking Engine": "Motor de valoració",
"Reranking Model": "Model de reavaluació",
"Reset": "Restableix",
@ -1366,10 +1372,8 @@
"Show": "Mostrar",
"Show \"What's New\" modal on login": "Veure 'Què hi ha de nou' a l'entrada",
"Show Admin Details in Account Pending Overlay": "Mostrar els detalls de l'administrador a la superposició del compte pendent",
"Show All": "Mostrar tot",
"Show Formatting Toolbar": "Mostrar la barra de format",
"Show image preview": "Mostrar la previsualització de la imatge",
"Show Less": "Mostrar menys",
"Show Model": "Mostrar el model",
"Show shortcuts": "Mostrar dreceres",
"Show your support!": "Mostra el teu suport!",
@ -1496,7 +1500,6 @@
"Tika": "Tika",
"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
"Tiktoken": "Tiktoken",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza les diverses variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
"Title": "Títol",
"Title (e.g. Tell me a fun fact)": "Títol (p. ex. Digues-me quelcom divertit)",
"Title Auto-Generation": "Generació automàtica de títol",
@ -1516,6 +1519,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "Per seleccionar kits d'eines aquí, afegeix-los primer a l'espai de treball \"Eines\".",
"Toast notifications for new updates": "Notificacions Toast de noves actualitzacions",
"Today": "Avui",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "Alternar cerca",
"Toggle settings": "Alterna preferències",
"Toggle sidebar": "Alterna la barra lateral",
@ -1656,6 +1660,7 @@
"Yacy Password": "Contrasenya de Yacy",
"Yacy Username": "Nom d'usuari de Yacy",
"Yesterday": "Ahir",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "Tu",
"You are currently using a trial license. Please contact support to upgrade your license.": "Actualment esteu utilitzant una llicència de prova. Poseu-vos en contacte amb el servei d'assistència per actualitzar la vostra llicència.",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Només pots xatejar amb un màxim de {{maxCount}} fitxers alhora.",
@ -1669,7 +1674,7 @@
"Your Account": "El teu compte",
"Your account status is currently pending activation.": "El compte està actualment pendent d'activació",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Tota la teva contribució anirà directament al desenvolupador del complement; Open WebUI no se'n queda cap percentatge. Tanmateix, la plataforma de finançament escollida pot tenir les seves pròpies comissions.",
"Youtube": "Youtube",
"YouTube": "Youtube",
"Youtube Language": "Idioma de YouTube",
"Youtube Proxy URL": "URL de Proxy de Youtube"
}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "",
"{{user}}'s Chats": "",
"{{webUIName}} Backend Required": "Backend {{webUIName}} gikinahanglan",
@ -146,6 +147,8 @@
"Ask a question": "",
"Assistant": "",
"Attach file from knowledge": "",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "Pagtagad sa mga detalye",
"Attribute for Mail": "",
"Attribute for Username": "",
@ -851,6 +854,7 @@
"Install from Github URL": "",
"Instant Auto-Send After Voice Transcription": "",
"Integration": "",
"Integrations": "",
"Interface": "Interface",
"Invalid file content": "",
"Invalid file format.": "",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"Legacy": "",
"lexical": "",
"License": "",
"Lift List": "",
@ -1221,6 +1226,7 @@
"Rename": "",
"Reorder Models": "",
"Reply in Thread": "",
"required": "",
"Reranking Engine": "",
"Reranking Model": "",
"Reset": "",
@ -1365,10 +1371,8 @@
"Show": "Pagpakita",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Formatting Toolbar": "",
"Show image preview": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Ipakita ang mga shortcut",
"Show your support!": "",
@ -1495,7 +1499,6 @@
"Tika": "",
"Tika Server URL required.": "",
"Tiktoken": "",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Sugyot: Pag-update sa daghang variable nga lokasyon nga sunud-sunod pinaagi sa pagpindot sa tab key sa chat entry pagkahuman sa matag puli.",
"Title": "Titulo",
"Title (e.g. Tell me a fun fact)": "",
"Title Auto-Generation": "Awtomatikong paghimo sa titulo",
@ -1515,6 +1518,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "",
"Toast notifications for new updates": "",
"Today": "",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "",
"Toggle settings": "I-toggle ang mga setting",
"Toggle sidebar": "I-toggle ang sidebar",
@ -1655,6 +1659,7 @@
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "",
@ -1668,7 +1673,7 @@
"Your Account": "",
"Your account status is currently pending activation.": "",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
"Youtube": "",
"YouTube": "",
"Youtube Language": "",
"Youtube Proxy URL": ""
}

View file

@ -16,6 +16,7 @@
"{{COUNT}} Replies": "{{COUNT}} odpovědí",
"{{COUNT}} Sources": "",
"{{COUNT}} words": "{{COUNT}} slov",
"{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "",
"{{model}} download has been canceled": "Stažení modelu {{model}} bylo zrušeno",
"{{user}}'s Chats": "Konverzace uživatele {{user}}",
"{{webUIName}} Backend Required": "Je vyžadován backend {{webUIName}}",
@ -146,6 +147,8 @@
"Ask a question": "Položit otázku",
"Assistant": "Asistent",
"Attach file from knowledge": "Připojit soubor ze znalostní báze",
"Attach Knowledge": "",
"Attach Notes": "",
"Attention to detail": "Pozornost k detailům",
"Attribute for Mail": "Atribut pro e-mail",
"Attribute for Username": "Atribut pro uživatelské jméno",
@ -851,6 +854,7 @@
"Install from Github URL": "Instalovat z URL na Githubu",
"Instant Auto-Send After Voice Transcription": "Okamžité automatické odeslání po přepisu hlasu",
"Integration": "Integrace",
"Integrations": "",
"Interface": "Rozhraní",
"Invalid file content": "Neplatný obsah souboru",
"Invalid file format.": "Neplatný formát souboru.",
@ -909,6 +913,7 @@
"Leave empty to include all models or select specific models": "Ponechte prázdné pro zahrnutí všech modelů nebo vyberte konkrétní modely.",
"Leave empty to use the default prompt, or enter a custom prompt": "Ponechte prázdné pro použití výchozích instrukcí, nebo zadejte vlastní instrukce.",
"Leave model field empty to use the default model.": "Ponechte pole modelu prázdné pro použití výchozího modelu.",
"Legacy": "",
"lexical": "lexikální",
"License": "Licence",
"Lift List": "Zvýraznit seznam",
@ -1221,6 +1226,7 @@
"Rename": "Přejmenovat",
"Reorder Models": "Změnit pořadí modelů",
"Reply in Thread": "Odpovědět ve vlákně",
"required": "",
"Reranking Engine": "Jádro pro přehodnocení",
"Reranking Model": "Model pro přehodnocení",
"Reset": "Resetovat",
@ -1367,10 +1373,8 @@
"Show": "Zobrazit",
"Show \"What's New\" modal on login": "Zobrazit okno \"Co je nového\" při přihlášení",
"Show Admin Details in Account Pending Overlay": "Zobrazit podrobnosti administrátora v překryvné vrstvě čekajícího účtu",
"Show All": "Zobrazit vše",
"Show Formatting Toolbar": "Zobrazit panel nástrojů pro formátování",
"Show image preview": "Zobrazit náhled obrázku",
"Show Less": "Zobrazit méně",
"Show Model": "Zobrazit model",
"Show shortcuts": "Zobrazit zkratky",
"Show your support!": "Vyjádřete svou podporu!",
@ -1497,7 +1501,6 @@
"Tika": "Tika",
"Tika Server URL required.": "Je vyžadována URL serveru Tika.",
"Tiktoken": "Tiktoken",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Aktualizujte více proměnných slotů po sobě stisknutím klávesy Tab ve vstupním poli konverzace po každé náhradě.",
"Title": "Název",
"Title (e.g. Tell me a fun fact)": "Název (např. Řekni mi zajímavost)",
"Title Auto-Generation": "Automatické generování názvu",
@ -1517,6 +1520,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "Pro výběr sad nástrojů zde je nejprve přidejte do pracovního prostoru \"Nástroje\".",
"Toast notifications for new updates": "Vyskakovací oznámení o nových aktualizacích",
"Today": "Dnes",
"Today at {{LOCALIZED_TIME}}": "",
"Toggle search": "Přepnout vyhledávání",
"Toggle settings": "Přepnout nastavení",
"Toggle sidebar": "Přepnout postranní panel",
@ -1657,6 +1661,7 @@
"Yacy Password": "Heslo pro Yacy",
"Yacy Username": "Uživatelské jméno pro Yacy",
"Yesterday": "Včera",
"Yesterday at {{LOCALIZED_TIME}}": "",
"You": "Vy",
"You are currently using a trial license. Please contact support to upgrade your license.": "V současné době používáte zkušební licenci. Pro upgrade licence prosím kontaktujte podporu.",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Můžete konverzovat s maximálně {{maxCount}} souborem (soubory) najednou.",
@ -1670,7 +1675,7 @@
"Your Account": "",
"Your account status is currently pending activation.": "Stav vašeho účtu aktuálně čeká na aktivaci.",
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Celý váš příspěvek půjde přímo vývojáři pluginu; Open WebUI si nebere žádné procento. Zvolená platforma pro financování však může mít vlastní poplatky.",
"Youtube": "YouTube",
"YouTube": "YouTube",
"Youtube Language": "Jazyk YouTube",
"Youtube Proxy URL": "Proxy URL pro YouTube"
}

Some files were not shown because too many files have changed in this diff Show more