Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
85dbdd23
Unverified
Commit
85dbdd23
authored
May 15, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
May 15, 2024
Browse files
Merge branch 'dev' into fix/share-chat-reactive-loop
parents
ea6f1a0e
ea812a3b
Changes
72
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1072 additions
and
561 deletions
+1072
-561
Dockerfile
Dockerfile
+28
-28
backend/main.py
backend/main.py
+5
-0
cypress/e2e/settings.cy.ts
cypress/e2e/settings.cy.ts
+2
-6
src/app.css
src/app.css
+13
-4
src/lib/components/ChangelogModal.svelte
src/lib/components/ChangelogModal.svelte
+1
-1
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+11
-11
src/lib/components/chat/Messages/CitationsModal.svelte
src/lib/components/chat/Messages/CitationsModal.svelte
+1
-1
src/lib/components/chat/Messages/Name.svelte
src/lib/components/chat/Messages/Name.svelte
+1
-1
src/lib/components/chat/Messages/ProfileImage.svelte
src/lib/components/chat/Messages/ProfileImage.svelte
+2
-2
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+38
-28
src/lib/components/chat/Messages/UserMessage.svelte
src/lib/components/chat/Messages/UserMessage.svelte
+77
-95
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+3
-3
src/lib/components/chat/ModelSelector/Selector.svelte
src/lib/components/chat/ModelSelector/Selector.svelte
+6
-6
src/lib/components/icons/MenuLines.svelte
src/lib/components/icons/MenuLines.svelte
+19
-0
src/lib/components/layout/Navbar.svelte
src/lib/components/layout/Navbar.svelte
+14
-49
src/lib/components/layout/Navbar/Menu.svelte
src/lib/components/layout/Navbar/Menu.svelte
+61
-88
src/lib/components/layout/Sidebar.svelte
src/lib/components/layout/Sidebar.svelte
+120
-167
src/lib/components/layout/Sidebar/ChatMenu.svelte
src/lib/components/layout/Sidebar/ChatMenu.svelte
+13
-1
src/lib/components/layout/Sidebar/UserMenu.svelte
src/lib/components/layout/Sidebar/UserMenu.svelte
+45
-70
src/lib/components/workspace/Documents.svelte
src/lib/components/workspace/Documents.svelte
+612
-0
No files found.
Dockerfile
View file @
85dbdd23
...
@@ -80,25 +80,25 @@ RUN mkdir -p $HOME/.cache/chroma
...
@@ -80,25 +80,25 @@ RUN mkdir -p $HOME/.cache/chroma
RUN
echo
-n
00000000-0000-0000-0000-000000000000
>
$HOME
/.cache/chroma/telemetry_user_id
RUN
echo
-n
00000000-0000-0000-0000-000000000000
>
$HOME
/.cache/chroma/telemetry_user_id
RUN if
[
"
$USE_OLLAMA
"
=
"true"
]
;
then
\
RUN if
[
"
$USE_OLLAMA
"
=
"true"
]
;
then
\
apt-get update
&&
\
apt-get update
&&
\
# Install pandoc and netcat
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
# for RAG OCR
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# install helper tools
# install helper tools
apt-get install -y --no-install-recommends curl && \
apt-get install -y --no-install-recommends curl && \
# install ollama
# install ollama
curl -fsSL https://ollama.com/install.sh | sh && \
curl -fsSL https://ollama.com/install.sh | sh && \
# cleanup
# cleanup
rm -rf /var/lib/apt/lists/*; \
rm -rf /var/lib/apt/lists/*; \
else \
else \
apt-get update && \
apt-get update && \
# Install pandoc and netcat
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
# for RAG OCR
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# cleanup
# cleanup
rm -rf /var/lib/apt/lists/*; \
rm -rf /var/lib/apt/lists/*; \
fi
fi
# install python dependencies
# install python dependencies
...
@@ -106,16 +106,16 @@ COPY ./backend/requirements.txt ./requirements.txt
...
@@ -106,16 +106,16 @@ COPY ./backend/requirements.txt ./requirements.txt
RUN
pip3
install
uv
&&
\
RUN
pip3
install
uv
&&
\
if
[
"
$USE_CUDA
"
=
"true"
]
;
then
\
if
[
"
$USE_CUDA
"
=
"true"
]
;
then
\
# If you use CUDA the whisper and embedding model will be downloaded on first use
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
else \
else \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
fi
fi
...
@@ -134,6 +134,6 @@ COPY ./backend .
...
@@ -134,6 +134,6 @@ COPY ./backend .
EXPOSE
8080
EXPOSE
8080
HEALTHCHECK
CMD curl --fail http://localhost:8080 || exit 1
HEALTHCHECK
CMD curl --fail http://localhost:8080
/health
|| exit 1
CMD
[ "bash", "start.sh"]
CMD
[ "bash", "start.sh"]
backend/main.py
View file @
85dbdd23
...
@@ -377,6 +377,11 @@ async def get_opensearch_xml():
...
@@ -377,6 +377,11 @@ async def get_opensearch_xml():
return
Response
(
content
=
xml_content
,
media_type
=
"application/xml"
)
return
Response
(
content
=
xml_content
,
media_type
=
"application/xml"
)
@
app
.
get
(
"/health"
)
async
def
healthcheck
():
return
{
"status"
:
True
}
app
.
mount
(
"/static"
,
StaticFiles
(
directory
=
STATIC_DIR
),
name
=
"static"
)
app
.
mount
(
"/static"
,
StaticFiles
(
directory
=
STATIC_DIR
),
name
=
"static"
)
app
.
mount
(
"/cache"
,
StaticFiles
(
directory
=
CACHE_DIR
),
name
=
"cache"
)
app
.
mount
(
"/cache"
,
StaticFiles
(
directory
=
CACHE_DIR
),
name
=
"cache"
)
...
...
cypress/e2e/settings.cy.ts
View file @
85dbdd23
...
@@ -15,12 +15,8 @@ describe('Settings', () => {
...
@@ -15,12 +15,8 @@ describe('Settings', () => {
cy
.
loginAdmin
();
cy
.
loginAdmin
();
// Visit the home page
// Visit the home page
cy
.
visit
(
'
/
'
);
cy
.
visit
(
'
/
'
);
// Open the sidebar if it is not already open
// Click on the user menu
cy
.
get
(
'
[aria-label="Open sidebar"]
'
).
then
(()
=>
{
cy
.
get
(
'
button[aria-label="User Menu"]
'
).
click
();
cy
.
get
(
'
button[id="sidebar-toggle-button"]
'
).
click
();
});
// Click on the profile link
cy
.
get
(
'
button
'
).
contains
(
adminUser
.
name
).
click
();
// Click on the settings link
// Click on the settings link
cy
.
get
(
'
button
'
).
contains
(
'
Settings
'
).
click
();
cy
.
get
(
'
button
'
).
contains
(
'
Settings
'
).
click
();
});
});
...
...
src/app.css
View file @
85dbdd23
...
@@ -83,11 +83,20 @@ select {
...
@@ -83,11 +83,20 @@ select {
display
:
none
;
display
:
none
;
}
}
.scrollbar-
none
:active::-webkit-scrollbar-thumb
,
.scrollbar-
hidden
:active::-webkit-scrollbar-thumb
,
.scrollbar-
none
:focus::-webkit-scrollbar-thumb
,
.scrollbar-
hidden
:focus::-webkit-scrollbar-thumb
,
.scrollbar-
none
:hover::-webkit-scrollbar-thumb
{
.scrollbar-
hidden
:hover::-webkit-scrollbar-thumb
{
visibility
:
visible
;
visibility
:
visible
;
}
}
.scrollbar-
none
::-webkit-scrollbar-thumb
{
.scrollbar-
hidden
::-webkit-scrollbar-thumb
{
visibility
:
hidden
;
visibility
:
hidden
;
}
}
.scrollbar-none
::-webkit-scrollbar
{
display
:
none
;
/* for Chrome, Safari and Opera */
}
.scrollbar-none
{
-ms-overflow-style
:
none
;
/* IE and Edge */
scrollbar-width
:
none
;
/* Firefox */
}
src/lib/components/ChangelogModal.svelte
View file @
85dbdd23
...
@@ -58,7 +58,7 @@
...
@@ -58,7 +58,7 @@
</div>
</div>
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
<div class=" overflow-y-scroll max-h-80 scrollbar-
none
">
<div class=" overflow-y-scroll max-h-80 scrollbar-
hidden
">
<div class="mb-3">
<div class="mb-3">
{#if changelog}
{#if changelog}
{#each Object.keys(changelog) as version}
{#each Object.keys(changelog) as version}
...
...
src/lib/components/chat/MessageInput.svelte
View file @
85dbdd23
<script lang="ts">
<script lang="ts">
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import { modelfiles, settings, showSidebar } from '$lib/stores';
import {
mobile,
modelfiles, settings, showSidebar } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import {
import {
...
@@ -412,7 +412,7 @@
...
@@ -412,7 +412,7 @@
{#if dragged}
{#if dragged}
<div
<div
class="fixed {$showSidebar
class="fixed {$showSidebar
? 'left-0
lg
:left-[260px]
lg
:w-[calc(100%-260px)]'
? 'left-0
md
:left-[260px]
md
:w-[calc(100%-260px)]'
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone"
id="dropzone"
role="region"
role="region"
...
@@ -428,9 +428,9 @@
...
@@ -428,9 +428,9 @@
</div>
</div>
{/if}
{/if}
<div class="fixed bottom-0 {$showSidebar ? 'left-0
lg
:left-[260px]' : 'left-0'} right-0">
<div class="fixed bottom-0 {$showSidebar ? 'left-0
md
:left-[260px]' : 'left-0'} right-0">
<div class="w-full">
<div class="w-full">
<div class="px-2.5
lg
:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="px-2.5
md
:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="flex flex-col max-w-5xl w-full">
<div class="flex flex-col max-w-5xl w-full">
<div class="relative">
<div class="relative">
{#if autoScroll === false && messages.length > 0}
{#if autoScroll === false && messages.length > 0}
...
@@ -535,7 +535,7 @@
...
@@ -535,7 +535,7 @@
</div>
</div>
<div class="bg-white dark:bg-gray-900">
<div class="bg-white dark:bg-gray-900">
<div class="max-w-6xl px-2.5
lg
:px-16 mx-auto inset-x-0">
<div class="max-w-6xl px-2.5
md
:px-16 mx-auto inset-x-0">
<div class=" pb-2">
<div class=" pb-2">
<input
<input
bind:this={filesInputElement}
bind:this={filesInputElement}
...
@@ -754,7 +754,7 @@
...
@@ -754,7 +754,7 @@
<textarea
<textarea
id="chat-textarea"
id="chat-textarea"
bind:this={chatTextAreaElement}
bind:this={chatTextAreaElement}
class="scrollbar-
none
dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
class="scrollbar-
hidden
dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
? ''
? ''
: ' pl-4'} rounded-xl resize-none h-[48px]"
: ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={chatInputPlaceholder !== ''
placeholder={chatInputPlaceholder !== ''
...
@@ -765,7 +765,7 @@
...
@@ -765,7 +765,7 @@
bind:value={prompt}
bind:value={prompt}
on:keypress={(e) => {
on:keypress={(e) => {
if (
if (
window.innerWidth > 1024
||
!$mobile
||
!(
!(
'ontouchstart' in window ||
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.maxTouchPoints > 0 ||
...
@@ -1046,12 +1046,12 @@
...
@@ -1046,12 +1046,12 @@
</div>
</div>
<style>
<style>
.scrollbar-
none
:active::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:active::-webkit-scrollbar-thumb,
.scrollbar-
none
:focus::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:focus::-webkit-scrollbar-thumb,
.scrollbar-
none
:hover::-webkit-scrollbar-thumb {
.scrollbar-
hidden
:hover::-webkit-scrollbar-thumb {
visibility: visible;
visibility: visible;
}
}
.scrollbar-
none
::-webkit-scrollbar-thumb {
.scrollbar-
hidden
::-webkit-scrollbar-thumb {
visibility: hidden;
visibility: hidden;
}
}
</style>
</style>
src/lib/components/chat/Messages/CitationsModal.svelte
View file @
85dbdd23
...
@@ -47,7 +47,7 @@
...
@@ -47,7 +47,7 @@
<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-6 pb-5 md:space-x-4">
<div
<div
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-
none
"
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-
hidden
"
>
>
{#each mergedDocuments as document, documentIdx}
{#each mergedDocuments as document, documentIdx}
<div class="flex flex-col w-full">
<div class="flex flex-col w-full">
...
...
src/lib/components/chat/Messages/Name.svelte
View file @
85dbdd23
<div class=" self-center font-bold mb-0.5
capitalize
line-clamp-1">
<div class=" self-center font-bold mb-0.5 line-clamp-1">
<slot />
<slot />
</div>
</div>
src/lib/components/chat/Messages/ProfileImage.svelte
View file @
85dbdd23
...
@@ -2,6 +2,6 @@
...
@@ -2,6 +2,6 @@
export let src = '/user.png';
export let src = '/user.png';
</script>
</script>
<div class=" mr-
4
">
<div class=" mr-
3
">
<img {src} class="
max-w-[28px]
object-cover rounded-full" alt="profile" draggable="false" />
<img {src} class="
w-8
object-cover rounded-full" alt="profile" draggable="false" />
</div>
</div>
src/lib/components/chat/Messages/ResponseMessage.svelte
View file @
85dbdd23
...
@@ -338,7 +338,7 @@
...
@@ -338,7 +338,7 @@
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
/>
/>
<div class="w-full overflow-hidden">
<div class="w-full overflow-hidden
pl-1
">
<Name>
<Name>
{#if message.model in modelfiles}
{#if message.model in modelfiles}
{modelfiles[message.model]?.title}
{modelfiles[message.model]?.title}
...
@@ -347,8 +347,10 @@
...
@@ -347,8 +347,10 @@
{/if}
{/if}
{#if message.timestamp}
{#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
<span
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
>
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
</span>
</span>
{/if}
{/if}
</Name>
</Name>
...
@@ -370,7 +372,7 @@
...
@@ -370,7 +372,7 @@
>
>
<div>
<div>
{#if edit === true}
{#if edit === true}
<div class="
w-full">
<div class="w-full
bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2
">
<textarea
<textarea
id="message-edit-{message.id}"
id="message-edit-{message.id}"
bind:this={editTextAreaElement}
bind:this={editTextAreaElement}
...
@@ -382,23 +384,25 @@
...
@@ -382,23 +384,25 @@
}}
}}
/>
/>
<div class=" mt-2 mb-1 flex justify-
c
en
ter
space-x-
2
text-sm font-medium">
<div class=" mt-2 mb-1 flex justify-en
d
space-x-
1.5
text-sm font-medium">
<button
<button
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
id="close-edit-message-button"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
on:click={() => {
on:click={() => {
e
ditMessage
ConfirmHandler
();
cancelE
ditMessage();
}}
}}
>
>
{$i18n.t('
Save
')}
{$i18n.t('
Cancel
')}
</button>
</button>
<button
<button
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
id="save-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
on:click={() => {
on:click={() => {
cancelE
ditMessage();
e
ditMessage
ConfirmHandler
();
}}
}}
>
>
{$i18n.t('
Cancel
')}
{$i18n.t('
Save
')}
</button>
</button>
</div>
</div>
</div>
</div>
...
@@ -490,47 +494,53 @@
...
@@ -490,47 +494,53 @@
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
>
>
{#if siblings.length > 1}
{#if siblings.length > 1}
<div class="flex self-center
min-w-fit
">
<div class="flex self-center">
<button
<button
class="self-center dark:hover:text-white hover:text-black transition"
class="self-center
p-1 hover:bg-black/5 dark:hover:bg-white/5
dark:hover:text-white hover:text-black
rounded-md
transition"
on:click={() => {
on:click={() => {
showPreviousMessage(message);
showPreviousMessage(message);
}}
}}
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="none"
fill="currentColor"
viewBox="0 0 24 24"
class="w-4 h-4"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
>
<path
<path
fill-rule="evenod
d"
stroke-linecap="roun
d"
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z
"
stroke-linejoin="round
"
clip-rule="evenodd
"
d="M15.75 19.5 8.25 12l7.5-7.5
"
/>
/>
</svg>
</svg>
</button>
</button>
<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
<div
{siblings.indexOf(message.id) + 1} / {siblings.length}
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
>
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
</div>
<button
<button
class="self-center dark:hover:text-white hover:text-black transition"
class="self-center
p-1 hover:bg-black/5 dark:hover:bg-white/5
dark:hover:text-white hover:text-black
rounded-md
transition"
on:click={() => {
on:click={() => {
showNextMessage(message);
showNextMessage(message);
}}
}}
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="none"
fill="currentColor"
viewBox="0 0 24 24"
class="w-4 h-4"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
>
<path
<path
fill-rule="evenod
d"
stroke-linecap="roun
d"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z
"
stroke-linejoin="round
"
clip-rule="evenodd
"
d="m8.25 4.5 7.5 7.5-7.5 7.5
"
/>
/>
</svg>
</svg>
</button>
</button>
...
...
src/lib/components/chat/Messages/UserMessage.svelte
View file @
85dbdd23
...
@@ -54,49 +54,20 @@
...
@@ -54,49 +54,20 @@
};
};
</script>
</script>
<div class=" flex w-full">
<div class=" flex w-full user-message">
<ProfileImage
src={message.user
? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ?? '/user.png'
: user?.profile_image_url ?? '/user.png'}
/>
<div class="w-full overflow-hidden">
<div class="w-full overflow-hidden">
<div class="user-message">
<Name>
{#if message.user}
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{:else}
{$i18n.t('You')}
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{/if}
{:else if $settings.showUsername}
{user.name}
{:else}
{$i18n.t('You')}
{/if}
{#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
</span>
{/if}
</Name>
</div>
<div
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
class="prose chat-{message.role} w-full max-w-full
flex flex-col justify-end
dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
>
>
{#if message.files}
{#if message.files}
<div class="m
y
-2.5 w-full flex overflow-x-auto gap-
2
flex-wrap">
<div class="m
t
-2.5
mb-1
w-full flex
flex-col justify-end
overflow-x-auto gap-
1
flex-wrap">
{#each message.files as file}
{#each message.files as file}
<div>
<div
class="self-end"
>
{#if file.type === 'image'}
{#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else if file.type === 'doc'}
{:else if file.type === 'doc'}
<button
<button
class="h-16 w-
[15rem]
flex items-center space-x-3 px-2.5 dark:bg-gray-
60
0 rounded-xl border border-gray-200 dark:border-none text-left"
class="h-16 w-
72
flex items-center space-x-3 px-2.5 dark:bg-gray-
85
0 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
type="button"
on:click={() => {
on:click={() => {
if (file?.url) {
if (file?.url) {
...
@@ -132,7 +103,7 @@
...
@@ -132,7 +103,7 @@
</button>
</button>
{:else if file.type === 'collection'}
{:else if file.type === 'collection'}
<button
<button
class="h-16 w-
[15rem]
flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
class="h-16 w-
72
flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
type="button"
>
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<div class="p-2.5 bg-red-400 text-white rounded-lg">
...
@@ -166,7 +137,7 @@
...
@@ -166,7 +137,7 @@
{/if}
{/if}
{#if edit === true}
{#if edit === true}
<div class=" w-full">
<div class=" w-full
bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2
">
<textarea
<textarea
id="message-edit-{message.id}"
id="message-edit-{message.id}"
bind:this={messageEditTextAreaElement}
bind:this={messageEditTextAreaElement}
...
@@ -190,81 +161,41 @@
...
@@ -190,81 +161,41 @@
}}
}}
/>
/>
<div class=" mt-2 mb-1 flex justify-
c
en
ter
space-x-
2
text-sm font-medium">
<div class=" mt-2 mb-1 flex justify-en
d
space-x-
1.5
text-sm font-medium">
<button
<button
id="
sav
e-edit-message-button"
id="
clos
e-edit-message-button"
class="px-4 py-2 bg-
emerald-7
00 hover:bg-
emerald
-8
0
0 text-gray-100 transition rounded-l
g
"
class="
px-4 py-2 bg-
gray-9
00 hover:bg-
gray
-8
5
0 text-gray-100 transition rounded-
3x
l"
on:click={() => {
on:click={() => {
e
ditMessage
ConfirmHandler
();
cancelE
ditMessage();
}}
}}
>
>
{$i18n.t('
Save & Submit
')}
{$i18n.t('
Cancel
')}
</button>
</button>
<button
<button
id="
clos
e-edit-message-button"
id="
sav
e-edit-message-button"
class="
px-4 py-2
hover:bg-gray-100 dark:bg-gray-800 dark:
hover:bg-gray-
7
00 text-gray-
700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600
rounded-l
g
"
class="px-4 py-2
bg-white
hover:bg-gray-
1
00 text-gray-
800 transition
rounded-
3x
l"
on:click={() => {
on:click={() => {
cancelE
ditMessage();
e
ditMessage
ConfirmHandler
();
}}
}}
>
>
{$i18n.t('
Cancel
')}
{$i18n.t('
Send
')}
</button>
</button>
</div>
</div>
</div>
</div>
{:else}
{:else}
<div class="w-full">
<div class="w-full">
<pre id="user-message">{message.content}</pre>
<div class="flex justify-end mb-2">
<div
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
class="rounded-3xl px-5 py-2 max-w-[90%] bg-gray-50 dark:bg-gray-850 {message.files
{#if siblings.length > 1}
? 'rounded-tr-lg'
<div class="flex self-center">
: ''}"
<button
>
class="self-center dark:hover:text-white hover:text-black transition"
<pre id="user-message">{message.content}</pre>
on:click={() => {
</div>
showPreviousMessage(message);
</div>
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
clip-rule="evenodd"
/>
</svg>
</button>
<div class="text-xs font-bold self-center dark:text-gray-100">
{siblings.indexOf(message.id) + 1} / {siblings.length}
</div>
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
<div class=" flex justify-end space-x-1 text-gray-700 dark:text-gray-500">
{#if !readOnly}
{#if !readOnly}
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
<button
...
@@ -340,6 +271,57 @@
...
@@ -340,6 +271,57 @@
</button>
</button>
</Tooltip>
</Tooltip>
{/if}
{/if}
{#if siblings.length > 1}
<div class="flex self-center">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
{/if}
</div>
</div>
</div>
</div>
{/if}
{/if}
...
...
src/lib/components/chat/ModelSelector.svelte
View file @
85dbdd23
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
import { Collapsible } from 'bits-ui';
import { Collapsible } from 'bits-ui';
import { setDefaultModels } from '$lib/apis/configs';
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { models, showSettings, settings, user
, mobile
} from '$lib/stores';
import { onMount, tick, getContext } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import Selector from './ModelSelector/Selector.svelte';
import Selector from './ModelSelector/Selector.svelte';
...
@@ -108,8 +108,8 @@
...
@@ -108,8 +108,8 @@
{/each}
{/each}
</div>
</div>
{#if showSetDefault}
{#if showSetDefault
&& !$mobile
}
<div class="
hidden md:absolute
text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div>
</div>
{/if}
{/if}
src/lib/components/chat/ModelSelector/Selector.svelte
View file @
85dbdd23
...
@@ -25,7 +25,7 @@
...
@@ -25,7 +25,7 @@
export let items = [{ value: 'mango', label: 'Mango' }];
export let items = [{ value: 'mango', label: 'Mango' }];
export let className = ' w-[3
2
rem]';
export let className = ' w-[3
0
rem]';
let show = false;
let show = false;
...
@@ -225,7 +225,7 @@
...
@@ -225,7 +225,7 @@
<hr class="border-gray-100 dark:border-gray-800" />
<hr class="border-gray-100 dark:border-gray-800" />
{/if}
{/if}
<div class="px-3 my-2 max-h-
72
overflow-y-auto scrollbar-
none
">
<div class="px-3 my-2 max-h-
64
overflow-y-auto scrollbar-
hidden
">
{#each filteredItems as item}
{#each filteredItems as item}
<button
<button
aria-label="model-item"
aria-label="model-item"
...
@@ -407,12 +407,12 @@
...
@@ -407,12 +407,12 @@
</DropdownMenu.Root>
</DropdownMenu.Root>
<style>
<style>
.scrollbar-
none
:active::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:active::-webkit-scrollbar-thumb,
.scrollbar-
none
:focus::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:focus::-webkit-scrollbar-thumb,
.scrollbar-
none
:hover::-webkit-scrollbar-thumb {
.scrollbar-
hidden
:hover::-webkit-scrollbar-thumb {
visibility: visible;
visibility: visible;
}
}
.scrollbar-
none
::-webkit-scrollbar-thumb {
.scrollbar-
hidden
::-webkit-scrollbar-thumb {
visibility: hidden;
visibility: hidden;
}
}
</style>
</style>
src/lib/components/icons/MenuLines.svelte
0 → 100644
View file @
85dbdd23
<script lang="ts">
export let className = 'size-5';
export let strokeWidth = '2';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
/>
</svg>
src/lib/components/layout/Navbar.svelte
View file @
85dbdd23
...
@@ -8,6 +8,7 @@
...
@@ -8,6 +8,7 @@
mobile,
mobile,
modelfiles,
modelfiles,
settings,
settings,
showArchivedChats,
showSettings,
showSettings,
showSidebar,
showSidebar,
user
user
...
@@ -20,6 +21,7 @@
...
@@ -20,6 +21,7 @@
import Menu from './Navbar/Menu.svelte';
import Menu from './Navbar/Menu.svelte';
import { page } from '$app/stores';
import { page } from '$app/stores';
import UserMenu from './Sidebar/UserMenu.svelte';
import UserMenu from './Sidebar/UserMenu.svelte';
import MenuLines from '../icons/MenuLines.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -40,7 +42,11 @@
...
@@ -40,7 +42,11 @@
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem]">
<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem]">
<div class="flex items-center w-full max-w-full">
<div class="flex items-center w-full max-w-full">
<div class="{$showSidebar ? 'md:hidden' : ''} mr-3 self-start flex flex-none items-center">
<div
class="{$showSidebar
? 'md:hidden'
: ''} mr-3 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
>
<button
<button
id="sidebar-toggle-button"
id="sidebar-toggle-button"
class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
...
@@ -49,20 +55,7 @@
...
@@ -49,20 +55,7 @@
}}
}}
>
>
<div class=" m-auto self-center">
<div class=" m-auto self-center">
<svg
<MenuLines />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
/>
</svg>
</div>
</div>
</button>
</button>
</div>
</div>
...
@@ -72,40 +65,10 @@
...
@@ -72,40 +65,10 @@
{/if}
{/if}
</div>
</div>
<div class="self-start flex flex-none items-center">
<div class="self-start flex flex-none items-center
text-gray-600 dark:text-gray-400
">
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
{#if !shareEnabled}
{#if shareEnabled}
<Tooltip content={$i18n.t('Settings')}>
<button
class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
id="open-settings-button"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</Tooltip>
{:else}
<Menu
<Menu
{chat}
{chat}
{shareEnabled}
{shareEnabled}
...
@@ -167,17 +130,19 @@
...
@@ -167,17 +130,19 @@
</button>
</button>
</Tooltip>
</Tooltip>
{#if
!$mobile &&
$user !== undefined}
{#if $user !== undefined}
<UserMenu
<UserMenu
className="max-w-[200px]"
role={$user.role}
role={$user.role}
on:show={(e) => {
on:show={(e) => {
if (e.detail === 'archived-chat') {
if (e.detail === 'archived-chat') {
//
showArchivedChats
Modal =
true;
showArchivedChats
.set(
true
)
;
}
}
}}
}}
>
>
<button
<button
class=" flex rounded-xl p-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
class=" flex rounded-xl p-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
aria-label="User Menu"
>
>
<div class=" self-center">
<div class=" self-center">
<img
<img
...
...
src/lib/components/layout/Navbar/Menu.svelte
View file @
85dbdd23
...
@@ -82,7 +82,7 @@
...
@@ -82,7 +82,7 @@
align="end"
align="end"
transition={flyAndScale}
transition={flyAndScale}
>
>
<DropdownMenu.Item
<!--
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
on:click={async () => {
on:click={async () => {
await showSettings.set(!$showSettings);
await showSettings.set(!$showSettings);
...
@@ -108,114 +108,87 @@
...
@@ -108,114 +108,87 @@
/>
/>
</svg>
</svg>
<div class="flex items-center">{$i18n.t('Settings')}</div>
<div class="flex items-center">{$i18n.t('Settings')}</div>
</DropdownMenu.Item>
</DropdownMenu.Item>
-->
{#if shareEnabled}
<DropdownMenu.Item
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
id="chat-share-button"
id="chat-share-button"
on:click={() => {
on:click={() => {
shareHandler();
shareHandler();
}}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
>
<svg
<path
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
viewBox="0 0 24 24"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
fill="currentColor"
clip-rule="evenodd"
class="size-4"
/>
>
</svg>
<path
<div class="flex items-center">{$i18n.t('Share')}</div>
fill-rule="evenodd"
</DropdownMenu.Item>
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
<!--
<DropdownMenu.Item
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
on:click={() => {
downloadHandler();
downloadHandler();
}}
}}
/> -->
/>
<DropdownMenu.Sub>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
<div class="flex items-center">{$i18n.t('Download')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-full rounded-lg 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-lg"
transition={flyAndScale}
sideOffset={8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadTxt();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadPdf();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags chatId={chat.id} />
</div>
<!-- <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
tagHandler();
}}
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="
2
"
stroke-width="
1.5
"
stroke="currentColor"
stroke="currentColor"
class="size-4"
class="size-4"
>
>
<path
<path
stroke-linecap="round"
stroke-linecap="round"
stroke-linejoin="round"
stroke-linejoin="round"
d="M
9.568 3H5
.25A2.25 2.25 0 0 0
3
5.25
v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z
"
d="M
3 16.5v2
.25A2.25 2.25 0 0 0 5.25
21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3
"
/>
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
</svg>
</svg>
<div class="flex items-center">Tag</div>
<div class="flex items-center">{$i18n.t('Download')}</div>
</DropdownMenu.Item> -->
</DropdownMenu.SubTrigger>
{/if}
<DropdownMenu.SubContent
class="w-full rounded-lg 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-lg"
transition={flyAndScale}
sideOffset={8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadTxt();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadPdf();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags chatId={chat.id} />
</div>
</DropdownMenu.Content>
</DropdownMenu.Content>
</div>
</div>
</Dropdown>
</Dropdown>
src/lib/components/layout/Sidebar.svelte
View file @
85dbdd23
...
@@ -8,7 +8,8 @@
...
@@ -8,7 +8,8 @@
chatId,
chatId,
tags,
tags,
showSidebar,
showSidebar,
mobile
mobile,
showArchivedChats
} from '$lib/stores';
} from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
...
@@ -33,7 +34,7 @@
...
@@ -33,7 +34,7 @@
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
import UserMenu from './Sidebar/UserMenu.svelte';
import UserMenu from './Sidebar/UserMenu.svelte';
const BREAKPOINT =
1024
;
const BREAKPOINT =
768
;
let show = false;
let show = false;
let navElement;
let navElement;
...
@@ -49,7 +50,6 @@
...
@@ -49,7 +50,6 @@
let chatTitleEditId = null;
let chatTitleEditId = null;
let chatTitle = '';
let chatTitle = '';
let showArchivedChatsModal = false;
let showShareChatModal = false;
let showShareChatModal = false;
let showDropdown = false;
let showDropdown = false;
let isEditing = false;
let isEditing = false;
...
@@ -76,7 +76,26 @@
...
@@ -76,7 +76,26 @@
}
}
});
});
mobile;
const onResize = () => {
if ($showSidebar && window.innerWidth < BREAKPOINT) {
showSidebar.set(false);
}
};
onMount(async () => {
onMount(async () => {
mobile.subscribe((e) => {
console.log(e);
if ($showSidebar && e) {
showSidebar.set(false);
}
if (!$showSidebar && !e) {
showSidebar.set(true);
}
});
showSidebar.set(window.innerWidth > BREAKPOINT);
showSidebar.set(window.innerWidth > BREAKPOINT);
await chats.set(await getChatList(localStorage.token));
await chats.set(await getChatList(localStorage.token));
...
@@ -106,20 +125,12 @@
...
@@ -106,20 +125,12 @@
checkDirection();
checkDirection();
};
};
const onResize = () => {
if ($showSidebar && window.innerWidth < BREAKPOINT) {
showSidebar.set(false);
}
};
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('resize', onResize);
return () => {
return () => {
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('resize', onResize);
};
};
});
});
...
@@ -186,7 +197,7 @@
...
@@ -186,7 +197,7 @@
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ArchivedChatsModal
<ArchivedChatsModal
bind:show={showArchivedChats
Modal
}
bind:show={
$
showArchivedChats}
on:change={async () => {
on:change={async () => {
await chats.set(await getChatList(localStorage.token));
await chats.set(await getChatList(localStorage.token));
}}
}}
...
@@ -206,8 +217,8 @@
...
@@ -206,8 +217,8 @@
<div
<div
bind:this={navElement}
bind:this={navElement}
id="sidebar"
id="sidebar"
class="h-screen max-h-[100dvh] min-h-screen {$showSidebar
class="h-screen max-h-[100dvh] min-h-screen
select-none
{$showSidebar
? '
lg
:relative w-[260px]'
? '
md
:relative w-[260px]'
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 rounded-r-2xl
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 rounded-r-2xl
"
"
data-state={$showSidebar}
data-state={$showSidebar}
...
@@ -217,34 +228,12 @@
...
@@ -217,34 +228,12 @@
? ''
? ''
: 'invisible'}"
: 'invisible'}"
>
>
<div class="px-2 flex justify-between space-x-2">
<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
<button
class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
on:click={() => {
showSidebar.set(!$showSidebar);
}}
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
/>
</svg>
</div>
</button>
<a
<a
id="sidebar-new-chat-button"
id="sidebar-new-chat-button"
class="flex justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
class="flex
flex-1
justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
href="/"
href="/"
draggable="false"
on:click={async () => {
on:click={async () => {
selectedChatId = null;
selectedChatId = null;
...
@@ -252,10 +241,24 @@
...
@@ -252,10 +241,24 @@
const newChatButton = document.getElementById('new-chat-button');
const newChatButton = document.getElementById('new-chat-button');
setTimeout(() => {
setTimeout(() => {
newChatButton?.click();
newChatButton?.click();
if ($mobile) {
showSidebar.set(false);
}
}, 0);
}, 0);
}}
}}
>
>
<div class="self-center">
<div class="self-center mx-1.5">
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
class=" size-6 -translate-x-1.5 rounded-full"
alt="logo"
/>
</div>
<div class=" self-center font-medium text-sm text-gray-850 dark:text-white">
{$i18n.t('New Chat')}
</div>
<div class="self-center ml-auto">
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
viewBox="0 0 20 20"
...
@@ -271,17 +274,42 @@
...
@@ -271,17 +274,42 @@
</svg>
</svg>
</div>
</div>
</a>
</a>
<button
class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
on:click={() => {
showSidebar.set(!$showSidebar);
}}
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
/>
</svg>
</div>
</button>
</div>
</div>
{#if $user?.role === 'admin'}
{#if $user?.role === 'admin'}
<div class="px-2 flex justify-center
mt-0.5
">
<div class="px-2
.5
flex justify-center
text-gray-800 dark:text-gray-200
">
<a
<a
class="flex-grow flex space-x-3 rounded-xl px-
3
.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-
2
.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/
modelfiles
"
href="/
workspace
"
on:click={() => {
on:click={() => {
selectedChatId = null;
selectedChatId = null;
chatId.set('');
chatId.set('');
}}
}}
draggable="false"
>
>
<div class="self-center">
<div class="self-center">
<svg
<svg
...
@@ -290,7 +318,7 @@
...
@@ -290,7 +318,7 @@
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2"
stroke="currentColor"
stroke="currentColor"
class="
w-4 h-4
"
class="
size-[1.1rem]
"
>
>
<path
<path
stroke-linecap="round"
stroke-linecap="round"
...
@@ -301,71 +329,7 @@
...
@@ -301,71 +329,7 @@
</div>
</div>
<div class="flex self-center">
<div class="flex self-center">
<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Workspace')}</div>
</div>
</a>
</div>
<div class="px-2 flex justify-center">
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/prompts"
on:click={() => {
selectedChatId = null;
chatId.set('');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
/>
</svg>
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
</div>
</a>
</div>
<div class="px-2 flex justify-center mb-1">
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/documents"
on:click={() => {
selectedChatId = null;
chatId.set('');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
/>
</svg>
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
</div>
</div>
</a>
</a>
</div>
</div>
...
@@ -415,9 +379,9 @@
...
@@ -415,9 +379,9 @@
</div>
</div>
{/if}
{/if}
<div class="px-2 mt-
1
mb-2 flex justify-center space-x-2">
<div class="px-2 mt-
0.5
mb-2 flex justify-center space-x-2">
<div class="flex w-full" id="chat-search">
<div class="flex w-full
rounded-xl
" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l-xl bg-
white dark:bg-gray-950
">
<div class="self-center pl-3 py-2 rounded-l-xl bg-
transparent
">
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
viewBox="0 0 20 20"
...
@@ -433,7 +397,7 @@
...
@@ -433,7 +397,7 @@
</div>
</div>
<input
<input
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm dark:text-gray-300
dark:bg-gray-950
outline-none"
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm
bg-transparent
dark:text-gray-300 outline-none"
placeholder={$i18n.t('Search')}
placeholder={$i18n.t('Search')}
bind:value={search}
bind:value={search}
on:focus={() => {
on:focus={() => {
...
@@ -444,9 +408,9 @@
...
@@ -444,9 +408,9 @@
</div>
</div>
{#if $tags.length > 0}
{#if $tags.length > 0}
<div class="px-2.5
mt-0.5
mb-2 flex gap-1 flex-wrap">
<div class="px-2.5 mb-2 flex gap-1 flex-wrap">
<button
<button
class="px-2.5 text-xs font-medium bg-gray-
10
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-
5
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
on:click={async () => {
on:click={async () => {
await chats.set(await getChatList(localStorage.token));
await chats.set(await getChatList(localStorage.token));
}}
}}
...
@@ -455,7 +419,7 @@
...
@@ -455,7 +419,7 @@
</button>
</button>
{#each $tags as tag}
{#each $tags as tag}
<button
<button
class="px-2.5 text-xs font-medium bg-gray-
10
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-
5
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
on:click={async () => {
on:click={async () => {
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
if (chatIds.length === 0) {
...
@@ -471,7 +435,7 @@
...
@@ -471,7 +435,7 @@
</div>
</div>
{/if}
{/if}
<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-
none
">
<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-
hidden
">
{#each filteredChatList as chat, idx}
{#each filteredChatList as chat, idx}
{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
<div
<div
...
@@ -526,7 +490,7 @@
...
@@ -526,7 +490,7 @@
href="/c/{chat.id}"
href="/c/{chat.id}"
on:click={() => {
on:click={() => {
selectedChatId = chat.id;
selectedChatId = chat.id;
if (
window.innerWidth < 1024
) {
if (
$mobile
) {
showSidebar.set(false);
showSidebar.set(false);
}
}
}}
}}
...
@@ -641,6 +605,9 @@
...
@@ -641,6 +605,9 @@
shareChatId = selectedChatId;
shareChatId = selectedChatId;
showShareChatModal = true;
showShareChatModal = true;
}}
}}
archiveChatHandler={() => {
archiveChatHandler(chat.id);
}}
renameHandler={() => {
renameHandler={() => {
chatTitle = chat.title;
chatTitle = chat.title;
chatTitleEditId = chat.id;
chatTitleEditId = chat.id;
...
@@ -672,18 +639,6 @@
...
@@ -672,18 +639,6 @@
</button>
</button>
</ChatMenu>
</ChatMenu>
<Tooltip content={$i18n.t('Archive')}>
<button
aria-label="Archive"
class=" self-center dark:hover:text-white transition"
on:click={() => {
archiveChatHandler(chat.id);
}}
>
<ArchiveBox />
</button>
</Tooltip>
{#if chat.id === $chatId}
{#if chat.id === $chatId}
<button
<button
id="delete-chat-button"
id="delete-chat-button"
...
@@ -712,43 +667,41 @@
...
@@ -712,43 +667,41 @@
</div>
</div>
</div>
</div>
{#if $mobile}
<div class="px-2.5">
<div class="px-2.5">
<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
<div class="flex flex-col">
<div class="flex flex-col">
{#if $user !== undefined}
{#if $user !== undefined}
<UserMenu
<UserMenu
role={$user.role}
role={$user.role}
on:show={(e) => {
on:show={(e) => {
if (e.detail === 'archived-chat') {
if (e.detail === 'archived-chat') {
showArchivedChats.set(true);
showArchivedChatsModal = true;
}
}
}}
>
<button
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
on:click={() => {
showDropdown = !showDropdown;
}}
}}
>
>
<button
<div class=" self-center mr-3">
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
<img
on:click={() => {
src={$user.profile_image_url}
showDropdown = !showDropdown;
class=" max-w-[30px] object-cover rounded-full"
}}
alt="User profile"
>
/>
<div class=" self-center mr-3">
</div>
<img
<div class=" self-center font-semibold">{$user.name}</div>
src={$user.profile_image_url}
</button>
class=" max-w-[30px] object-cover rounded-full"
</UserMenu>
alt="User profile"
{/if}
/>
</div>
<div class=" self-center font-semibold">{$user.name}</div>
</button>
</UserMenu>
{/if}
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
<div
<!--
<div
id="sidebar-handle"
id="sidebar-handle"
class=" hidden md:fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
class=" hidden md:fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
>
>
...
@@ -783,16 +736,16 @@
...
@@ -783,16 +736,16 @@
</span>
</span>
</button>
</button>
</Tooltip>
</Tooltip>
</div>
</div>
-->
</div>
</div>
<style>
<style>
.scrollbar-
none
:active::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:active::-webkit-scrollbar-thumb,
.scrollbar-
none
:focus::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:focus::-webkit-scrollbar-thumb,
.scrollbar-
none
:hover::-webkit-scrollbar-thumb {
.scrollbar-
hidden
:hover::-webkit-scrollbar-thumb {
visibility: visible;
visibility: visible;
}
}
.scrollbar-
none
::-webkit-scrollbar-thumb {
.scrollbar-
hidden
::-webkit-scrollbar-thumb {
visibility: hidden;
visibility: hidden;
}
}
</style>
</style>
src/lib/components/layout/Sidebar/ChatMenu.svelte
View file @
85dbdd23
...
@@ -9,10 +9,12 @@
...
@@ -9,10 +9,12 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
export let shareHandler: Function;
export let shareHandler: Function;
export let archiveChatHandler: Function;
export let renameHandler: Function;
export let renameHandler: Function;
export let deleteHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
export let onClose: Function;
...
@@ -36,7 +38,7 @@
...
@@ -36,7 +38,7 @@
<div slot="content">
<div slot="content">
<DropdownMenu.Content
<DropdownMenu.Content
class="w-full max-w-[1
8
0px] 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"
class="w-full max-w-[1
6
0px] 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"
sideOffset={-2}
sideOffset={-2}
side="bottom"
side="bottom"
align="start"
align="start"
...
@@ -62,6 +64,16 @@
...
@@ -62,6 +64,16 @@
<div class="flex items-center">{$i18n.t('Rename')}</div>
<div class="flex items-center">{$i18n.t('Rename')}</div>
</DropdownMenu.Item>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-800 rounded-md"
on:click={() => {
archiveChatHandler();
}}
>
<ArchiveBox strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Archive')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-800 rounded-md"
on:click={() => {
on:click={() => {
...
...
src/lib/components/layout/Sidebar/UserMenu.svelte
View file @
85dbdd23
...
@@ -12,6 +12,7 @@
...
@@ -12,6 +12,7 @@
export let show = false;
export let show = false;
export let role = '';
export let role = '';
export let className = 'max-w-[240px]';
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
</script>
</script>
...
@@ -28,81 +29,14 @@
...
@@ -28,81 +29,14 @@
<slot name="content">
<slot name="content">
<DropdownMenu.Content
<DropdownMenu.Content
class="w-full
max-w-[240px]
rounded-l
g
p-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-gray-850 text-white
text-sm
"
class="w-full
{className} text-sm
rounded-
x
l p
x
-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
"
sideOffset={8}
sideOffset={8}
side="bottom"
side="bottom"
align="start"
align="start"
transition={(e) => fade(e, { duration: 100 })}
transition={(e) => fade(e, { duration: 100 })}
>
>
{#if role === 'admin'}
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
goto('/admin');
show = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
</button>
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
goto('/playground');
show = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
</button>
{/if}
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
dispatch('show', 'archived-chat');
show = false;
}}
>
<div class=" self-center mr-3">
<ArchiveBox className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
</button>
<button
<button
class="flex rounded-md py-2
.5
px-3
.5
w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={async () => {
on:click={async () => {
await showSettings.set(true);
await showSettings.set(true);
show = false;
show = false;
...
@@ -132,10 +66,51 @@
...
@@ -132,10 +66,51 @@
<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
</button>
</button>
<button
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
dispatch('show', 'archived-chat');
show = false;
}}
>
<div class=" self-center mr-3">
<ArchiveBox className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
</button>
{#if role === 'admin'}
<button
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
goto('/admin');
show = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
</button>
{/if}
<hr class=" dark:border-gray-800 my-2 p-0" />
<hr class=" dark:border-gray-800 my-2 p-0" />
<button
<button
class="flex rounded-md py-2
.5
px-3
.5
w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
localStorage.removeItem('token');
localStorage.removeItem('token');
location.href = '/auth';
location.href = '/auth';
...
...
src/lib/components/workspace/Documents.svelte
0 → 100644
View file @
85dbdd23
<script lang="ts">
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, documents } from '$lib/stores';
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
import { uploadDocToVectorDB } from '$lib/apis/rag';
import { transformFileName } from '$lib/utils';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
const i18n = getContext('i18n');
let importFiles = '';
let inputFiles = '';
let query = '';
let documentsImportInputElement: HTMLInputElement;
let tags = [];
let showSettingsModal = false;
let showAddDocModal = false;
let showEditDocModal = false;
let selectedDoc;
let selectedTag = '';
let dragged = false;
const deleteDoc = async (name) => {
await deleteDocByName(localStorage.token, name);
await documents.set(await getDocs(localStorage.token));
};
const deleteDocs = async (docs) => {
const res = await Promise.all(
docs.map(async (doc) => {
return await deleteDocByName(localStorage.token, doc.name);
})
);
await documents.set(await getDocs(localStorage.token));
};
const uploadDoc = async (file) => {
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
await createNewDoc(
localStorage.token,
res.collection_name,
res.filename,
transformFileName(res.filename),
res.filename
).catch((error) => {
toast.error(error);
return null;
});
await documents.set(await getDocs(localStorage.token));
}
};
onMount(() => {
documents.subscribe((docs) => {
tags = docs.reduce((a, e, i, arr) => {
return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
}, []);
});
const dropZone = document.querySelector('body');
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) {
let reader = new FileReader();
reader.onload = (event) => {
files = [
...files,
{
type: 'image',
url: `${event.target.result}`
}
];
};
const inputFiles = e.dataTransfer?.files;
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
console.log(file, file.name.split('.').at(-1));
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else {
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
);
uploadDoc(file);
}
}
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave);
return () => {
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
};
});
let filteredDocs;
$: filteredDocs = $documents.filter(
(doc) =>
(selectedTag === '' ||
(doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) &&
(query === '' || doc.name.includes(query))
);
</script>
<svelte:head>
<title>
{$i18n.t('Documents')} | {$WEBUI_NAME}
</title>
</svelte:head>
{#if dragged}
<div
class="fixed 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 rounded-xl w-full h-full backdrop-blur 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}
{#key selectedDoc}
<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
{/key}
<AddDocModal bind:show={showAddDocModal} />
<SettingsModal bind:show={showSettingsModal} />
<div class="mb-3">
<div class="flex justify-between items-center">
<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
<div>
<button
class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition"
type="button"
on:click={() => {
showSettingsModal = !showSettingsModal;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
clip-rule="evenodd"
/>
</svg>
<div class=" text-xs">{$i18n.t('Document Settings')}</div>
</button>
</div>
</div>
<div class=" text-gray-500 text-xs mt-1">
ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Documents')}
/>
</div>
<div>
<button
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
on:click={() => {
showAddDocModal = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
</div>
<!-- <div>
<div
class="my-3 py-16 rounded-lg border-2 border-dashed dark:border-gray-600 {dragged &&
' dark:bg-gray-700'} "
role="region"
on:drop={onDrop}
on:dragover={onDragOver}
on:dragleave={onDragLeave}
>
<div class=" pointer-events-none">
<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents
</div>
</div>
</div>
</div> -->
<hr class=" dark:border-gray-850 my-2.5" />
{#if tags.length > 0}
<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
<div class="ml-0.5 pr-3 my-auto flex items-center">
<Checkbox
state={filteredDocs.filter((doc) => doc?.selected === 'checked').length ===
filteredDocs.length
? 'checked'
: 'unchecked'}
indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 &&
filteredDocs.filter((doc) => doc?.selected === 'checked').length !== filteredDocs.length}
on:change={(e) => {
if (e.detail === 'checked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' }));
} else if (e.detail === 'unchecked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' }));
}
}}
/>
</div>
{#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('all')}</div>
</button>
{#each tags as tag}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = tag;
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
#{tag}
</div>
</button>
{/each}
{:else}
<div class="flex-1 flex w-full justify-between items-center">
<div class="text-xs font-medium py-0.5 self-center mr-1">
{filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected
</div>
<div class="flex gap-1">
<!-- <button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">add tags</div>
</button> -->
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked'));
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
{$i18n.t('delete')}
</div>
</button>
</div>
</div>
{/if}
</div>
{/if}
<div class="my-3 mb-5">
{#each filteredDocs as doc}
<button
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
on:click={() => {
if (doc?.selected === 'checked') {
doc.selected = 'unchecked';
} else {
doc.selected = 'checked';
}
}}
>
<div class="my-auto flex items-center">
<Checkbox state={doc?.selected ?? 'unchecked'} />
</div>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<div class=" flex items-center space-x-3">
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if doc}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else}
<svg
class=" w-6 h-6 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
{/if}
</div>
<div class=" self-center flex-1">
<div class=" font-bold line-clamp-1">#{doc.name} ({doc.filename})</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{doc.title}
</div>
</div>
</div>
</div>
<div class="flex flex-row space-x-1 self-center">
<button
class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={async (e) => {
e.stopPropagation();
showEditDocModal = !showEditDocModal;
selectedDoc = doc;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<!-- <button
class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
type="button"
on:click={() => {
console.log('download file');
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</button> -->
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={(e) => {
e.stopPropagation();
deleteDoc(doc.name);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div>
</button>
{/each}
</div>
<div class=" flex justify-end w-full mb-2">
<div class="flex space-x-2">
<input
id="documents-import-input"
bind:this={documentsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
const reader = new FileReader();
reader.onload = async (event) => {
const savedDocs = JSON.parse(event.target.result);
console.log(savedDocs);
for (const doc of savedDocs) {
await createNewDoc(
localStorage.token,
doc.collection_name,
doc.filename,
doc.name,
doc.title
).catch((error) => {
toast.error(error);
return null;
});
}
await documents.set(await getDocs(localStorage.token));
};
reader.readAsText(importFiles[0]);
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
documentsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Documents Mapping')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
let blob = new Blob([JSON.stringify($documents)], {
type: 'application/json'
});
saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Documents Mapping')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
</div>
Prev
1
2
3
4
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment