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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek