Browse Source

refac: replace timestamp field

Timothy J. Baek 1 year ago
parent
commit
b12edb4a7a

+ 77 - 0
backend/apps/web/internal/migrations/005_add_updated_at.py

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

+ 33 - 10
backend/apps/web/models/chats.py

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

+ 12 - 0
backend/apps/web/routers/chats.py

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

+ 31 - 0
src/lib/apis/chats/index.ts

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

+ 3 - 1
src/lib/components/common/Modal.svelte

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

+ 24 - 8
src/lib/components/layout/Sidebar.svelte

@@ -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 - 0
src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte

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