refac: replace timestamp field

This commit is contained in:
Timothy J. Baek 2024-04-20 18:24:18 -05:00
parent 50e8979c00
commit b12edb4a7a
7 changed files with 240 additions and 19 deletions

View 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))

View file

@ -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]:

View file

@ -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
############################

View file

@ -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;

View file

@ -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]';
}
};

View file

@ -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';

View 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>