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
b12edb4a
Commit
b12edb4a
authored
Apr 20, 2024
by
Timothy J. Baek
Browse files
refac: replace timestamp field
parent
50e8979c
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
240 additions
and
19 deletions
+240
-19
backend/apps/web/internal/migrations/005_add_updated_at.py
backend/apps/web/internal/migrations/005_add_updated_at.py
+77
-0
backend/apps/web/models/chats.py
backend/apps/web/models/chats.py
+33
-10
backend/apps/web/routers/chats.py
backend/apps/web/routers/chats.py
+12
-0
src/lib/apis/chats/index.ts
src/lib/apis/chats/index.ts
+31
-0
src/lib/components/common/Modal.svelte
src/lib/components/common/Modal.svelte
+3
-1
src/lib/components/layout/Sidebar.svelte
src/lib/components/layout/Sidebar.svelte
+24
-8
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
+60
-0
No files found.
backend/apps/web/internal/migrations/005_add_updated_at.py
0 → 100644
View file @
b12edb4a
"""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."""
# Adding fields created_at and updated_at to the 'chat' table
migrator
.
add_fields
(
"chat"
,
created_at
=
pw
.
DateTimeField
(
null
=
True
),
# Allow null for transition
updated_at
=
pw
.
DateTimeField
(
null
=
True
),
# Allow null for transition
)
# Populate the new fields from an existing 'timestamp' field
migrator
.
sql
(
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
)
# Now that the data has been copied, remove the original 'timestamp' field
migrator
.
remove_fields
(
"chat"
,
"timestamp"
)
# Update the fields to be not null now that they are populated
migrator
.
change_fields
(
"chat"
,
created_at
=
pw
.
DateTimeField
(
null
=
False
),
updated_at
=
pw
.
DateTimeField
(
null
=
False
),
)
def
rollback
(
migrator
:
Migrator
,
database
:
pw
.
Database
,
*
,
fake
=
False
):
"""Write your rollback migrations here."""
# Recreate the timestamp field initially allowing null values for safe transition
migrator
.
add_fields
(
"chat"
,
timestamp
=
pw
.
DateTimeField
(
null
=
True
))
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator
.
sql
(
"UPDATE chat SET timestamp = created_at"
)
# Remove the created_at and updated_at fields
migrator
.
remove_fields
(
"chat"
,
"created_at"
,
"updated_at"
)
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator
.
change_fields
(
"chat"
,
timestamp
=
pw
.
DateTimeField
(
null
=
False
))
backend/apps/web/models/chats.py
View file @
b12edb4a
...
@@ -19,7 +19,10 @@ class Chat(Model):
...
@@ -19,7 +19,10 @@ class Chat(Model):
user_id
=
CharField
()
user_id
=
CharField
()
title
=
CharField
()
title
=
CharField
()
chat
=
TextField
()
# Save Chat JSON as Text
chat
=
TextField
()
# Save Chat JSON as Text
timestamp
=
DateField
()
created_at
=
DateTimeField
()
updated_at
=
DateTimeField
()
share_id
=
CharField
(
null
=
True
,
unique
=
True
)
share_id
=
CharField
(
null
=
True
,
unique
=
True
)
archived
=
BooleanField
(
default
=
False
)
archived
=
BooleanField
(
default
=
False
)
...
@@ -32,7 +35,10 @@ class ChatModel(BaseModel):
...
@@ -32,7 +35,10 @@ class ChatModel(BaseModel):
user_id
:
str
user_id
:
str
title
:
str
title
:
str
chat
:
str
chat
:
str
timestamp
:
int
# timestamp in epoch
created_at
:
int
# timestamp in epoch
updated_at
:
int
# timestamp in epoch
share_id
:
Optional
[
str
]
=
None
share_id
:
Optional
[
str
]
=
None
archived
:
bool
=
False
archived
:
bool
=
False
...
@@ -55,13 +61,16 @@ class ChatResponse(BaseModel):
...
@@ -55,13 +61,16 @@ class ChatResponse(BaseModel):
user_id
:
str
user_id
:
str
title
:
str
title
:
str
chat
:
dict
chat
:
dict
timestamp
:
int
# timestamp in epoch
updated_at
:
int
# timestamp in epoch
created_at
:
int
# timestamp in epoch
share_id
:
Optional
[
str
]
=
None
# id of the chat to be shared
share_id
:
Optional
[
str
]
=
None
# id of the chat to be shared
class
ChatTitleIdResponse
(
BaseModel
):
class
ChatTitleIdResponse
(
BaseModel
):
id
:
str
id
:
str
title
:
str
title
:
str
updated_at
:
int
created_at
:
int
class
ChatTable
:
class
ChatTable
:
...
@@ -79,7 +88,8 @@ class ChatTable:
...
@@ -79,7 +88,8 @@ class ChatTable:
form_data
.
chat
[
"title"
]
if
"title"
in
form_data
.
chat
else
"New Chat"
form_data
.
chat
[
"title"
]
if
"title"
in
form_data
.
chat
else
"New Chat"
),
),
"chat"
:
json
.
dumps
(
form_data
.
chat
),
"chat"
:
json
.
dumps
(
form_data
.
chat
),
"timestamp"
:
int
(
time
.
time
()),
"created_at"
:
int
(
time
.
time
()),
"updated_at"
:
int
(
time
.
time
()),
}
}
)
)
...
@@ -91,7 +101,7 @@ class ChatTable:
...
@@ -91,7 +101,7 @@ class ChatTable:
query
=
Chat
.
update
(
query
=
Chat
.
update
(
chat
=
json
.
dumps
(
chat
),
chat
=
json
.
dumps
(
chat
),
title
=
chat
[
"title"
]
if
"title"
in
chat
else
"New Chat"
,
title
=
chat
[
"title"
]
if
"title"
in
chat
else
"New Chat"
,
timestamp
=
int
(
time
.
time
()),
updated_at
=
int
(
time
.
time
()),
).
where
(
Chat
.
id
==
id
)
).
where
(
Chat
.
id
==
id
)
query
.
execute
()
query
.
execute
()
...
@@ -113,7 +123,7 @@ class ChatTable:
...
@@ -113,7 +123,7 @@ class ChatTable:
"user_id"
:
f
"shared-
{
chat_id
}
"
,
"user_id"
:
f
"shared-
{
chat_id
}
"
,
"title"
:
chat
.
title
,
"title"
:
chat
.
title
,
"chat"
:
chat
.
chat
,
"chat"
:
chat
.
chat
,
"
timestamp
"
:
int
(
time
.
time
()),
"
created_at
"
:
int
(
time
.
time
()),
}
}
)
)
shared_result
=
Chat
.
create
(
**
shared_chat
.
model_dump
())
shared_result
=
Chat
.
create
(
**
shared_chat
.
model_dump
())
...
@@ -179,6 +189,19 @@ class ChatTable:
...
@@ -179,6 +189,19 @@ class ChatTable:
except
:
except
:
return
None
return
None
def
get_archived_chat_lists_by_user_id
(
self
,
user_id
:
str
,
skip
:
int
=
0
,
limit
:
int
=
50
)
->
List
[
ChatModel
]:
return
[
ChatModel
(
**
model_to_dict
(
chat
))
for
chat
in
Chat
.
select
()
.
where
(
Chat
.
archived
==
True
)
.
where
(
Chat
.
user_id
==
user_id
)
.
order_by
(
Chat
.
updated_at
.
desc
())
# .limit(limit)
# .offset(skip)
]
def
get_chat_lists_by_user_id
(
def
get_chat_lists_by_user_id
(
self
,
user_id
:
str
,
skip
:
int
=
0
,
limit
:
int
=
50
self
,
user_id
:
str
,
skip
:
int
=
0
,
limit
:
int
=
50
)
->
List
[
ChatModel
]:
)
->
List
[
ChatModel
]:
...
@@ -187,7 +210,7 @@ class ChatTable:
...
@@ -187,7 +210,7 @@ class ChatTable:
for
chat
in
Chat
.
select
()
for
chat
in
Chat
.
select
()
.
where
(
Chat
.
archived
==
False
)
.
where
(
Chat
.
archived
==
False
)
.
where
(
Chat
.
user_id
==
user_id
)
.
where
(
Chat
.
user_id
==
user_id
)
.
order_by
(
Chat
.
timestamp
.
desc
())
.
order_by
(
Chat
.
updated_at
.
desc
())
# .limit(limit)
# .limit(limit)
# .offset(skip)
# .offset(skip)
]
]
...
@@ -200,7 +223,7 @@ class ChatTable:
...
@@ -200,7 +223,7 @@ class ChatTable:
for
chat
in
Chat
.
select
()
for
chat
in
Chat
.
select
()
.
where
(
Chat
.
archived
==
False
)
.
where
(
Chat
.
archived
==
False
)
.
where
(
Chat
.
id
.
in_
(
chat_ids
))
.
where
(
Chat
.
id
.
in_
(
chat_ids
))
.
order_by
(
Chat
.
timestamp
.
desc
())
.
order_by
(
Chat
.
updated_at
.
desc
())
]
]
def
get_all_chats
(
self
)
->
List
[
ChatModel
]:
def
get_all_chats
(
self
)
->
List
[
ChatModel
]:
...
@@ -208,7 +231,7 @@ class ChatTable:
...
@@ -208,7 +231,7 @@ class ChatTable:
ChatModel
(
**
model_to_dict
(
chat
))
ChatModel
(
**
model_to_dict
(
chat
))
for
chat
in
Chat
.
select
()
for
chat
in
Chat
.
select
()
.
where
(
Chat
.
archived
==
False
)
.
where
(
Chat
.
archived
==
False
)
.
order_by
(
Chat
.
timestamp
.
desc
())
.
order_by
(
Chat
.
updated_at
.
desc
())
]
]
def
get_all_chats_by_user_id
(
self
,
user_id
:
str
)
->
List
[
ChatModel
]:
def
get_all_chats_by_user_id
(
self
,
user_id
:
str
)
->
List
[
ChatModel
]:
...
@@ -217,7 +240,7 @@ class ChatTable:
...
@@ -217,7 +240,7 @@ class ChatTable:
for
chat
in
Chat
.
select
()
for
chat
in
Chat
.
select
()
.
where
(
Chat
.
archived
==
False
)
.
where
(
Chat
.
archived
==
False
)
.
where
(
Chat
.
user_id
==
user_id
)
.
where
(
Chat
.
user_id
==
user_id
)
.
order_by
(
Chat
.
timestamp
.
desc
())
.
order_by
(
Chat
.
updated_at
.
desc
())
]
]
def
get_chat_by_id
(
self
,
id
:
str
)
->
Optional
[
ChatModel
]:
def
get_chat_by_id
(
self
,
id
:
str
)
->
Optional
[
ChatModel
]:
...
...
backend/apps/web/routers/chats.py
View file @
b12edb4a
...
@@ -47,6 +47,18 @@ async def get_user_chats(
...
@@ -47,6 +47,18 @@ async def get_user_chats(
return
Chats
.
get_chat_lists_by_user_id
(
user
.
id
,
skip
,
limit
)
return
Chats
.
get_chat_lists_by_user_id
(
user
.
id
,
skip
,
limit
)
############################
# GetArchivedChats
############################
@
router
.
get
(
"/archived"
,
response_model
=
List
[
ChatTitleIdResponse
])
async
def
get_archived_user_chats
(
user
=
Depends
(
get_current_user
),
skip
:
int
=
0
,
limit
:
int
=
50
):
return
Chats
.
get_archived_chat_lists_by_user_id
(
user
.
id
,
skip
,
limit
)
############################
############################
# GetAllChats
# GetAllChats
############################
############################
...
...
src/lib/apis/chats/index.ts
View file @
b12edb4a
...
@@ -62,6 +62,37 @@ export const getChatList = async (token: string = '') => {
...
@@ -62,6 +62,37 @@ export const getChatList = async (token: string = '') => {
return
res
;
return
res
;
};
};
export
const
getArchivedChatList
=
async
(
token
:
string
=
''
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/chats/archived`
,
{
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
getAllChats
=
async
(
token
:
string
)
=>
{
export
const
getAllChats
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
let
error
=
null
;
...
...
src/lib/components/common/Modal.svelte
View file @
b12edb4a
...
@@ -15,8 +15,10 @@
...
@@ -15,8 +15,10 @@
return 'w-[16rem]';
return 'w-[16rem]';
} else if (size === 'sm') {
} else if (size === 'sm') {
return 'w-[30rem]';
return 'w-[30rem]';
} else {
} else
if (size === 'md')
{
return 'w-[44rem]';
return 'w-[44rem]';
} else {
return 'w-[48rem]';
}
}
};
};
...
...
src/lib/components/layout/Sidebar.svelte
View file @
b12edb4a
...
@@ -27,6 +27,7 @@
...
@@ -27,6 +27,7 @@
import ChatMenu from './Sidebar/ChatMenu.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte';
import ShareChatModal from '../chat/ShareChatModal.svelte';
import ShareChatModal from '../chat/ShareChatModal.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte';
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
let show = false;
let show = false;
let navElement;
let navElement;
...
@@ -42,6 +43,7 @@
...
@@ -42,6 +43,7 @@
let chatTitleEditId = null;
let chatTitleEditId = null;
let chatTitle = '';
let chatTitle = '';
let showArchivedChatsModal = false;
let showShareChatModal = false;
let showShareChatModal = false;
let showDropdown = false;
let showDropdown = false;
let isEditing = false;
let isEditing = false;
...
@@ -148,6 +150,7 @@
...
@@ -148,6 +150,7 @@
</script>
</script>
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ArchivedChatsModal bind:show={showArchivedChatsModal} />
<div
<div
bind:this={navElement}
bind:this={navElement}
...
@@ -638,13 +641,13 @@
...
@@ -638,13 +641,13 @@
{#if showDropdown}
{#if showDropdown}
<div
<div
id="dropdownDots"
id="dropdownDots"
class="absolute z-40 bottom-[70px]
4.5rem
rounded-
x
l shadow w-[240px] bg-white dark:bg-gray-900"
class="absolute z-40 bottom-[70px] rounded-l
g
shadow w-[240px] bg-white dark:bg-gray-900"
transition:fade|slide={{ duration: 100 }}
transition:fade|slide={{ duration: 100 }}
>
>
<div class="py-2 w-full">
<div class="
p-1
py-2 w-full">
{#if $user.role === 'admin'}
{#if $user.role === 'admin'}
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex
rounded-md
py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
goto('/admin');
goto('/admin');
showDropdown = false;
showDropdown = false;
...
@@ -670,7 +673,7 @@
...
@@ -670,7 +673,7 @@
</button>
</button>
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex
rounded-md
py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
goto('/playground');
goto('/playground');
showDropdown = false;
showDropdown = false;
...
@@ -697,7 +700,20 @@
...
@@ -697,7 +700,20 @@
{/if}
{/if}
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
showArchivedChatsModal = true;
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<ArchiveBox className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
</button>
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={async () => {
on:click={async () => {
await showSettings.set(true);
await showSettings.set(true);
showDropdown = false;
showDropdown = false;
...
@@ -728,11 +744,11 @@
...
@@ -728,11 +744,11 @@
</button>
</button>
</div>
</div>
<hr class=" dark:border-gray-
7
00 m-0 p-0" />
<hr class=" dark:border-gray-
8
00 m-0 p-0" />
<div class="py-2 w-full">
<div class="
p-1
py-2 w-full">
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class="flex
rounded-md
py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
localStorage.removeItem('token');
localStorage.removeItem('token');
location.href = '/auth';
location.href = '/auth';
...
...
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
0 → 100644
View file @
b12edb4a
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount, getContext } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
import { getArchivedChatList } from '$lib/apis/chats';
const i18n = getContext('i18n');
export let show = false;
let chats = [];
onMount(async () => {
chats = await getArchivedChatList(localStorage.token);
});
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Archived Chats')}</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-850" />
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
{#if chats.length > 0}
<div class="text-left text-sm w-full mb-8">
{#each chats as chat}
<div>
{JSON.stringify(chat)}
</div>
{/each}
</div>
{:else}
<div class="text-left text-sm w-full mb-8">You have no archived conversations.</div>
{/if}
</div>
</div>
</div>
</Modal>
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