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
f9106cf3
Unverified
Commit
f9106cf3
authored
Apr 02, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
Apr 02, 2024
Browse files
Merge pull request #1364 from cheahjs/feat/local-sharing
feat: add local sharing of chats
parents
a363c1f2
807a60cc
Changes
17
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
962 additions
and
175 deletions
+962
-175
backend/apps/web/internal/db.py
backend/apps/web/internal/db.py
+4
-1
backend/apps/web/internal/migrations/001_initial_schema.py
backend/apps/web/internal/migrations/001_initial_schema.py
+149
-0
backend/apps/web/internal/migrations/002_add_local_sharing.py
...end/apps/web/internal/migrations/002_add_local_sharing.py
+48
-0
backend/apps/web/internal/migrations/README.md
backend/apps/web/internal/migrations/README.md
+21
-0
backend/apps/web/models/chats.py
backend/apps/web/models/chats.py
+93
-1
backend/apps/web/routers/chats.py
backend/apps/web/routers/chats.py
+72
-0
backend/requirements.txt
backend/requirements.txt
+1
-0
src/lib/apis/chats/index.ts
src/lib/apis/chats/index.ts
+96
-0
src/lib/components/chat/Messages.svelte
src/lib/components/chat/Messages.svelte
+3
-0
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+82
-76
src/lib/components/chat/Messages/UserMessage.svelte
src/lib/components/chat/Messages/UserMessage.svelte
+26
-23
src/lib/components/chat/ShareChatModal.svelte
src/lib/components/chat/ShareChatModal.svelte
+162
-23
src/lib/components/icons/Link.svelte
src/lib/components/icons/Link.svelte
+16
-0
src/lib/components/layout/Navbar.svelte
src/lib/components/layout/Navbar.svelte
+7
-49
src/routes/(app)/+page.svelte
src/routes/(app)/+page.svelte
+1
-1
src/routes/(app)/c/[id]/+page.svelte
src/routes/(app)/c/[id]/+page.svelte
+1
-1
src/routes/s/[id]/+page.svelte
src/routes/s/[id]/+page.svelte
+180
-0
No files found.
backend/apps/web/internal/db.py
View file @
f9106cf3
from
peewee
import
*
from
peewee_migrate
import
Router
from
config
import
SRC_LOG_LEVELS
,
DATA_DIR
import
os
import
logging
...
...
@@ -16,4 +17,6 @@ else:
DB
=
SqliteDatabase
(
f
"
{
DATA_DIR
}
/webui.db"
)
DB
.
connect
()
router
=
Router
(
DB
,
migrate_dir
=
"apps/web/internal/migrations"
,
logger
=
log
)
router
.
run
()
DB
.
connect
(
reuse_if_open
=
True
)
backend/apps/web/internal/migrations/001_initial_schema.py
0 → 100644
View file @
f9106cf3
"""Peewee migrations -- 001_initial_schema.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from
contextlib
import
suppress
import
peewee
as
pw
from
peewee_migrate
import
Migrator
with
suppress
(
ImportError
):
import
playhouse.postgres_ext
as
pw_pext
def
migrate
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your migrations here."""
@
migrator
.
create_model
class
Auth
(
pw
.
Model
):
id
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
email
=
pw
.
CharField
(
max_length
=
255
)
password
=
pw
.
CharField
(
max_length
=
255
)
active
=
pw
.
BooleanField
()
class
Meta
:
table_name
=
"auth"
@
migrator
.
create_model
class
Chat
(
pw
.
Model
):
id
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
user_id
=
pw
.
CharField
(
max_length
=
255
)
title
=
pw
.
CharField
()
chat
=
pw
.
TextField
()
timestamp
=
pw
.
DateField
()
class
Meta
:
table_name
=
"chat"
@
migrator
.
create_model
class
ChatIdTag
(
pw
.
Model
):
id
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
tag_name
=
pw
.
CharField
(
max_length
=
255
)
chat_id
=
pw
.
CharField
(
max_length
=
255
)
user_id
=
pw
.
CharField
(
max_length
=
255
)
timestamp
=
pw
.
DateField
()
class
Meta
:
table_name
=
"chatidtag"
@
migrator
.
create_model
class
Document
(
pw
.
Model
):
id
=
pw
.
AutoField
()
collection_name
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
name
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
title
=
pw
.
CharField
()
filename
=
pw
.
CharField
()
content
=
pw
.
TextField
(
null
=
True
)
user_id
=
pw
.
CharField
(
max_length
=
255
)
timestamp
=
pw
.
DateField
()
class
Meta
:
table_name
=
"document"
@
migrator
.
create_model
class
Modelfile
(
pw
.
Model
):
id
=
pw
.
AutoField
()
tag_name
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
user_id
=
pw
.
CharField
(
max_length
=
255
)
modelfile
=
pw
.
TextField
()
timestamp
=
pw
.
DateField
()
class
Meta
:
table_name
=
"modelfile"
@
migrator
.
create_model
class
Prompt
(
pw
.
Model
):
id
=
pw
.
AutoField
()
command
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
user_id
=
pw
.
CharField
(
max_length
=
255
)
title
=
pw
.
CharField
()
content
=
pw
.
TextField
()
timestamp
=
pw
.
DateField
()
class
Meta
:
table_name
=
"prompt"
@
migrator
.
create_model
class
Tag
(
pw
.
Model
):
id
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
name
=
pw
.
CharField
(
max_length
=
255
)
user_id
=
pw
.
CharField
(
max_length
=
255
)
data
=
pw
.
TextField
(
null
=
True
)
class
Meta
:
table_name
=
"tag"
@
migrator
.
create_model
class
User
(
pw
.
Model
):
id
=
pw
.
CharField
(
max_length
=
255
,
unique
=
True
)
name
=
pw
.
CharField
(
max_length
=
255
)
email
=
pw
.
CharField
(
max_length
=
255
)
role
=
pw
.
CharField
(
max_length
=
255
)
profile_image_url
=
pw
.
CharField
(
max_length
=
255
)
timestamp
=
pw
.
DateField
()
class
Meta
:
table_name
=
"user"
def
rollback
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your rollback migrations here."""
migrator
.
remove_model
(
"user"
)
migrator
.
remove_model
(
"tag"
)
migrator
.
remove_model
(
"prompt"
)
migrator
.
remove_model
(
"modelfile"
)
migrator
.
remove_model
(
"document"
)
migrator
.
remove_model
(
"chatidtag"
)
migrator
.
remove_model
(
"chat"
)
migrator
.
remove_model
(
"auth"
)
backend/apps/web/internal/migrations/002_add_local_sharing.py
0 → 100644
View file @
f9106cf3
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from
contextlib
import
suppress
import
peewee
as
pw
from
peewee_migrate
import
Migrator
with
suppress
(
ImportError
):
import
playhouse.postgres_ext
as
pw_pext
def
migrate
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your migrations here."""
migrator
.
add_fields
(
"chat"
,
share_id
=
pw
.
CharField
(
max_length
=
255
,
null
=
True
,
unique
=
True
)
)
def
rollback
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your rollback migrations here."""
migrator
.
remove_fields
(
"chat"
,
"share_id"
)
backend/apps/web/internal/migrations/README.md
0 → 100644
View file @
f9106cf3
# Database Migrations
This directory contains all the database migrations for the web app.
Migrations are done using the
[
`peewee-migrate`
](
https://github.com/klen/peewee_migrate
)
library.
Migrations are automatically ran at app startup.
## Creating a migration
Have you made a change to the schema of an existing model?
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
1.
Have a database file (
`webui.db`
) that has the old schema prior to any of your changes.
2.
Make your changes to the models.
3.
From the
`backend`
directory, run the following command:
```
bash
pw_migrate create
--auto
--auto-source
apps.web.models
--database
sqlite:///
${
SQLITE_DB
}
--directory
apps/web/internal/migrations
${
MIGRATION_NAME
}
```
-
`$SQLITE_DB`
should be the path to the database file.
-
`$MIGRATION_NAME`
should be a descriptive name for the migration.
4.
The migration file will be created in the
`apps/web/internal/migrations`
directory.
backend/apps/web/models/chats.py
View file @
f9106cf3
...
...
@@ -20,6 +20,7 @@ class Chat(Model):
title
=
CharField
()
chat
=
TextField
()
# Save Chat JSON as Text
timestamp
=
DateField
()
share_id
=
CharField
(
null
=
True
,
unique
=
True
)
class
Meta
:
database
=
DB
...
...
@@ -31,6 +32,7 @@ class ChatModel(BaseModel):
title
:
str
chat
:
str
timestamp
:
int
# timestamp in epoch
share_id
:
Optional
[
str
]
=
None
####################
...
...
@@ -52,6 +54,7 @@ class ChatResponse(BaseModel):
title
:
str
chat
:
dict
timestamp
:
int
# timestamp in epoch
share_id
:
Optional
[
str
]
=
None
# id of the chat to be shared
class
ChatTitleIdResponse
(
BaseModel
):
...
...
@@ -95,6 +98,71 @@ class ChatTable:
except
:
return
None
def
insert_shared_chat_by_chat_id
(
self
,
chat_id
:
str
)
->
Optional
[
ChatModel
]:
# Get the existing chat to share
chat
=
Chat
.
get
(
Chat
.
id
==
chat_id
)
# Check if the chat is already shared
if
chat
.
share_id
:
return
self
.
get_chat_by_id_and_user_id
(
chat
.
share_id
,
"shared"
)
# Create a new chat with the same data, but with a new ID
shared_chat
=
ChatModel
(
**
{
"id"
:
str
(
uuid
.
uuid4
()),
"user_id"
:
f
"shared-
{
chat_id
}
"
,
"title"
:
chat
.
title
,
"chat"
:
chat
.
chat
,
"timestamp"
:
int
(
time
.
time
()),
}
)
shared_result
=
Chat
.
create
(
**
shared_chat
.
model_dump
())
# Update the original chat with the share_id
result
=
(
Chat
.
update
(
share_id
=
shared_chat
.
id
).
where
(
Chat
.
id
==
chat_id
).
execute
()
)
return
shared_chat
if
(
shared_result
and
result
)
else
None
def
update_shared_chat_by_chat_id
(
self
,
chat_id
:
str
)
->
Optional
[
ChatModel
]:
try
:
print
(
"update_shared_chat_by_id"
)
chat
=
Chat
.
get
(
Chat
.
id
==
chat_id
)
print
(
chat
)
query
=
Chat
.
update
(
title
=
chat
.
title
,
chat
=
chat
.
chat
,
).
where
(
Chat
.
id
==
chat
.
share_id
)
query
.
execute
()
chat
=
Chat
.
get
(
Chat
.
id
==
chat
.
share_id
)
return
ChatModel
(
**
model_to_dict
(
chat
))
except
:
return
None
def
delete_shared_chat_by_chat_id
(
self
,
chat_id
:
str
)
->
bool
:
try
:
query
=
Chat
.
delete
().
where
(
Chat
.
user_id
==
f
"shared-
{
chat_id
}
"
)
query
.
execute
()
# Remove the rows, return number of rows removed.
return
True
except
:
return
False
def
update_chat_share_id_by_id
(
self
,
id
:
str
,
share_id
:
Optional
[
str
]
)
->
Optional
[
ChatModel
]:
try
:
query
=
Chat
.
update
(
share_id
=
share_id
,
).
where
(
Chat
.
id
==
id
)
query
.
execute
()
chat
=
Chat
.
get
(
Chat
.
id
==
id
)
return
ChatModel
(
**
model_to_dict
(
chat
))
except
:
return
None
def
get_chat_lists_by_user_id
(
self
,
user_id
:
str
,
skip
:
int
=
0
,
limit
:
int
=
50
)
->
List
[
ChatModel
]:
...
...
@@ -131,6 +199,13 @@ class ChatTable:
.
order_by
(
Chat
.
timestamp
.
desc
())
]
def
get_chat_by_id
(
self
,
id
:
str
)
->
Optional
[
ChatModel
]:
try
:
chat
=
Chat
.
get
(
Chat
.
id
==
id
)
return
ChatModel
(
**
model_to_dict
(
chat
))
except
:
return
None
def
get_chat_by_id_and_user_id
(
self
,
id
:
str
,
user_id
:
str
)
->
Optional
[
ChatModel
]:
try
:
chat
=
Chat
.
get
(
Chat
.
id
==
id
,
Chat
.
user_id
==
user_id
)
...
...
@@ -149,12 +224,15 @@ class ChatTable:
query
=
Chat
.
delete
().
where
((
Chat
.
id
==
id
)
&
(
Chat
.
user_id
==
user_id
))
query
.
execute
()
# Remove the rows, return number of rows removed.
return
True
return
True
and
self
.
delete_shared_chat_by_chat_id
(
id
)
except
:
return
False
def
delete_chats_by_user_id
(
self
,
user_id
:
str
)
->
bool
:
try
:
self
.
delete_shared_chats_by_user_id
(
user_id
)
query
=
Chat
.
delete
().
where
(
Chat
.
user_id
==
user_id
)
query
.
execute
()
# Remove the rows, return number of rows removed.
...
...
@@ -162,5 +240,19 @@ class ChatTable:
except
:
return
False
def
delete_shared_chats_by_user_id
(
self
,
user_id
:
str
)
->
bool
:
try
:
shared_chat_ids
=
[
f
"shared-
{
chat
.
id
}
"
for
chat
in
Chat
.
select
().
where
(
Chat
.
user_id
==
user_id
)
]
query
=
Chat
.
delete
().
where
(
Chat
.
user_id
<<
shared_chat_ids
)
query
.
execute
()
# Remove the rows, return number of rows removed.
return
True
except
:
return
False
Chats
=
ChatTable
(
DB
)
backend/apps/web/routers/chats.py
View file @
f9106cf3
...
...
@@ -189,6 +189,78 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
return
result
############################
# ShareChatById
############################
@
router
.
post
(
"/{id}/share"
,
response_model
=
Optional
[
ChatResponse
])
async
def
share_chat_by_id
(
id
:
str
,
user
=
Depends
(
get_current_user
)):
chat
=
Chats
.
get_chat_by_id_and_user_id
(
id
,
user
.
id
)
if
chat
:
if
chat
.
share_id
:
shared_chat
=
Chats
.
update_shared_chat_by_chat_id
(
chat
.
id
)
return
ChatResponse
(
**
{
**
shared_chat
.
model_dump
(),
"chat"
:
json
.
loads
(
shared_chat
.
chat
)}
)
shared_chat
=
Chats
.
insert_shared_chat_by_chat_id
(
chat
.
id
)
if
not
shared_chat
:
raise
HTTPException
(
status_code
=
status
.
HTTP_500_INTERNAL_SERVER_ERROR
,
detail
=
ERROR_MESSAGES
.
DEFAULT
(),
)
return
ChatResponse
(
**
{
**
shared_chat
.
model_dump
(),
"chat"
:
json
.
loads
(
shared_chat
.
chat
)}
)
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
ACCESS_PROHIBITED
,
)
############################
# DeletedSharedChatById
############################
@
router
.
delete
(
"/{id}/share"
,
response_model
=
Optional
[
bool
])
async
def
delete_shared_chat_by_id
(
id
:
str
,
user
=
Depends
(
get_current_user
)):
chat
=
Chats
.
get_chat_by_id_and_user_id
(
id
,
user
.
id
)
if
chat
:
if
not
chat
.
share_id
:
return
False
result
=
Chats
.
delete_shared_chat_by_chat_id
(
id
)
update_result
=
Chats
.
update_chat_share_id_by_id
(
id
,
None
)
return
result
and
update_result
!=
None
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
ACCESS_PROHIBITED
,
)
############################
# GetSharedChatById
############################
@
router
.
get
(
"/share/{share_id}"
,
response_model
=
Optional
[
ChatResponse
])
async
def
get_shared_chat_by_id
(
share_id
:
str
,
user
=
Depends
(
get_current_user
)):
chat
=
Chats
.
get_chat_by_id
(
share_id
)
if
chat
:
return
ChatResponse
(
**
{
**
chat
.
model_dump
(),
"chat"
:
json
.
loads
(
chat
.
chat
)})
else
:
raise
HTTPException
(
status_code
=
status
.
HTTP_401_UNAUTHORIZED
,
detail
=
ERROR_MESSAGES
.
NOT_FOUND
)
############################
# GetChatTagsById
############################
...
...
backend/requirements.txt
View file @
f9106cf3
...
...
@@ -14,6 +14,7 @@ uuid
requests
aiohttp
peewee
peewee-migrate
bcrypt
litellm==1.30.7
...
...
src/lib/apis/chats/index.ts
View file @
f9106cf3
...
...
@@ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => {
return
res
;
};
export
const
getChatByShareId
=
async
(
token
:
string
,
share_id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/chats/share/
${
share_id
}
`
,
{
method
:
'
GET
'
,
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
;
};
export
const
shareChatById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/chats/
${
id
}
/share`
,
{
method
:
'
POST
'
,
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
;
};
export
const
deleteSharedChatById
=
async
(
token
:
string
,
id
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/chats/
${
id
}
/share`
,
{
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
;
};
export
const
updateChatById
=
async
(
token
:
string
,
id
:
string
,
chat
:
object
)
=>
{
let
error
=
null
;
...
...
src/lib/components/chat/Messages.svelte
View file @
f9106cf3
...
...
@@ -16,6 +16,7 @@
const i18n = getContext('i18n');
export let chatId = '';
export let readOnly = false;
export let sendPrompt: Function;
export let continueGeneration: Function;
export let regenerateResponse: Function;
...
...
@@ -317,6 +318,7 @@
<UserMessage
on:delete={() => messageDeleteHandler(message.id)}
user={$user}
{readOnly}
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
...
...
@@ -335,6 +337,7 @@
modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{confirmEditResponseMessage}
{showPreviousMessage}
{showNextMessage}
...
...
src/lib/components/chat/Messages/ResponseMessage.svelte
View file @
f9106cf3
...
...
@@ -33,6 +33,8 @@
export let isLastMessage = true;
export let readOnly = false;
export let confirmEditResponseMessage: Function;
export let showPreviousMessage: Function;
export let showNextMessage: Function;
...
...
@@ -469,6 +471,7 @@
</div>
{/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom">
<button
class="{isLastMessage
...
...
@@ -494,6 +497,7 @@
</svg>
</button>
</Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom">
<button
...
...
@@ -521,6 +525,7 @@
</button>
</Tooltip>
{#if !readOnly}
<Tooltip content="Good Response" placement="bottom">
<button
class="{isLastMessage
...
...
@@ -574,6 +579,7 @@
>
</button>
</Tooltip>
{/if}
<Tooltip content="Read Aloud" placement="bottom">
<button
...
...
@@ -656,7 +662,7 @@
</button>
</Tooltip>
{#if $config.images}
{#if $config.images
&& !readOnly
}
<Tooltip content="Generate Image" placement="bottom">
<button
class="{isLastMessage
...
...
@@ -752,7 +758,7 @@
</Tooltip>
{/if}
{#if isLastMessage}
{#if isLastMessage
&& !readOnly
}
<Tooltip content="Continue Response" placement="bottom">
<button
type="button"
...
...
src/lib/components/chat/Messages/UserMessage.svelte
View file @
f9106cf3
...
...
@@ -15,6 +15,7 @@
export let message;
export let siblings;
export let isFirstMessage: boolean;
export let readOnly: boolean;
export let confirmEditMessage: Function;
export let showPreviousMessage: Function;
...
...
@@ -250,6 +251,7 @@
</div>
{/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
...
...
@@ -273,6 +275,7 @@
</svg>
</button>
</Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom">
<button
...
...
@@ -298,7 +301,7 @@
</button>
</Tooltip>
{#if !isFirstMessage}
{#if !isFirstMessage
&& !readOnly
}
<Tooltip content="Delete" placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
...
...
src/lib/components/chat/ShareChatModal.svelte
View file @
f9106cf3
<script lang="ts">
import { getContext } from 'svelte';
import { getContext, onMount } from 'svelte';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { toast } from 'svelte-sonner';
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
import { chatId, modelfiles } from '$lib/stores';
import { copyToClipboard } from '$lib/utils';
import Modal from '../common/Modal.svelte';
import Link from '../icons/Link.svelte';
let chat = null;
const i18n = getContext('i18n');
export let downloadChat: Function;
export let shareChat: Function;
const shareLocalChat = async () => {
const _chat = chat;
const sharedChat = await shareChatById(localStorage.token, $chatId);
const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`;
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
copyToClipboard(chatShareUrl);
chat = await getChatById(localStorage.token, $chatId);
};
const shareChat = async () => {
const _chat = chat.chat;
console.log('share', _chat);
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
const tab = await window.open(`${url}/chats/upload`, '_blank');
window.addEventListener(
'message',
(event) => {
if (event.origin !== url) return;
if (event.data === 'loaded') {
tab.postMessage(
JSON.stringify({
chat: _chat,
modelfiles: $modelfiles.filter((modelfile) =>
_chat.models.includes(modelfile.tagName)
)
}),
'*'
);
}
},
false
);
};
const downloadChat = async () => {
const _chat = chat.chat;
console.log('download', chat);
const chatText = _chat.messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
let blob = new Blob([chatText], {
type: 'text/plain'
});
saveAs(blob, `chat-${_chat.title}.txt`);
};
export let show = false;
onMount(async () => {
chatId.subscribe(async (value) => {
chat = await getChatById(localStorage.token, value);
console.log(chat);
});
});
</script>
<Modal bind:show size="xs">
<Modal bind:show size="sm">
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<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>
<hr class=" dark:border-gray-800" />
{#if chat}
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
<div class=" text-sm dark:text-gray-300 mb-1">
{#if chat.share_id}
<a href="/s/{chat.share_id}" target="_blank"
>You have shared this chat <span class=" underline">before</span>.</a
>
Click here to
<button
class="underline"
on:click={async () => {
const res = await deleteSharedChatById(localStorage.token, $chatId);
if (res) {
chat = await getChatById(localStorage.token, $chatId);
}
}}>delete this link</button
> and create a new shared link.
{:else}
Messages you send after creating your link won't be shared. Users with the URL will be
able to view the shared chat.
{/if}
</div>
<div class="flex justify-end">
<div class="flex flex-col items-end space-x-1 mt-1.5">
<div class="flex gap-1">
<button
class=" self-center px-
8
py-
1.5 w-full
rounded-
ful
l text-sm font-medium bg-
blue-600 hover:bg-blue-500
text-white"
class=" self-center px-
3.5
py-
2
rounded-
x
l text-sm font-medium bg-
gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:
text-white"
type="button"
on:click={() => {
shareChat();
...
...
@@ -23,11 +142,27 @@
{$i18n.t('Share to OpenWebUI Community')}
</button>
<div class="flex justify-center space-x-1 mt-1.5">
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
<button
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
type="button"
on:click={() => {
shareLocalChat();
show = false;
}}
>
<Link />
{#if chat.share_id}
{$i18n.t('Update and Copy Link')}
{:else}
{$i18n.t('Copy Link')}
{/if}
</button>
</div>
<div class="flex gap-1 mt-1.5">
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
<button
class="
self-center
rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
class="
text-right
rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
type="button"
on:click={() => {
downloadChat();
...
...
@@ -38,4 +173,8 @@
</button>
</div>
</div>
</div>
</div>
{/if}
</div>
</Modal>
src/lib/components/icons/Link.svelte
0 → 100644
View file @
f9106cf3
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>
src/lib/components/layout/Navbar.svelte
View file @
f9106cf3
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { Separator } from 'bits-ui';
import { getChatById } from '$lib/apis/chats';
import { getChatById
, shareChatById
} from '$lib/apis/chats';
import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
import { slide } from 'svelte/transition';
...
...
@@ -32,55 +30,13 @@
export let addTag: Function;
export let deleteTag: Function;
export let showModelSelector =
fals
e;
export let showModelSelector =
tru
e;
let showShareChatModal = false;
let showTagChatModal = false;
const shareChat = async () => {
const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('share', chat);
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
const tab = await window.open(`${url}/chats/upload`, '_blank');
window.addEventListener(
'message',
(event) => {
if (event.origin !== url) return;
if (event.data === 'loaded') {
tab.postMessage(
JSON.stringify({
chat: chat,
modelfiles: $modelfiles.filter((modelfile) => chat.models.includes(modelfile.tagName))
}),
'*'
);
}
},
false
);
};
const downloadChat = async () => {
const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('download', chat);
const chatText = chat.messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
let blob = new Blob([chatText], {
type: 'text/plain'
});
saveAs(blob, `chat-${chat.title}.txt`);
};
</script>
<ShareChatModal bind:show={showShareChatModal}
{downloadChat} {shareChat}
/>
<ShareChatModal bind:show={showShareChatModal} />
<!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> -->
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div
...
...
@@ -135,8 +91,10 @@
</div> -->
<div class="flex items-center w-full max-w-full">
<div class="w-full flex-1 overflow-hidden max-w-full">
<div class="flex-1 overflow-hidden max-w-full">
{#if showModelSelector}
<ModelSelector bind:selectedModels />
{/if}
</div>
<div class="self-start flex flex-none items-center">
...
...
src/routes/(app)/+page.svelte
View file @
f9106cf3
...
...
@@ -48,7 +48,7 @@
let
messagesContainerElement
:
HTMLDivElement
;
let
currentRequestId
=
null
;
let
showModelSelector
=
fals
e
;
let
showModelSelector
=
tru
e
;
let
selectedModels
=
[
''
];
let
selectedModelfile
=
null
;
...
...
src/routes/(app)/c/[id]/+page.svelte
View file @
f9106cf3
...
...
@@ -56,7 +56,7 @@
let
currentRequestId
=
null
;
//
let
chatId
=
$
page
.
params
.
id
;
let
showModelSelector
=
fals
e
;
let
showModelSelector
=
tru
e
;
let
selectedModels
=
[
''
];
let
selectedModelfile
=
null
;
...
...
src/routes/s/[id]/+page.svelte
0 → 100644
View file @
f9106cf3
<script lang="ts">
import { onMount, tick, getContext } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import dayjs from 'dayjs';
import { modelfiles, settings, chatId, WEBUI_NAME } from '$lib/stores';
import { convertMessagesToHistory } from '$lib/utils';
import { getChatByShareId } from '$lib/apis/chats';
import Messages from '$lib/components/chat/Messages.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
const i18n = getContext('i18n');
let loaded = false;
let autoScroll = true;
let processing = '';
let messagesContainerElement: HTMLDivElement;
// let chatId = $page.params.id;
let showModelSelector = false;
let selectedModels = [''];
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 title = '';
let files = [];
let messages = [];
let history = {
messages: {},
currentId: null
};
$: if (history.currentId !== null) {
let _messages = [];
let currentMessage = history.messages[history.currentId];
while (currentMessage !== null) {
_messages.unshift({ ...currentMessage });
currentMessage =
currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
}
messages = _messages;
} else {
messages = [];
}
$: if ($page.params.id) {
(async () => {
if (await loadSharedChat()) {
await tick();
loaded = true;
window.setTimeout(() => scrollToBottom(), 0);
const chatInput = document.getElementById('chat-textarea');
chatInput?.focus();
} else {
await goto('/');
}
})();
}
//////////////////////////
// Web functions
//////////////////////////
const loadSharedChat = async () => {
await chatId.set($page.params.id);
chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => {
await goto('/');
return null;
});
if (chat) {
const chatContent = chat.chat;
if (chatContent) {
console.log(chatContent);
selectedModels =
(chatContent?.models ?? undefined) !== undefined
? chatContent.models
: [chatContent.models ?? ''];
history =
(chatContent?.history ?? undefined) !== undefined
? chatContent.history
: convertMessagesToHistory(chatContent.messages);
title = chatContent.title;
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
await settings.set({
..._settings,
system: chatContent.system ?? _settings.system,
options: chatContent.options ?? _settings.options
});
autoScroll = true;
await tick();
if (messages.length > 0) {
history.messages[messages.at(-1).id].done = true;
}
await tick();
return true;
} else {
return null;
}
}
};
</script>
<svelte:head>
<title>
{title
? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
: `${$WEBUI_NAME}`}
</title>
</svelte:head>
{#if loaded}
<div
class="min-h-screen max-h-screen w-full flex flex-col text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900"
>
<div class="flex flex-col flex-auto justify-center py-8">
<div class="px-3 w-full max-w-3xl mx-auto">
<div>
<div class=" text-3xl font-semibold line-clamp-1">
{title}
</div>
<div class=" mt-1 text-gray-400">
{dayjs(chat.chat.timestamp).format('MMMM D, YYYY')}
</div>
</div>
<hr class=" dark:border-gray-800 mt-6 mb-2" />
</div>
<div
class=" flex flex-col justify-center w-full flex-auto overflow-auto h-0"
id="messages-container"
>
<div class=" h-full w-full flex flex-col py-4">
<div class="py-2">
<Messages
chatId={$chatId}
readOnly={true}
{selectedModels}
{selectedModelfiles}
{processing}
bind:history
bind:messages
bind:autoScroll
bottomPadding={files.length > 0}
sendPrompt={() => {}}
continueGeneration={() => {}}
regenerateResponse={() => {}}
/>
</div>
</div>
</div>
</div>
</div>
{/if}
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