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() |     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): | ||||||
|     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): | ||||||
|     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: | ||||||
|                     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: | ||||||
|             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: | ||||||
|                 "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: | ||||||
|         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: | ||||||
|             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: | ||||||
|             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: | ||||||
|             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: | ||||||
|             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]: | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| ############################ | ############################ | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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]'; | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 @@ | ||||||
| 	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 @@ | ||||||
| </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 @@ | ||||||
| 					{#if showDropdown} | 					{#if showDropdown} | ||||||
| 						<div | 						<div | ||||||
| 							id="dropdownDots" | 							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 }} | 							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 @@ | ||||||
| 									</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 @@ | ||||||
| 								{/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 @@ | ||||||
| 								</button> | 								</button> | ||||||
| 							</div> | 							</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 | 								<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'; | ||||||
|  |  | ||||||
							
								
								
									
										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