Browse Source

enh: files chat control

Timothy J. Baek 9 months ago
parent
commit
4eecdbadd3

+ 23 - 37
src/lib/components/chat/Chat.svelte

@@ -98,6 +98,8 @@
 
 
 	let title = '';
 	let title = '';
 	let prompt = '';
 	let prompt = '';
+
+	let chatFiles = [];
 	let files = [];
 	let files = [];
 	let messages = [];
 	let messages = [];
 	let history = {
 	let history = {
@@ -333,6 +335,7 @@
 				}
 				}
 
 
 				params = chatContent?.params ?? {};
 				params = chatContent?.params ?? {};
+				chatFiles = chatContent?.files ?? {};
 
 
 				autoScroll = true;
 				autoScroll = true;
 				await tick();
 				await tick();
@@ -408,7 +411,8 @@
 					models: selectedModels,
 					models: selectedModels,
 					messages: messages,
 					messages: messages,
 					history: history,
 					history: history,
-					params: params
+					params: params,
+					files: chatFiles
 				});
 				});
 				await chats.set(await getChatList(localStorage.token));
 				await chats.set(await getChatList(localStorage.token));
 			}
 			}
@@ -453,7 +457,8 @@
 					models: selectedModels,
 					models: selectedModels,
 					messages: messages,
 					messages: messages,
 					history: history,
 					history: history,
-					params: params
+					params: params,
+					files: chatFiles
 				});
 				});
 				await chats.set(await getChatList(localStorage.token));
 				await chats.set(await getChatList(localStorage.token));
 			}
 			}
@@ -514,6 +519,13 @@
 			}
 			}
 
 
 			const _files = JSON.parse(JSON.stringify(files));
 			const _files = JSON.parse(JSON.stringify(files));
+			chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
+			chatFiles = chatFiles.filter(
+				// Remove duplicates
+				(item, index, array) =>
+					array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
+			);
+
 			files = [];
 			files = [];
 
 
 			prompt = '';
 			prompt = '';
@@ -754,25 +766,10 @@
 			}
 			}
 		});
 		});
 
 
-		let files = [];
+		let files = JSON.parse(JSON.stringify(chatFiles));
 		if (model?.info?.meta?.knowledge ?? false) {
 		if (model?.info?.meta?.knowledge ?? false) {
-			files = model.info.meta.knowledge;
+			files.push(...model.info.meta.knowledge);
 		}
 		}
-		const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
-
-		files = [
-			...files,
-			...(lastUserMessage?.files?.filter((item) =>
-				['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
-			) ?? []),
-			...(responseMessage?.files?.filter((item) =>
-				['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
-			) ?? [])
-		].filter(
-			// Remove duplicates
-			(item, index, array) =>
-				array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
-		);
 
 
 		eventTarget.dispatchEvent(
 		eventTarget.dispatchEvent(
 			new CustomEvent('chat:start', {
 			new CustomEvent('chat:start', {
@@ -936,7 +933,8 @@
 						messages: messages,
 						messages: messages,
 						history: history,
 						history: history,
 						models: selectedModels,
 						models: selectedModels,
-						params: params
+						params: params,
+						files: chatFiles
 					});
 					});
 					await chats.set(await getChatList(localStorage.token));
 					await chats.set(await getChatList(localStorage.token));
 				}
 				}
@@ -1003,24 +1001,10 @@
 		let _response = null;
 		let _response = null;
 		const responseMessage = history.messages[responseMessageId];
 		const responseMessage = history.messages[responseMessageId];
 
 
-		let files = [];
+		let files = JSON.parse(JSON.stringify(chatFiles));
 		if (model?.info?.meta?.knowledge ?? false) {
 		if (model?.info?.meta?.knowledge ?? false) {
-			files = model.info.meta.knowledge;
+			files.push(...model.info.meta.knowledge);
 		}
 		}
-		const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1);
-		files = [
-			...files,
-			...(lastUserMessage?.files?.filter((item) =>
-				['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
-			) ?? []),
-			...(responseMessage?.files?.filter((item) =>
-				['doc', 'file', 'collection', 'web_search_results'].includes(item.type)
-			) ?? [])
-		].filter(
-			// Remove duplicates
-			(item, index, array) =>
-				array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
-		);
 
 
 		scrollToBottom();
 		scrollToBottom();
 
 
@@ -1214,7 +1198,8 @@
 							models: selectedModels,
 							models: selectedModels,
 							messages: messages,
 							messages: messages,
 							history: history,
 							history: history,
-							params: params
+							params: params,
+							files: chatFiles
 						});
 						});
 						await chats.set(await getChatList(localStorage.token));
 						await chats.set(await getChatList(localStorage.token));
 					}
 					}
@@ -1632,6 +1617,7 @@
 				return a;
 				return a;
 			}, [])}
 			}, [])}
 			bind:show={showControls}
 			bind:show={showControls}
+			bind:chatFiles
 			bind:params
 			bind:params
 			bind:valves
 			bind:valves
 		/>
 		/>

+ 4 - 1
src/lib/components/chat/ChatControls.svelte

@@ -9,8 +9,9 @@
 	export let models = [];
 	export let models = [];
 
 
 	export let chatId = null;
 	export let chatId = null;
-	export let valves = {};
 
 
+	export let chatFiles = [];
+	export let valves = {};
 	export let params = {};
 	export let params = {};
 
 
 	let largeScreen = false;
 	let largeScreen = false;
@@ -48,6 +49,7 @@
 							show = false;
 							show = false;
 						}}
 						}}
 						{models}
 						{models}
+						bind:chatFiles
 						bind:valves
 						bind:valves
 						bind:params
 						bind:params
 					/>
 					/>
@@ -63,6 +65,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 				{models}
 				{models}
+				bind:chatFiles
 				bind:valves
 				bind:valves
 				bind:params
 				bind:params
 			/>
 			/>

+ 28 - 1
src/lib/components/chat/Controls/Controls.svelte

@@ -6,8 +6,11 @@
 	import XMark from '$lib/components/icons/XMark.svelte';
 	import XMark from '$lib/components/icons/XMark.svelte';
 	import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
 	import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
 	import Valves from '$lib/components/common/Valves.svelte';
 	import Valves from '$lib/components/common/Valves.svelte';
+	import FileItem from '$lib/components/common/FileItem.svelte';
 
 
 	export let models = [];
 	export let models = [];
+
+	export let chatFiles = [];
 	export let valves = {};
 	export let valves = {};
 	export let params = {};
 	export let params = {};
 </script>
 </script>
@@ -26,9 +29,33 @@
 	</div>
 	</div>
 
 
 	<div class=" dark:text-gray-200 text-sm font-primary">
 	<div class=" dark:text-gray-200 text-sm font-primary">
+		{#if chatFiles.length > 0}
+			<div>
+				<div class="mb-1.5 font-medium">{$i18n.t('Files')}</div>
+
+				<div>
+					{#each chatFiles as file}
+						<FileItem
+							className="w-full"
+							url={`${file?.url}`}
+							name={file.name}
+							type={file.type}
+							dismissible={true}
+							on:dismiss={() => {
+								// Remove the file from the chatFiles array
+								chatFiles = chatFiles.filter((f) => f.id !== file.id);
+							}}
+						/>
+					{/each}
+				</div>
+			</div>
+
+			<hr class="my-2 border-gray-100 dark:border-gray-800" />
+		{/if}
+
 		{#if models.length === 1 && models[0]?.pipe?.valves_spec}
 		{#if models.length === 1 && models[0]?.pipe?.valves_spec}
 			<div>
 			<div>
-				<div class=" font-medium">Valves</div>
+				<div class=" font-medium">{$i18n.t('Valves')}</div>
 
 
 				<div>
 				<div>
 					<Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves />
 					<Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves />

+ 27 - 124
src/lib/components/chat/MessageInput.svelte

@@ -40,6 +40,7 @@
 	import Headphone from '../icons/Headphone.svelte';
 	import Headphone from '../icons/Headphone.svelte';
 	import VoiceRecording from './MessageInput/VoiceRecording.svelte';
 	import VoiceRecording from './MessageInput/VoiceRecording.svelte';
 	import { transcribeAudio } from '$lib/apis/audio';
 	import { transcribeAudio } from '$lib/apis/audio';
+	import FileItem from '../common/FileItem.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -502,8 +503,8 @@
 							{#if files.length > 0}
 							{#if files.length > 0}
 								<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
 								<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
 									{#each files as file, fileIdx}
 									{#each files as file, fileIdx}
-										<div class=" relative group">
-											{#if file.type === 'image'}
+										{#if file.type === 'image'}
+											<div class=" relative group">
 												<div class="relative">
 												<div class="relative">
 													<img
 													<img
 														src={file.url}
 														src={file.url}
@@ -534,137 +535,39 @@
 														</Tooltip>
 														</Tooltip>
 													{/if}
 													{/if}
 												</div>
 												</div>
-											{:else if ['doc', 'file'].includes(file.type)}
-												<div
-													class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-none"
-												>
-													<div class="p-2.5 bg-red-400 text-white rounded-lg">
-														{#if file.status === 'processed'}
-															<svg
-																xmlns="http://www.w3.org/2000/svg"
-																viewBox="0 0 24 24"
-																fill="currentColor"
-																class="w-6 h-6"
-															>
-																<path
-																	fill-rule="evenodd"
-																	d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
-																	clip-rule="evenodd"
-																/>
-																<path
-																	d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
-																/>
-															</svg>
-														{:else}
-															<svg
-																class=" w-6 h-6 translate-y-[0.5px]"
-																fill="currentColor"
-																viewBox="0 0 24 24"
-																xmlns="http://www.w3.org/2000/svg"
-																><style>
-																	.spinner_qM83 {
-																		animation: spinner_8HQG 1.05s infinite;
-																	}
-																	.spinner_oXPr {
-																		animation-delay: 0.1s;
-																	}
-																	.spinner_ZTLf {
-																		animation-delay: 0.2s;
-																	}
-																	@keyframes spinner_8HQG {
-																		0%,
-																		57.14% {
-																			animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
-																			transform: translate(0);
-																		}
-																		28.57% {
-																			animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
-																			transform: translateY(-6px);
-																		}
-																		100% {
-																			transform: translate(0);
-																		}
-																	}
-																</style><circle
-																	class="spinner_qM83"
-																	cx="4"
-																	cy="12"
-																	r="2.5"
-																/><circle
-																	class="spinner_qM83 spinner_oXPr"
-																	cx="12"
-																	cy="12"
-																	r="2.5"
-																/><circle
-																	class="spinner_qM83 spinner_ZTLf"
-																	cx="20"
-																	cy="12"
-																	r="2.5"
-																/></svg
-															>
-														{/if}
-													</div>
-
-													<div class="flex flex-col justify-center -space-y-0.5">
-														<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
-															{file.name}
-														</div>
-
-														<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
-													</div>
-												</div>
-											{:else if file.type === 'collection'}
-												<div
-													class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-none"
-												>
-													<div class="p-2.5 bg-red-400 text-white rounded-lg">
+												<div class=" absolute -top-1 -right-1">
+													<button
+														class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
+														type="button"
+														on:click={() => {
+															files.splice(fileIdx, 1);
+															files = files;
+														}}
+													>
 														<svg
 														<svg
 															xmlns="http://www.w3.org/2000/svg"
 															xmlns="http://www.w3.org/2000/svg"
-															viewBox="0 0 24 24"
+															viewBox="0 0 20 20"
 															fill="currentColor"
 															fill="currentColor"
-															class="w-6 h-6"
+															class="w-4 h-4"
 														>
 														>
 															<path
 															<path
-																d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
-															/>
-															<path
-																d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
+																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>
 														</svg>
-													</div>
-
-													<div class="flex flex-col justify-center -space-y-0.5">
-														<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
-															{file?.title ?? `#${file.name}`}
-														</div>
-
-														<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
-													</div>
+													</button>
 												</div>
 												</div>
-											{/if}
-
-											<div class=" absolute -top-1 -right-1">
-												<button
-													class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
-													type="button"
-													on:click={() => {
-														files.splice(fileIdx, 1);
-														files = files;
-													}}
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														viewBox="0 0 20 20"
-														fill="currentColor"
-														class="w-4 h-4"
-													>
-														<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>
 											</div>
-										</div>
+										{:else}
+											<FileItem
+												name={file.name}
+												type={file.type}
+												dismissible={true}
+												on:dismiss={() => {
+													files.splice(fileIdx, 1);
+													files = files;
+												}}
+											/>
+										{/if}
 									{/each}
 									{/each}
 								</div>
 								</div>
 							{/if}
 							{/if}

+ 4 - 98
src/lib/components/chat/Messages/UserMessage.svelte

@@ -9,6 +9,7 @@
 
 
 	import { user as _user } from '$lib/stores';
 	import { user as _user } from '$lib/stores';
 	import { getFileContentById } from '$lib/apis/files';
 	import { getFileContentById } from '$lib/apis/files';
+	import FileItem from '$lib/components/common/FileItem.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -99,106 +100,11 @@
 							{#if file.type === 'image'}
 							{#if file.type === 'image'}
 								<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
 								<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
 							{:else if file.type === 'file'}
 							{:else if file.type === 'file'}
-								<button
-									class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
-									type="button"
-									on:click={async () => {
-										if (file?.url) {
-											window.open(`${file?.url}/content`, '_blank').focus();
-										}
-									}}
-								>
-									<div class="p-2.5 bg-red-400 text-white rounded-lg">
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 24 24"
-											fill="currentColor"
-											class="w-6 h-6"
-										>
-											<path
-												fill-rule="evenodd"
-												d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
-												clip-rule="evenodd"
-											/>
-											<path
-												d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
-											/>
-										</svg>
-									</div>
-
-									<div class="flex flex-col justify-center -space-y-0.5">
-										<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
-											{file.name}
-										</div>
-
-										<div class=" text-gray-500 text-sm">{$i18n.t('File')}</div>
-									</div>
-								</button>
+								<FileItem url={`${file?.url}/content`} name={file.name} type={$i18n.t('File')} />
 							{:else if file.type === 'doc'}
 							{:else if file.type === 'doc'}
-								<button
-									class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
-									type="button"
-									on:click={() => {
-										if (file?.url) {
-											window.open(file?.url, '_blank').focus();
-										}
-									}}
-								>
-									<div class="p-2.5 bg-red-400 text-white rounded-lg">
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 24 24"
-											fill="currentColor"
-											class="w-6 h-6"
-										>
-											<path
-												fill-rule="evenodd"
-												d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
-												clip-rule="evenodd"
-											/>
-											<path
-												d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
-											/>
-										</svg>
-									</div>
-
-									<div class="flex flex-col justify-center -space-y-0.5">
-										<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
-											{file.name}
-										</div>
-
-										<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
-									</div>
-								</button>
+								<FileItem url={`${file?.url}`} name={file.name} type={$i18n.t('Document')} />
 							{:else if file.type === 'collection'}
 							{:else if file.type === 'collection'}
-								<button
-									class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
-									type="button"
-								>
-									<div class="p-2.5 bg-red-400 text-white rounded-lg">
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 24 24"
-											fill="currentColor"
-											class="w-6 h-6"
-										>
-											<path
-												d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
-											/>
-											<path
-												d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
-											/>
-										</svg>
-									</div>
-
-									<div class="flex flex-col justify-center -space-y-0.5">
-										<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
-											{file?.title ?? `#${file.name}`}
-										</div>
-
-										<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
-									</div>
-								</button>
+								<FileItem name={file?.title ?? `#${file.name}`} type={$i18n.t('Collection')} />
 							{/if}
 							{/if}
 						</div>
 						</div>
 					{/each}
 					{/each}

+ 130 - 0
src/lib/components/common/FileItem.svelte

@@ -0,0 +1,130 @@
+<script lang="ts">
+	import { createEventDispatcher, getContext } from 'svelte';
+
+	const i18n = getContext('i18n');
+	const dispatch = createEventDispatcher();
+
+	export let className = 'w-72';
+	export let url: string | null = null;
+
+	export let dismissible = false;
+
+	export let status = 'processed';
+
+	export let name: string;
+	export let type: string;
+</script>
+
+<div class="relative group">
+	<button
+		class="h-14 {className} flex items-center space-x-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-100 dark:border-gray-800 text-left"
+		type="button"
+		on:click={async () => {
+			if (url) {
+				if (type === 'file') {
+					window.open(`${url}/content`, '_blank').focus();
+				} else {
+					window.open(`${url}`, '_blank').focus();
+				}
+			}
+		}}
+	>
+		<div class="p-4 py-[1.1rem] bg-red-400 text-white rounded-l-lg">
+			{#if status === 'processed'}
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 24 24"
+					fill="currentColor"
+					class=" size-5"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
+						clip-rule="evenodd"
+					/>
+					<path
+						d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
+					/>
+				</svg>
+			{:else}
+				<svg
+					class=" size-5 translate-y-[0.5px]"
+					fill="currentColor"
+					viewBox="0 0 24 24"
+					xmlns="http://www.w3.org/2000/svg"
+					><style>
+						.spinner_qM83 {
+							animation: spinner_8HQG 1.05s infinite;
+						}
+						.spinner_oXPr {
+							animation-delay: 0.1s;
+						}
+						.spinner_ZTLf {
+							animation-delay: 0.2s;
+						}
+						@keyframes spinner_8HQG {
+							0%,
+							57.14% {
+								animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
+								transform: translate(0);
+							}
+							28.57% {
+								animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
+								transform: translateY(-6px);
+							}
+							100% {
+								transform: translate(0);
+							}
+						}
+					</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
+						class="spinner_qM83 spinner_oXPr"
+						cx="12"
+						cy="12"
+						r="2.5"
+					/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
+				>
+			{/if}
+		</div>
+
+		<div class="flex flex-col justify-center -space-y-0.5 pl-1.5 pr-4 w-full">
+			<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
+				{name}
+			</div>
+
+			<div class=" text-gray-500 text-xs">
+				{#if type === 'file'}
+					{$i18n.t('File')}
+				{:else if type === 'doc'}
+					{$i18n.t('Document')}
+				{:else if type === 'collection'}
+					{$i18n.t('Collection')}
+				{:else}
+					<span class=" capitalize">{type}</span>
+				{/if}
+			</div>
+		</div>
+	</button>
+
+	{#if dismissible}
+		<div class=" absolute -top-1 -right-1">
+			<button
+				class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
+				type="button"
+				on:click={() => {
+					dispatch('dismiss');
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-4 h-4"
+				>
+					<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>
+	{/if}
+</div>