forked from open-webui/open-webui
refac: replace timestamp field
This commit is contained in:
parent
50e8979c00
commit
b12edb4a7a
7 changed files with 240 additions and 19 deletions
77
backend/apps/web/internal/migrations/005_add_updated_at.py
Normal file
77
backend/apps/web/internal/migrations/005_add_updated_at.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
"""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))
|
|
@ -19,7 +19,10 @@ class Chat(Model):
|
|||
user_id = CharField()
|
||||
title = CharField()
|
||||
chat = TextField() # Save Chat JSON as Text
|
||||
timestamp = DateField()
|
||||
|
||||
created_at = DateTimeField()
|
||||
updated_at = DateTimeField()
|
||||
|
||||
share_id = CharField(null=True, unique=True)
|
||||
archived = BooleanField(default=False)
|
||||
|
||||
|
@ -32,7 +35,10 @@ class ChatModel(BaseModel):
|
|||
user_id: str
|
||||
title: 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
|
||||
archived: bool = False
|
||||
|
||||
|
@ -55,13 +61,16 @@ class ChatResponse(BaseModel):
|
|||
user_id: str
|
||||
title: str
|
||||
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
|
||||
|
||||
|
||||
class ChatTitleIdResponse(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
updated_at: int
|
||||
created_at: int
|
||||
|
||||
|
||||
class ChatTable:
|
||||
|
@ -79,7 +88,8 @@ class ChatTable:
|
|||
form_data.chat["title"] if "title" in form_data.chat else "New 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:
|
|||
query = Chat.update(
|
||||
chat=json.dumps(chat),
|
||||
title=chat["title"] if "title" in chat else "New Chat",
|
||||
timestamp=int(time.time()),
|
||||
updated_at=int(time.time()),
|
||||
).where(Chat.id == id)
|
||||
query.execute()
|
||||
|
||||
|
@ -113,7 +123,7 @@ class ChatTable:
|
|||
"user_id": f"shared-{chat_id}",
|
||||
"title": chat.title,
|
||||
"chat": chat.chat,
|
||||
"timestamp": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
shared_result = Chat.create(**shared_chat.model_dump())
|
||||
|
@ -179,6 +189,19 @@ class ChatTable:
|
|||
except:
|
||||
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(
|
||||
self, user_id: str, skip: int = 0, limit: int = 50
|
||||
) -> List[ChatModel]:
|
||||
|
@ -187,7 +210,7 @@ class ChatTable:
|
|||
for chat in Chat.select()
|
||||
.where(Chat.archived == False)
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.timestamp.desc())
|
||||
.order_by(Chat.updated_at.desc())
|
||||
# .limit(limit)
|
||||
# .offset(skip)
|
||||
]
|
||||
|
@ -200,7 +223,7 @@ class ChatTable:
|
|||
for chat in Chat.select()
|
||||
.where(Chat.archived == False)
|
||||
.where(Chat.id.in_(chat_ids))
|
||||
.order_by(Chat.timestamp.desc())
|
||||
.order_by(Chat.updated_at.desc())
|
||||
]
|
||||
|
||||
def get_all_chats(self) -> List[ChatModel]:
|
||||
|
@ -208,7 +231,7 @@ class ChatTable:
|
|||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.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]:
|
||||
|
@ -217,7 +240,7 @@ class ChatTable:
|
|||
for chat in Chat.select()
|
||||
.where(Chat.archived == False)
|
||||
.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]:
|
||||
|
|
|
@ -47,6 +47,18 @@ async def get_user_chats(
|
|||
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
|
||||
############################
|
||||
|
|
|
@ -62,6 +62,37 @@ export const getChatList = async (token: string = '') => {
|
|||
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) => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
return 'w-[16rem]';
|
||||
} else if (size === 'sm') {
|
||||
return 'w-[30rem]';
|
||||
} else {
|
||||
} else if (size === 'md') {
|
||||
return 'w-[44rem]';
|
||||
} else {
|
||||
return 'w-[48rem]';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
import ChatMenu from './Sidebar/ChatMenu.svelte';
|
||||
import ShareChatModal from '../chat/ShareChatModal.svelte';
|
||||
import ArchiveBox from '../icons/ArchiveBox.svelte';
|
||||
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
|
||||
|
||||
let show = false;
|
||||
let navElement;
|
||||
|
@ -42,6 +43,7 @@
|
|||
let chatTitleEditId = null;
|
||||
let chatTitle = '';
|
||||
|
||||
let showArchivedChatsModal = false;
|
||||
let showShareChatModal = false;
|
||||
let showDropdown = false;
|
||||
let isEditing = false;
|
||||
|
@ -148,6 +150,7 @@
|
|||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
|
||||
<ArchivedChatsModal bind:show={showArchivedChatsModal} />
|
||||
|
||||
<div
|
||||
bind:this={navElement}
|
||||
|
@ -638,13 +641,13 @@
|
|||
{#if showDropdown}
|
||||
<div
|
||||
id="dropdownDots"
|
||||
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-white dark:bg-gray-900"
|
||||
class="absolute z-40 bottom-[70px] rounded-lg shadow w-[240px] bg-white dark:bg-gray-900"
|
||||
transition:fade|slide={{ duration: 100 }}
|
||||
>
|
||||
<div class="py-2 w-full">
|
||||
<div class="p-1 py-2 w-full">
|
||||
{#if $user.role === 'admin'}
|
||||
<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={() => {
|
||||
goto('/admin');
|
||||
showDropdown = false;
|
||||
|
@ -670,7 +673,7 @@
|
|||
</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={() => {
|
||||
goto('/playground');
|
||||
showDropdown = false;
|
||||
|
@ -697,7 +700,20 @@
|
|||
{/if}
|
||||
|
||||
<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 () => {
|
||||
await showSettings.set(true);
|
||||
showDropdown = false;
|
||||
|
@ -728,11 +744,11 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 m-0 p-0" />
|
||||
<hr class=" dark:border-gray-800 m-0 p-0" />
|
||||
|
||||
<div class="py-2 w-full">
|
||||
<div class="p-1 py-2 w-full">
|
||||
<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={() => {
|
||||
localStorage.removeItem('token');
|
||||
location.href = '/auth';
|
||||
|
|
60
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
Normal file
60
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte
Normal file
|
@ -0,0 +1,60 @@
|
|||
<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>
|
Loading…
Reference in a new issue