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
7753851e
Unverified
Commit
7753851e
authored
Dec 30, 2023
by
Timothy Jaeryang Baek
Committed by
GitHub
Dec 30, 2023
Browse files
Merge branch 'main' into fix-openai-settings
parents
b919ac76
13881bee
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
1122 additions
and
916 deletions
+1122
-916
backend/apps/web/routers/chats.py
backend/apps/web/routers/chats.py
+20
-0
src/lib/apis/chats/index.ts
src/lib/apis/chats/index.ts
+32
-0
src/lib/components/chat/Messages.svelte
src/lib/components/chat/Messages.svelte
+49
-788
src/lib/components/chat/Messages/Name.svelte
src/lib/components/chat/Messages/Name.svelte
+3
-0
src/lib/components/chat/Messages/Placeholder.svelte
src/lib/components/chat/Messages/Placeholder.svelte
+71
-0
src/lib/components/chat/Messages/ProfileImage.svelte
src/lib/components/chat/Messages/ProfileImage.svelte
+7
-0
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+537
-0
src/lib/components/chat/Messages/Skeleton.svelte
src/lib/components/chat/Messages/Skeleton.svelte
+19
-0
src/lib/components/chat/Messages/UserMessage.svelte
src/lib/components/chat/Messages/UserMessage.svelte
+195
-0
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+4
-3
src/lib/components/chat/SettingsModal.svelte
src/lib/components/chat/SettingsModal.svelte
+137
-123
src/lib/components/chat/ShortcutsModal.svelte
src/lib/components/chat/ShortcutsModal.svelte
+17
-0
src/routes/(app)/+layout.svelte
src/routes/(app)/+layout.svelte
+7
-0
src/routes/(app)/+page.svelte
src/routes/(app)/+page.svelte
+12
-1
src/routes/(app)/c/[id]/+page.svelte
src/routes/(app)/c/[id]/+page.svelte
+12
-1
static/ollama-dark.png
static/ollama-dark.png
+0
-0
static/ollama.png
static/ollama.png
+0
-0
No files found.
backend/apps/web/routers/chats.py
View file @
7753851e
...
@@ -159,3 +159,23 @@ async def delete_chat_by_id(id: str, cred=Depends(bearer_scheme)):
...
@@ -159,3 +159,23 @@ async def delete_chat_by_id(id: str, cred=Depends(bearer_scheme)):
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
INVALID_TOKEN
,
detail
=
ERROR_MESSAGES
.
INVALID_TOKEN
,
)
)
############################
# DeleteAllChats
############################
@
router
.
delete
(
"/"
,
response_model
=
bool
)
async
def
delete_all_user_chats
(
cred
=
Depends
(
bearer_scheme
)):
token
=
cred
.
credentials
user
=
Users
.
get_user_by_token
(
token
)
if
user
:
result
=
Chats
.
delete_chats_by_user_id
(
user
.
id
)
return
result
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
INVALID_TOKEN
,
)
src/lib/apis/chats/index.ts
View file @
7753851e
...
@@ -191,3 +191,35 @@ export const deleteChatById = async (token: string, id: string) => {
...
@@ -191,3 +191,35 @@ export const deleteChatById = async (token: string, id: string) => {
return
res
;
return
res
;
};
};
export
const
deleteAllChats
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/chats/`
,
{
method
:
'
DELETE
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
...(
token
&&
{
authorization
:
`Bearer
${
token
}
`
})
}
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
then
((
json
)
=>
{
return
json
;
})
.
catch
((
err
)
=>
{
error
=
err
;
console
.
log
(
err
);
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
src/lib/components/chat/Messages.svelte
View file @
7753851e
<script lang="ts">
<script lang="ts">
import
{
marked
}
from
'
marked
'
;
import { v4 as uuidv4 } from 'uuid';
import { v4 as uuidv4 } from 'uuid';
import
tippy
from
'
tippy.js
'
;
import
hljs
from
'
highlight.js
'
;
import
'
highlight.js/styles/github-dark.min.css
'
;
import
auto_render
from
'
katex/dist/contrib/auto-render.mjs
'
;
import
'
katex/dist/katex.min.css
'
;
import { chats, config, db, modelfiles, settings, user } from '$lib/stores';
import { chats, config, db, modelfiles, settings, user } from '$lib/stores';
import { tick } from 'svelte';
import { tick } from 'svelte';
...
@@ -14,6 +7,10 @@
...
@@ -14,6 +7,10 @@
import toast from 'svelte-french-toast';
import toast from 'svelte-french-toast';
import { getChatList, updateChatById } from '$lib/apis/chats';
import { getChatList, updateChatById } from '$lib/apis/chats';
import UserMessage from './Messages/UserMessage.svelte';
import ResponseMessage from './Messages/ResponseMessage.svelte';
import Placeholder from './Messages/Placeholder.svelte';
export let chatId = '';
export let chatId = '';
export let sendPrompt: Function;
export let sendPrompt: Function;
export let regenerateResponse: Function;
export let regenerateResponse: Function;
...
@@ -24,56 +21,7 @@
...
@@ -24,56 +21,7 @@
export let history = {};
export let history = {};
export let messages = [];
export let messages = [];
export
let
selectedModelfile
=
null
;
export let selectedModelfiles = [];
$
:
if
(
messages
&&
messages
.
length
>
0
&&
(
messages
.
at
(
-
1
).
done
??
false
))
{
(
async
()
=>
{
await
tick
();
[...
document
.
querySelectorAll
(
'
*
'
)].
forEach
((
node
)
=>
{
if
(
node
.
_tippy
)
{
node
.
_tippy
.
destroy
();
}
});
console
.
log
(
'
rendering message
'
);
renderLatex
();
hljs
.
highlightAll
();
createCopyCodeBlockButton
();
for
(
const
message
of
messages
)
{
if
(
message
.
info
)
{
console
.
log
(
message
);
tippy
(
`#info-
${
message
.
id
}
`
,
{
content
:
`<span class="text-xs" id="tooltip-
${
message
.
id
}
">token/s:
${
`
${
Math
.
round
(
((
message
.
info
.
eval_count
??
0
)
/
(
message
.
info
.
eval_duration
/
1000000000
))
*
100
)
/
100
}
tokens
` ?? 'N/A'
}<br/>
total_duration:
${
Math
.
round
(((
message
.
info
.
total_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
br
/>
load_duration
:
$
{
Math
.
round
(((
message
.
info
.
load_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
br
/>
prompt_eval_count
:
$
{
message
.
info
.
prompt_eval_count
??
'
N/A
'
}
<
br
/>
prompt_eval_duration
:
$
{
Math
.
round
(((
message
.
info
.
prompt_eval_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
br
/>
eval_count
:
$
{
message
.
info
.
eval_count
??
'
N/A
'
}
<
br
/>
eval_duration
:
$
{
Math
.
round
(((
message
.
info
.
eval_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
/span>`
,
allowHTML
:
true
});
}
}
})();
}
$: if (autoScroll && bottomPadding) {
$: if (autoScroll && bottomPadding) {
(async () => {
(async () => {
...
@@ -82,95 +30,6 @@
...
@@ -82,95 +30,6 @@
})();
})();
}
}
const
speakMessage
=
(
message
)
=>
{
const
speak
=
new
SpeechSynthesisUtterance
(
message
);
speechSynthesis
.
speak
(
speak
);
};
const
createCopyCodeBlockButton
=
()
=>
{
// use a class selector if available
let
blocks
=
document
.
querySelectorAll
(
'
pre
'
);
blocks
.
forEach
((
block
)
=>
{
// only add button if browser supports Clipboard API
if
(
block
.
childNodes
.
length
<
2
&&
block
.
id
!==
'
user-message
'
)
{
let
code
=
block
.
querySelector
(
'
code
'
);
code
.
style
.
borderTopRightRadius
=
0
;
code
.
style
.
borderTopLeftRadius
=
0
;
let
topBarDiv
=
document
.
createElement
(
'
div
'
);
topBarDiv
.
style
.
backgroundColor
=
'
#202123
'
;
topBarDiv
.
style
.
overflowX
=
'
auto
'
;
topBarDiv
.
style
.
display
=
'
flex
'
;
topBarDiv
.
style
.
justifyContent
=
'
space-between
'
;
topBarDiv
.
style
.
padding
=
'
0 1rem
'
;
topBarDiv
.
style
.
paddingTop
=
'
4px
'
;
topBarDiv
.
style
.
borderTopRightRadius
=
'
8px
'
;
topBarDiv
.
style
.
borderTopLeftRadius
=
'
8px
'
;
let
langDiv
=
document
.
createElement
(
'
div
'
);
let
codeClassNames
=
code
?.
className
.
split
(
'
'
);
langDiv
.
textContent
=
codeClassNames
[
0
]
===
'
hljs
'
?
codeClassNames
[
1
].
slice
(
9
)
:
codeClassNames
[
0
].
slice
(
9
);
langDiv
.
style
.
color
=
'
white
'
;
langDiv
.
style
.
margin
=
'
4px
'
;
langDiv
.
style
.
fontSize
=
'
0.75rem
'
;
let
button
=
document
.
createElement
(
'
button
'
);
button
.
className
=
'
copy-code-button
'
;
button
.
textContent
=
'
Copy Code
'
;
button
.
style
.
background
=
'
none
'
;
button
.
style
.
fontSize
=
'
0.75rem
'
;
button
.
style
.
border
=
'
none
'
;
button
.
style
.
margin
=
'
4px
'
;
button
.
style
.
cursor
=
'
pointer
'
;
button
.
style
.
color
=
'
#ddd
'
;
button
.
addEventListener
(
'
click
'
,
()
=>
copyCode
(
block
,
button
));
topBarDiv
.
appendChild
(
langDiv
);
topBarDiv
.
appendChild
(
button
);
block
.
prepend
(
topBarDiv
);
}
});
async
function
copyCode
(
block
,
button
)
{
let
code
=
block
.
querySelector
(
'
code
'
);
let
text
=
code
.
innerText
;
await
copyToClipboard
(
text
);
// visual feedback that task is completed
button
.
innerText
=
'
Copied!
'
;
setTimeout
(()
=>
{
button
.
innerText
=
'
Copy Code
'
;
},
1000
);
}
};
const
renderLatex
=
()
=>
{
let
chatMessageElements
=
document
.
getElementsByClassName
(
'
chat-assistant
'
);
// let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];
for
(
const
element
of
chatMessageElements
)
{
auto_render
(
element
,
{
// customised options
// • auto-render specific keys, e.g.:
delimiters
:
[
{
left
:
'
$$
'
,
right
:
'
$$
'
,
display
:
true
},
// { left: '$', right: '$', display: false },
{
left
:
'
\\
(
'
,
right
:
'
\\
)
'
,
display
:
true
},
{
left
:
'
\\
[
'
,
right
:
'
\\
]
'
,
display
:
true
}
],
// • rendering keys, e.g.:
throwOnError
:
false
});
}
};
const copyToClipboard = (text) => {
const copyToClipboard = (text) => {
if (!navigator.clipboard) {
if (!navigator.clipboard) {
var textArea = document.createElement('textarea');
var textArea = document.createElement('textarea');
...
@@ -207,24 +66,8 @@
...
@@ -207,24 +66,8 @@
);
);
};
};
const
editMessageHandler
=
async
(
messageId
)
=>
{
const confirmEditMessage = async (messageId, content) => {
// let editMessage = history.messages[messageId];
let userPrompt = content;
history
.
messages
[
messageId
].
edit
=
true
;
history
.
messages
[
messageId
].
originalContent
=
history
.
messages
[
messageId
].
content
;
history
.
messages
[
messageId
].
editedContent
=
history
.
messages
[
messageId
].
content
;
await
tick
();
const
editElement
=
document
.
getElementById
(
`message-edit-
${
messageId
}
`
);
editElement
.
style
.
height
=
''
;
editElement
.
style
.
height
=
`
${
editElement
.
scrollHeight
}
px`
;
};
const
confirmEditMessage
=
async
(
messageId
)
=>
{
history
.
messages
[
messageId
].
edit
=
false
;
let
userPrompt
=
history
.
messages
[
messageId
].
editedContent
;
let userMessageId = uuidv4();
let userMessageId = uuidv4();
let userMessage = {
let userMessage = {
...
@@ -252,25 +95,23 @@
...
@@ -252,25 +95,23 @@
await sendPrompt(userPrompt, userMessageId, chatId);
await sendPrompt(userPrompt, userMessageId, chatId);
};
};
const
confirmEditResponseMessage
=
async
(
messageId
)
=>
{
const confirmEditResponseMessage = async (messageId, content) => {
history
.
messages
[
messageId
].
edit
=
false
;
history.messages[messageId].originalContent = history.messages[messageId].content;
history
.
messages
[
messageId
].
content
=
history
.
messages
[
messageId
].
editedContent
;
history.messages[messageId].content = content;
};
const
cancelEditMessage
=
(
messageId
)
=>
{
await tick();
history
.
messages
[
messageId
].
edit
=
false
;
history
.
messages
[
messageId
].
editedContent
=
undefined
;
};
const
rateMessage
=
async
(
messageIdx
,
rating
)
=>
{
await updateChatById(localStorage.token, chatId, {
// TODO: Move this function to parent
messages: messages,
messages
=
messages
.
map
((
message
,
idx
)
=>
{
history: history
if
(
messageIdx
===
idx
)
{
message
.
rating
=
rating
;
}
return
message
;
});
});
await chats.set(await getChatList(localStorage.token));
};
const rateMessage = async (messageId, rating) => {
history.messages[messageId].rating = rating;
await tick();
await updateChatById(localStorage.token, chatId, {
await updateChatById(localStorage.token, chatId, {
messages: messages,
messages: messages,
history: history
history: history
...
@@ -372,619 +213,39 @@
...
@@ -372,619 +213,39 @@
</script>
</script>
{#if messages.length == 0}
{#if messages.length == 0}
<div
class=
"m-auto text-center max-w-md pb-56 px-2"
>
<Placeholder models={selectedModels} modelfiles={selectedModelfiles} />
<div
class=
"flex justify-center mt-8"
>
{#if selectedModelfile
&&
selectedModelfile.imageUrl}
<img
src=
{selectedModelfile?.imageUrl}
alt=
"modelfile"
class=
" w-20 mb-2 rounded-full"
draggable=
"false"
/>
{:else}
<img
src=
"/ollama.png"
class=
" w-16 invert-[10%] dark:invert-[100%] rounded-full"
alt=
"ollama"
draggable=
"false"
/>
{/if}
</div>
<div
class=
" mt-2 text-2xl text-gray-800 dark:text-gray-100 font-semibold"
>
{#if selectedModelfile}
<span
class=
" capitalize"
>
{selectedModelfile.title}
</span>
<div
class=
"mt-0.5 text-base font-normal text-gray-600 dark:text-gray-400"
>
{selectedModelfile.desc}
</div>
{#if selectedModelfile.user}
<div
class=
"mt-0.5 text-sm font-normal text-gray-500 dark:text-gray-500"
>
By
<a
href=
"https://ollamahub.com/m/{selectedModelfile.user.username}"
>
{selectedModelfile.user.name
? selectedModelfile.user.name
: `@${selectedModelfile.user.username}`}
</a
>
</div>
{/if}
{:else}
How can I help you today?
{/if}
</div>
</div>
{:else}
{:else}
{#each messages as message, messageIdx}
{#each messages as message, messageIdx}
<div class=" w-full">
<div class=" w-full">
<div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
<div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
<div
class=
" flex w-full"
>
{#if message.role === 'user'}
<div
class=
" mr-4"
>
<UserMessage
{#if message.role === 'user'}
user={$user}
{#if $config === null || !($config?.auth ?? true)}
{message}
<img
siblings={message.parentId !== null
src=
"{$settings.gravatarUrl ? $settings.gravatarUrl : '/user'}.png"
? history.messages[message.parentId]?.childrenIds ?? []
class=
" max-w-[28px] object-cover rounded-full"
: Object.values(history.messages)
alt=
"User profile"
.filter((message) => message.parentId === null)
draggable=
"false"
.map((message) => message.id) ?? []}
/>
{confirmEditMessage}
{:else}
{showPreviousMessage}
<img
{showNextMessage}
src=
{$user
?
$
user.profile_image_url
:
'/
user.png
'}
{copyToClipboard}
class=
" max-w-[28px] object-cover rounded-full"
/>
alt=
"User profile"
{:else}
draggable=
"false"
<ResponseMessage
/>
{message}
{/if}
modelfiles={selectedModelfiles}
{:else if selectedModelfile}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
<img
isLastMessage={messageIdx + 1 === messages.length}
src=
{selectedModelfile?.imageUrl
??
'/
favicon.png
'}
{confirmEditResponseMessage}
class=
" max-w-[28px] object-cover rounded-full"
{showPreviousMessage}
alt=
"Ollama profile"
{showNextMessage}
draggable=
"false"
{rateMessage}
/>
{copyToClipboard}
{:else}
{regenerateResponse}
<img
/>
src=
"/favicon.png"
{/if}
class=
" max-w-[28px] object-cover rounded-full"
alt=
"Ollama profile"
draggable=
"false"
/>
{/if}
</div>
<div
class=
"w-full overflow-hidden"
>
<div
class=
" self-center font-bold mb-0.5"
>
{#if message.role === 'user'}
You
{:else if selectedModelfile}
<span
class=
"capitalize"
>
{selectedModelfile.title}
</span>
{:else}
Ollama
<span
class=
" text-gray-500 text-sm font-medium"
>
{message.model ? ` ${message.model}` : ''}
</span
>
{/if}
</div>
{#if message.role !== 'user'
&&
message.content === ''}
<div
class=
"w-full mt-3"
>
<div
class=
"animate-pulse flex w-full"
>
<div
class=
"space-y-2 w-full"
>
<div
class=
"h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14"
/>
<div
class=
"grid grid-cols-3 gap-4"
>
<div
class=
"h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2"
/>
<div
class=
"h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1"
/>
</div>
<div
class=
"grid grid-cols-4 gap-4"
>
<div
class=
"h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1"
/>
<div
class=
"h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2"
/>
<div
class=
"h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4"
/>
</div>
<div
class=
"h-2 bg-gray-200 dark:bg-gray-600 rounded"
/>
</div>
</div>
</div>
{:else}
<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"
>
{#if message.role == 'user'}
{#if message.files}
<div
class=
"my-3 w-full flex overflow-x-auto space-x-2"
>
{#each message.files as file}
<div>
{#if file.type === 'image'}
<img
src=
{file.url}
alt=
"input"
class=
" max-h-96 rounded-lg"
draggable=
"false"
/>
{/if}
</div>
{/each}
</div>
{/if}
{#if message?.edit === true}
<div
class=
" w-full"
>
<textarea
id=
"message-edit-{message.id}"
class=
" bg-transparent outline-none w-full resize-none"
bind:value=
{history.messages[message.id].editedContent}
on:input=
{(e)
=
>
{
e.target.style.height = `${e.target.scrollHeight}px`;
}}
/>
<div
class=
" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium"
>
<button
class=
"px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click=
{()
=
>
{
confirmEditMessage(message.id);
}}
>
Save
&
Submit
</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"
on:click=
{()
=
>
{
cancelEditMessage(message.id);
}}
>
Cancel
</button>
</div>
</div>
{:else}
<div
class=
"w-full"
>
<pre
id=
"user-message"
>
{message.content}
</pre>
<div
class=
" flex justify-start space-x-1"
>
{#if message.parentId !== null
&&
message.parentId in history.messages
&&
(history.messages[message.parentId]?.childrenIds.length ?? 0) > 1}
<div
class=
"flex self-center"
>
<button
class=
"self-center"
on:click=
{()
=
>
{
showPreviousMessage(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=
"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"
>
{history.messages[message.parentId].childrenIds.indexOf(message.id) +
1} / {history.messages[message.parentId].childrenIds.length}
</div>
<button
class=
"self-center"
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>
{:else if message.parentId === null
&&
Object.values(history.messages).filter((message) => message.parentId === null).length > 1}
<div
class=
"flex self-center"
>
<button
class=
"self-center"
on:click=
{()
=
>
{
showPreviousMessage(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=
"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"
>
{Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id)
.indexOf(message.id) + 1} / {Object.values(history.messages).filter(
(message) => message.parentId === null
).length}
</div>
<button
class=
"self-center"
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}
<button
class=
"invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
editMessageHandler(message.id);
}}
>
<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=
"invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
copyToClipboard(message.content);
}}
>
<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=
"M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
</div>
</div>
{/if}
{/if}
{#if message.role === 'assistant'}
<div>
{#if message?.edit === true}
<div
class=
" w-full"
>
<textarea
id=
"message-edit-{message.id}"
class=
" bg-transparent outline-none w-full resize-none"
bind:value=
{history.messages[message.id].editedContent}
on:input=
{(e)
=
>
{
e.target.style.height = `${e.target.scrollHeight}px`;
}}
/>
<div
class=
" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium"
>
<button
class=
"px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click=
{()
=
>
{
confirmEditResponseMessage(message.id);
}}
>
Save
</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"
on:click=
{()
=
>
{
cancelEditMessage(message.id);
}}
>
Cancel
</button>
</div>
</div>
{:else}
<div
class=
"w-full"
>
{#if message?.error === true}
<div
class=
"flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
>
<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 self-center"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<div
class=
" self-center"
>
{message.content}
</div>
</div>
{:else}
{@html marked(message.content.replace('\\\\', '\\\\\\'))}
{/if}
{#if message.done}
<div
class=
" flex justify-start space-x-1 -mt-2"
>
{#if message.parentId !== null
&&
message.parentId in history.messages
&&
(history.messages[message.parentId]?.childrenIds.length ?? 0) > 1}
<div
class=
"flex self-center"
>
<button
class=
"self-center"
on:click=
{()
=
>
{
showPreviousMessage(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=
"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"
>
{history.messages[message.parentId].childrenIds.indexOf(
message.id
) + 1} / {history.messages[message.parentId].childrenIds.length}
</div>
<button
class=
"self-center"
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}
<button
class=
"{messageIdx + 1 === messages.length
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
editMessageHandler(message.id);
}}
>
<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=
"{messageIdx + 1 === messages.length
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition copy-response-button"
on:click=
{()
=
>
{
copyToClipboard(message.content);
}}
>
<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=
"M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
<button
class=
"{messageIdx + 1 === messages.length
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
rateMessage(messageIdx, 1);
}}
>
<svg
stroke=
"currentColor"
fill=
"none"
stroke-width=
"2"
viewBox=
"0 0 24 24"
stroke-linecap=
"round"
stroke-linejoin=
"round"
class=
"w-4 h-4"
xmlns=
"http://www.w3.org/2000/svg"
><path
d=
"M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
</button>
<button
class=
"{messageIdx + 1 === messages.length
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
rateMessage(messageIdx, -1);
}}
>
<svg
stroke=
"currentColor"
fill=
"none"
stroke-width=
"2"
viewBox=
"0 0 24 24"
stroke-linecap=
"round"
stroke-linejoin=
"round"
class=
"w-4 h-4"
xmlns=
"http://www.w3.org/2000/svg"
><path
d=
"M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
</button>
<button
class=
"{messageIdx + 1 === messages.length
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
speakMessage(message.content);
}}
>
<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=
"M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
/>
</svg>
</button>
{#if message.info}
<button
class=
" {messageIdx + 1 === messages.length
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition whitespace-pre-wrap"
on:click=
{()
=
>
{
console.log(message);
}}
id="info-{message.id}"
>
<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=
"M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
</button>
{/if}
{#if messageIdx + 1 === messages.length}
<button
type=
"button"
class=
"{messageIdx + 1 === messages.length
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{regenerateResponse}
>
<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.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
{/if}
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>
<!-- {} -->
</div>
</div>
</div>
</div>
</div>
{/each}
{/each}
...
...
src/lib/components/chat/Messages/Name.svelte
0 → 100644
View file @
7753851e
<div class=" self-center font-bold mb-0.5 capitalize">
<slot />
</div>
src/lib/components/chat/Messages/Placeholder.svelte
0 → 100644
View file @
7753851e
<script lang="ts">
import { onMount } from 'svelte';
export let models = [];
export let modelfiles = [];
let modelfile = null;
let selectedModelIdx = 0;
$: modelfile =
models[selectedModelIdx] in modelfiles ? modelfiles[models[selectedModelIdx]] : null;
$: if (models.length > 0) {
selectedModelIdx = models.length - 1;
}
</script>
{#if models.length > 0}
<div class="m-auto text-center max-w-md pb-56 px-2">
<div class="flex justify-center mt-8">
<div class="flex -space-x-10">
{#each models as model, modelIdx}
<button
on:click={() => {
selectedModelIdx = modelIdx;
}}
>
{#if model in modelfiles}
<img
src={modelfiles[model]?.imageUrl}
alt="modelfile"
class=" w-20 mb-2 rounded-full {models.length > 1
? ' border-[5px] border-white dark:border-gray-800'
: ''}"
draggable="false"
/>
{:else}
<img
src={models.length === 1 ? '/ollama.png' : 'ollama-dark.png'}
class=" w-20 mb-2 {models.length === 1
? 'invert-[10%] dark:invert-[100%]'
: 'border-[5px] border-white dark:border-gray-800'} rounded-full"
alt="ollama"
draggable="false"
/>
{/if}
</button>
{/each}
</div>
</div>
<div class=" mt-2 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
{#if modelfile}
<span class=" capitalize">
{modelfile.title}
</span>
<div class="mt-0.5 text-base font-normal text-gray-600 dark:text-gray-400">
{modelfile.desc}
</div>
{#if modelfile.user}
<div class="mt-0.5 text-sm font-normal text-gray-500 dark:text-gray-500">
By <a href="https://ollamahub.com/m/{modelfile.user.username}"
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
>
</div>
{/if}
{:else}
How can I help you today?
{/if}
</div>
</div>
{/if}
src/lib/components/chat/Messages/ProfileImage.svelte
0 → 100644
View file @
7753851e
<script lang="ts">
export let src = '/user.png';
</script>
<div class=" mr-4">
<img {src} class=" max-w-[28px] object-cover rounded-full" alt="profile" draggable="false" />
</div>
src/lib/components/chat/Messages/ResponseMessage.svelte
0 → 100644
View file @
7753851e
<script
lang=
"ts"
>
import
{
marked
}
from
'
marked
'
;
import
tippy
from
'
tippy.js
'
;
import
hljs
from
'
highlight.js
'
;
import
'
highlight.js/styles/github-dark.min.css
'
;
import
auto_render
from
'
katex/dist/contrib/auto-render.mjs
'
;
import
'
katex/dist/katex.min.css
'
;
import
Name
from
'
./Name.svelte
'
;
import
ProfileImage
from
'
./ProfileImage.svelte
'
;
import
Skeleton
from
'
./Skeleton.svelte
'
;
import
{
onMount
,
tick
}
from
'
svelte
'
;
export
let
modelfiles
=
[];
export
let
message
;
export
let
siblings
;
export
let
isLastMessage
=
true
;
export
let
confirmEditResponseMessage
:
Function
;
export
let
showPreviousMessage
:
Function
;
export
let
showNextMessage
:
Function
;
export
let
rateMessage
:
Function
;
export
let
copyToClipboard
:
Function
;
export
let
regenerateResponse
:
Function
;
let
edit
=
false
;
let
editedContent
=
''
;
let
tooltipInstance
=
null
;
let
speaking
=
null
;
$
:
if
(
message
)
{
renderStyling
();
}
const
renderStyling
=
async
()
=>
{
await
tick
();
if
(
tooltipInstance
)
{
tooltipInstance
[
0
].
destroy
();
}
renderLatex
();
hljs
.
highlightAll
();
createCopyCodeBlockButton
();
if
(
message
.
info
)
{
tooltipInstance
=
tippy
(
`#info-
${
message
.
id
}
`
,
{
content
:
`<span class="text-xs" id="tooltip-
${
message
.
id
}
">token/s:
${
`
${
Math
.
round
(
((
message
.
info
.
eval_count
??
0
)
/
(
message
.
info
.
eval_duration
/
1000000000
))
*
100
)
/
100
}
tokens
` ?? 'N/A'
}<br/>
total_duration:
${
Math
.
round
(((
message
.
info
.
total_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
br
/>
load_duration
:
$
{
Math
.
round
(((
message
.
info
.
load_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
br
/>
prompt_eval_count
:
$
{
message
.
info
.
prompt_eval_count
??
'
N/A
'
}
<
br
/>
prompt_eval_duration
:
$
{
Math
.
round
(((
message
.
info
.
prompt_eval_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
br
/>
eval_count
:
$
{
message
.
info
.
eval_count
??
'
N/A
'
}
<
br
/>
eval_duration
:
$
{
Math
.
round
(((
message
.
info
.
eval_duration
??
0
)
/
1000000
)
*
100
)
/
100
??
'
N/A
'
}
ms
<
/span>`
,
allowHTML
:
true
});
}
};
const
createCopyCodeBlockButton
=
()
=>
{
// use a class selector if available
let
blocks
=
document
.
querySelectorAll
(
'
pre
'
);
blocks
.
forEach
((
block
)
=>
{
// only add button if browser supports Clipboard API
if
(
block
.
childNodes
.
length
<
2
&&
block
.
id
!==
'
user-message
'
)
{
let
code
=
block
.
querySelector
(
'
code
'
);
code
.
style
.
borderTopRightRadius
=
0
;
code
.
style
.
borderTopLeftRadius
=
0
;
let
topBarDiv
=
document
.
createElement
(
'
div
'
);
topBarDiv
.
style
.
backgroundColor
=
'
#202123
'
;
topBarDiv
.
style
.
overflowX
=
'
auto
'
;
topBarDiv
.
style
.
display
=
'
flex
'
;
topBarDiv
.
style
.
justifyContent
=
'
space-between
'
;
topBarDiv
.
style
.
padding
=
'
0 1rem
'
;
topBarDiv
.
style
.
paddingTop
=
'
4px
'
;
topBarDiv
.
style
.
borderTopRightRadius
=
'
8px
'
;
topBarDiv
.
style
.
borderTopLeftRadius
=
'
8px
'
;
let
langDiv
=
document
.
createElement
(
'
div
'
);
let
codeClassNames
=
code
?.
className
.
split
(
'
'
);
langDiv
.
textContent
=
codeClassNames
[
0
]
===
'
hljs
'
?
codeClassNames
[
1
].
slice
(
9
)
:
codeClassNames
[
0
].
slice
(
9
);
langDiv
.
style
.
color
=
'
white
'
;
langDiv
.
style
.
margin
=
'
4px
'
;
langDiv
.
style
.
fontSize
=
'
0.75rem
'
;
let
button
=
document
.
createElement
(
'
button
'
);
button
.
className
=
'
copy-code-button
'
;
button
.
textContent
=
'
Copy Code
'
;
button
.
style
.
background
=
'
none
'
;
button
.
style
.
fontSize
=
'
0.75rem
'
;
button
.
style
.
border
=
'
none
'
;
button
.
style
.
margin
=
'
4px
'
;
button
.
style
.
cursor
=
'
pointer
'
;
button
.
style
.
color
=
'
#ddd
'
;
button
.
addEventListener
(
'
click
'
,
()
=>
copyCode
(
block
,
button
));
topBarDiv
.
appendChild
(
langDiv
);
topBarDiv
.
appendChild
(
button
);
block
.
prepend
(
topBarDiv
);
}
});
async
function
copyCode
(
block
,
button
)
{
let
code
=
block
.
querySelector
(
'
code
'
);
let
text
=
code
.
innerText
;
await
copyToClipboard
(
text
);
// visual feedback that task is completed
button
.
innerText
=
'
Copied!
'
;
setTimeout
(()
=>
{
button
.
innerText
=
'
Copy Code
'
;
},
1000
);
}
};
const
renderLatex
=
()
=>
{
let
chatMessageElements
=
document
.
getElementsByClassName
(
'
chat-assistant
'
);
// let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];
for
(
const
element
of
chatMessageElements
)
{
auto_render
(
element
,
{
// customised options
// • auto-render specific keys, e.g.:
delimiters
:
[
{
left
:
'
$$
'
,
right
:
'
$$
'
,
display
:
true
},
// { left: '$', right: '$', display: false },
{
left
:
'
\\
(
'
,
right
:
'
\\
)
'
,
display
:
true
},
{
left
:
'
\\
[
'
,
right
:
'
\\
]
'
,
display
:
true
}
],
// • rendering keys, e.g.:
throwOnError
:
false
});
}
};
const
toggleSpeakMessage
=
async
()
=>
{
if
(
speaking
)
{
speechSynthesis
.
cancel
();
speaking
=
null
;
}
else
{
speaking
=
true
;
const
speak
=
new
SpeechSynthesisUtterance
(
message
.
content
);
speechSynthesis
.
speak
(
speak
);
}
};
const
editMessageHandler
=
async
()
=>
{
edit
=
true
;
editedContent
=
message
.
content
;
await
tick
();
const
editElement
=
document
.
getElementById
(
`message-edit-
${
message
.
id
}
`
);
editElement
.
style
.
height
=
''
;
editElement
.
style
.
height
=
`
${
editElement
.
scrollHeight
}
px`
;
};
const
editMessageConfirmHandler
=
async
()
=>
{
confirmEditResponseMessage
(
message
.
id
,
editedContent
);
edit
=
false
;
editedContent
=
''
;
await
tick
();
renderStyling
();
};
const
cancelEditMessage
=
async
()
=>
{
edit
=
false
;
editedContent
=
''
;
await
tick
();
renderStyling
();
};
onMount
(
async
()
=>
{
await
tick
();
renderStyling
();
});
</script>
<div
class=
" flex w-full message-{message.id}"
>
<ProfileImage
src=
{modelfiles[message.model]?.imageUrl
??
'/
favicon.png
'}
/>
<div
class=
"w-full overflow-hidden"
>
<Name>
{#if message.model in modelfiles}
{modelfiles[message.model]?.title}
{:else}
Ollama
<span
class=
" text-gray-500 text-sm font-medium"
>
{message.model ? ` ${message.model}` : ''}
</span
>
{/if}
</Name>
{#if message.content === ''}
<Skeleton
/>
{:else}
<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"
>
<div>
{#if edit === true}
<div
class=
" w-full"
>
<textarea
id=
"message-edit-{message.id}"
class=
" bg-transparent outline-none w-full resize-none"
bind:value=
{editedContent}
on:input=
{(e)
=
>
{
e.target.style.height = `${e.target.scrollHeight}px`;
}}
/>
<div
class=
" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium"
>
<button
class=
"px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click=
{()
=
>
{
editMessageConfirmHandler();
}}
>
Save
</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"
on:click=
{()
=
>
{
cancelEditMessage();
}}
>
Cancel
</button>
</div>
</div>
{:else}
<div
class=
"w-full"
>
{#if message?.error === true}
<div
class=
"flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
>
<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 self-center"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<div
class=
" self-center"
>
{message.content}
</div>
</div>
{:else}
{@html marked(message.content.replace('\\\\', '\\\\\\'))}
{/if}
{#if message.done}
<div
class=
" flex justify-start space-x-1 -mt-2"
>
{#if siblings.length > 1}
<div
class=
"flex self-center"
>
<button
class=
"self-center"
on:click=
{()
=
>
{
showPreviousMessage(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=
"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"
>
{siblings.indexOf(message.id) + 1} / {siblings.length}
</div>
<button
class=
"self-center"
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}
<button
class=
"{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
editMessageHandler();
}}
>
<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=
"{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition copy-response-button"
on:click=
{()
=
>
{
copyToClipboard(message.content);
}}
>
<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=
"M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
<button
class=
"{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
? 'bg-gray-100 dark:bg-gray-900'
: ''} transition"
on:click=
{()
=
>
{
rateMessage(message.id, 1);
}}
>
<svg
stroke=
"currentColor"
fill=
"none"
stroke-width=
"2"
viewBox=
"0 0 24 24"
stroke-linecap=
"round"
stroke-linejoin=
"round"
class=
"w-4 h-4"
xmlns=
"http://www.w3.org/2000/svg"
><path
d=
"M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
</button>
<button
class=
"{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
? 'bg-gray-100 dark:bg-gray-900'
: ''} transition"
on:click=
{()
=
>
{
rateMessage(message.id, -1);
}}
>
<svg
stroke=
"currentColor"
fill=
"none"
stroke-width=
"2"
viewBox=
"0 0 24 24"
stroke-linecap=
"round"
stroke-linejoin=
"round"
class=
"w-4 h-4"
xmlns=
"http://www.w3.org/2000/svg"
><path
d=
"M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
</button>
<button
class=
"{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{()
=
>
{
toggleSpeakMessage(message);
}}
>
{#if speaking}
<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=
"M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
/>
</svg>
{:else}
<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=
"M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
/>
</svg>
{/if}
</button>
{#if message.info}
<button
class=
" {isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition whitespace-pre-wrap"
on:click=
{()
=
>
{
console.log(message);
}}
id="info-{message.id}"
>
<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=
"M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
</button>
{/if}
{#if isLastMessage}
<button
type=
"button"
class=
"{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
on:click=
{regenerateResponse}
>
<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.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
{/if}
</div>
{/if}
</div>
{/if}
</div>
</div>
{/if}
</div>
</div>
src/lib/components/chat/Messages/Skeleton.svelte
0 → 100644
View file @
7753851e
<div class="w-full mt-3">
<div class="animate-pulse flex w-full">
<div class="space-y-2 w-full">
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
<div class="grid grid-cols-3 gap-4">
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
</div>
<div class="grid grid-cols-4 gap-4">
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4" />
</div>
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded" />
</div>
</div>
</div>
src/lib/components/chat/Messages/UserMessage.svelte
0 → 100644
View file @
7753851e
<script lang="ts">
import { tick } from 'svelte';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
export let user;
export let message;
export let siblings;
export let confirmEditMessage: Function;
export let showPreviousMessage: Function;
export let showNextMessage: Function;
export let copyToClipboard: Function;
let edit = false;
let editedContent = '';
const editMessageHandler = async () => {
edit = true;
editedContent = message.content;
await tick();
const editElement = document.getElementById(`message-edit-${message.id}`);
editElement.style.height = '';
editElement.style.height = `${editElement.scrollHeight}px`;
};
const editMessageConfirmHandler = async () => {
confirmEditMessage(message.id, editedContent);
edit = false;
editedContent = '';
};
const cancelEditMessage = () => {
edit = false;
editedContent = '';
};
</script>
<div class=" flex w-full">
<ProfileImage src={user?.profile_image_url ?? '/user.png'} />
<div class="w-full overflow-hidden">
<Name>You</Name>
<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"
>
{#if message.files}
<div class="my-3 w-full flex overflow-x-auto space-x-2">
{#each message.files as file}
<div>
{#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{/if}
</div>
{/each}
</div>
{/if}
{#if edit === true}
<div class=" w-full">
<textarea
id="message-edit-{message.id}"
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
on:input={(e) => {
e.target.style.height = `${e.target.scrollHeight}px`;
}}
/>
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click={() => {
editMessageConfirmHandler();
}}
>
Save & Submit
</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"
on:click={() => {
cancelEditMessage();
}}
>
Cancel
</button>
</div>
</div>
{:else}
<div class="w-full">
<pre id="user-message">{message.content}</pre>
<div class=" flex justify-start space-x-1">
{#if siblings.length > 1}
<div class="flex self-center">
<button
class="self-center"
on:click={() => {
showPreviousMessage(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="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">
{siblings.indexOf(message.id) + 1} / {siblings.length}
</div>
<button
class="self-center"
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}
<button
class="invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition"
on:click={() => {
editMessageHandler();
}}
>
<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="invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition"
on:click={() => {
copyToClipboard(message.content);
}}
>
<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="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
</div>
</div>
{/if}
</div>
</div>
</div>
src/lib/components/chat/ModelSelector.svelte
View file @
7753851e
...
@@ -6,8 +6,8 @@
...
@@ -6,8 +6,8 @@
export let disabled = false;
export let disabled = false;
const saveDefaultModel = () => {
const saveDefaultModel = () => {
const hasEmptyModel = selectedModels.filter(it => it === '');
const hasEmptyModel = selectedModels.filter(
(
it
)
=> it === '');
if(hasEmptyModel.length){
if
(hasEmptyModel.length)
{
toast.error('Choose a model before saving...');
toast.error('Choose a model before saving...');
return;
return;
}
}
...
@@ -88,8 +88,9 @@
...
@@ -88,8 +88,9 @@
{#if selectedModelIdx === 0}
{#if selectedModelIdx === 0}
<button
<button
class=" self-center dark:hover:text-gray-300"
class=" self-center dark:hover:text-gray-300"
id="open-settings-button"
on:click={async () => {
on:click={async () => {
await showSettings.set(
true
);
await showSettings.set(
!$showSettings
);
}}
}}
>
>
<svg
<svg
...
...
src/lib/components/chat/SettingsModal.svelte
View file @
7753851e
...
@@ -8,7 +8,7 @@
...
@@ -8,7 +8,7 @@
import { splitStream, getGravatarURL } from '$lib/utils';
import { splitStream, getGravatarURL } from '$lib/utils';
import { getOllamaVersion } from '$lib/apis/ollama';
import { getOllamaVersion } from '$lib/apis/ollama';
import { createNewChat, getAllChats, getChatList } from '$lib/apis/chats';
import { createNewChat,
deleteAllChats,
getAllChats, getChatList } from '$lib/apis/chats';
import {
import {
WEB_UI_VERSION,
WEB_UI_VERSION,
OLLAMA_API_BASE_URL,
OLLAMA_API_BASE_URL,
...
@@ -19,6 +19,7 @@
...
@@ -19,6 +19,7 @@
import Advanced from './Settings/Advanced.svelte';
import Advanced from './Settings/Advanced.svelte';
import Modal from '../common/Modal.svelte';
import Modal from '../common/Modal.svelte';
import { updateUserPassword } from '$lib/apis/auths';
import { updateUserPassword } from '$lib/apis/auths';
import { goto } from '$app/navigation';
export let show = false;
export let show = false;
...
@@ -84,7 +85,7 @@
...
@@ -84,7 +85,7 @@
// Chats
// Chats
let importFiles;
let importFiles;
let showDelete
History
Confirm = false;
let showDeleteConfirm = false;
const importChats = async (_chats) => {
const importChats = async (_chats) => {
for (const chat of _chats) {
for (const chat of _chats) {
...
@@ -115,6 +116,12 @@
...
@@ -115,6 +116,12 @@
reader.readAsText(importFiles[0]);
reader.readAsText(importFiles[0]);
}
}
const deleteChats = async () => {
await goto('/');
await deleteAllChats(localStorage.token);
await chats.set(await getChatList(localStorage.token));
};
// Auth
// Auth
let authEnabled = false;
let authEnabled = false;
let authType = 'Basic';
let authType = 'Basic';
...
@@ -1621,147 +1628,154 @@
...
@@ -1621,147 +1628,154 @@
</form>
</form>
{:else if selectedTab === 'chats'}
{:else if selectedTab === 'chats'}
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
<div class="flex flex-col">
<div class=" space-y-2">
<input
<div class="flex flex-col">
id="chat-import-input"
<input
bind:files={importFiles}
id="chat-import-input"
type="file"
bind:files={importFiles}
accept=".json"
type="file"
hidden
accept=".json"
/>
hidden
<button
/>
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
<button
on:click={() => {
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
document.getElementById('chat-import-input').click();
on:click={() => {
}}
document.getElementById('chat-import-input').click();
>
}}
<div class=" self-center mr-3">
>
<svg
<div class=" self-center mr-3">
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>
<div class=" self-center text-sm font-medium">Import Chats</div>
</button>
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
exportChats();
}}
>
<div class=" self-center mr-3">
<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>
<div class=" self-center text-sm font-medium">Export Chats</div>
</button>
</div>
<!-- {#if showDeleteHistoryConfirm}
<div
class="flex justify-between rounded-md items-center py-3 px-3.5 w-full transition"
>
<div class="flex items-center">
<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 mr-3"
>
<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>
<span>Are you sure?</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
deleteChatHistory();
showDeleteHistoryConfirm = false;
}}
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0
20 20
"
viewBox="0 0
16 16
"
fill="currentColor"
fill="currentColor"
class="w-4 h-4"
class="w-4 h-4"
>
>
<path
<path
fill-rule="evenodd"
fill-rule="evenodd"
d="M
16.704 4.153a.75.75 0 01.143 1.052l-8 10
.5a.75.75 0 01-
1.127.075l-4.5-4.5
a.75.75 0 0
1
1.06-1.06l
3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z
"
d="M
4 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.72
a.75.75 0 0
1-
1.06-1.06l
2-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"
clip-rule="evenodd"
/>
/>
</svg>
</svg>
</button>
</div>
<button
<div class=" self-center text-sm font-medium">Import Chats</div>
class="hover:text-white transition"
</button>
on:click={() => {
<button
showDeleteHistoryConfirm = false;
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
}}
on:click={() => {
>
exportChats();
}}
>
<div class=" self-center mr-3">
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0
20 20
"
viewBox="0 0
16 16
"
fill="currentColor"
fill="currentColor"
class="w-4 h-4"
class="w-4 h-4"
>
>
<path
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
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>
</svg>
</button>
</div>
</div>
<div class=" self-center text-sm font-medium">Export Chats</div>
</button>
</div>
</div>
{:else}
<button
<hr class=" dark:border-gray-700" />
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
on:click={() => {
{#if showDeleteConfirm}
showDeleteHistoryConfirm = true;
<div
}}
class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"
>
>
<div class="mr-3">
<div class="flex items-center space-x-3">
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 16 16"
viewBox="0 0 24 24"
fill="currentColor"
stroke-width="1.5"
class="w-4 h-4"
stroke="currentColor"
>
class="w-5 h-5"
<path
>
d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z"
<path
/>
stroke-linecap="round"
<path
stroke-linejoin="round"
fill-rule="evenodd"
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"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
/>
clip-rule="evenodd"
</svg>
/>
</svg>
<span>Are you sure?</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
deleteChats();
showDeleteConfirm = false;
}}
>
<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="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
on:click={() => {
showDeleteConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
</div>
<span>Clear conversations</span>
{:else}
</button>
<button
{/if} -->
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showDeleteConfirm = true;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z"
/>
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Delete All Chats</div>
</button>
{/if}
</div>
</div>
</div>
{:else if selectedTab === 'auth'}
{:else if selectedTab === 'auth'}
<form
<form
...
...
src/lib/components/chat/ShortcutsModal.svelte
View file @
7753851e
...
@@ -123,6 +123,23 @@
...
@@ -123,6 +123,23 @@
</div>
</div>
<div class="flex flex-col space-y-3 w-full self-start">
<div class="flex flex-col space-y-3 w-full self-start">
<div class="w-full flex justify-between items-center">
<div class=" text-sm">Toggle settings</div>
<div class="flex space-x-1 text-xs">
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Ctrl/⌘
</div>
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
.
</div>
</div>
</div>
<div class="w-full flex justify-between items-center">
<div class="w-full flex justify-between items-center">
<div class=" text-sm">Toggle sidebar</div>
<div class=" text-sm">Toggle sidebar</div>
...
...
src/routes/(app)/+layout.svelte
View file @
7753851e
...
@@ -160,6 +160,13 @@
...
@@ -160,6 +160,13 @@
document.getElementById('delete-chat-button')?.click();
document.getElementById('delete-chat-button')?.click();
}
}
// Check if Ctrl + . is pressed
if (isCtrlPressed && event.key === '.') {
event.preventDefault();
console.log('openSettings');
document.getElementById('open-settings-button')?.click();
}
// Check if Ctrl + / is pressed
// Check if Ctrl + / is pressed
if (isCtrlPressed && event.key === '/') {
if (isCtrlPressed && event.key === '/') {
event.preventDefault();
event.preventDefault();
...
...
src/routes/(app)/+page.svelte
View file @
7753851e
...
@@ -30,6 +30,17 @@
...
@@ -30,6 +30,17 @@
? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
: null;
: null;
let selectedModelfiles = {};
$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
const modelfile =
$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;
return {
...a,
...(modelfile && { [tagName]: modelfile })
};
}, {});
let chat = null;
let chat = null;
let title = '';
let title = '';
...
@@ -612,7 +623,7 @@
...
@@ -612,7 +623,7 @@
<Messages
<Messages
chatId={$chatId}
chatId={$chatId}
{selectedModels}
{selectedModels}
{selectedModelfile}
{selectedModelfile
s
}
bind:history
bind:history
bind:messages
bind:messages
bind:autoScroll
bind:autoScroll
...
...
src/routes/(app)/c/[id]/+page.svelte
View file @
7753851e
...
@@ -31,6 +31,17 @@
...
@@ -31,6 +31,17 @@
? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
: null;
: null;
let selectedModelfiles = {};
$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
const modelfile =
$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;
return {
...a,
...(modelfile && { [tagName]: modelfile })
};
}, {});
let chat = null;
let chat = null;
let title = '';
let title = '';
...
@@ -646,7 +657,7 @@
...
@@ -646,7 +657,7 @@
<Messages
<Messages
chatId={$chatId}
chatId={$chatId}
{selectedModels}
{selectedModels}
{selectedModelfile}
{selectedModelfile
s
}
bind:history
bind:history
bind:messages
bind:messages
bind:autoScroll
bind:autoScroll
...
...
static/ollama-dark.png
0 → 100644
View file @
7753851e
13.2 KB
static/ollama.png
View replaced file @
b919ac76
View file @
7753851e
7.31 KB
|
W:
|
H:
7.61 KB
|
W:
|
H:
2-up
Swipe
Onion skin
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