refac: documents settings ui

This commit is contained in:
Timothy Jaeryang Baek 2025-02-26 13:48:56 -08:00
parent 78a8ef8e66
commit fa91d83ac3

View File

@ -241,7 +241,6 @@
}; };
const toggleHybridSearch = async () => { const toggleHybridSearch = async () => {
querySettings.hybrid = !querySettings.hybrid;
querySettings = await updateQuerySettings(localStorage.token, querySettings); querySettings = await updateQuerySettings(localStorage.token, querySettings);
}; };
@ -313,10 +312,14 @@
}} }}
> >
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5"> <div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
<div class="flex flex-col gap-0.5"> <div class="">
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div> <div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<div class=" flex w-full justify-between"> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex flex-col w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
@ -367,77 +370,17 @@
/> />
</div> </div>
{/if} {/if}
{#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'}
<div class="flex mt-0.5 space-x-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="1"
max="2048"
step="1"
bind:value={embeddingBatchSize}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div> </div>
<div class=" mb-2.5 flex flex-col w-full">
<div class=" mb-1 text-xs font-medium">{$i18n.t('Embedding Model')}</div>
<div class=""> <div class="">
<input
bind:value={embeddingBatchSize}
type="number"
class=" bg-transparent text-center w-14"
min="-2"
max="16000"
step="1"
/>
</div>
</div>
{/if}
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleHybridSearch();
}}
type="button"
>
{#if querySettings.hybrid === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
<div class="flex items-center relative">
<Tooltip
content={RAG_FULL_CONTEXT
? 'Inject entire contents as context for comprehensive processing, this is recommended for complex queries.'
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
>
<Switch bind:state={RAG_FULL_CONTEXT} />
</Tooltip>
</div>
</div>
</div>
<hr class="border-gray-100 dark:border-gray-850" />
<div class="space-y-2" />
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Embedding Model')}</div>
{#if embeddingEngine === 'ollama'} {#if embeddingEngine === 'ollama'}
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
bind:value={embeddingModel} bind:value={embeddingModel}
placeholder={$i18n.t('Set embedding model')} placeholder={$i18n.t('Set embedding model')}
required required
@ -448,7 +391,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', { placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40) model: embeddingModel.slice(-40)
})} })}
@ -458,7 +401,7 @@
{#if embeddingEngine === ''} {#if embeddingEngine === ''}
<button <button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-transparent text-gray-800 dark:bg-transparent dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
embeddingModelUpdateHandler(); embeddingModelUpdateHandler();
}} }}
@ -513,21 +456,66 @@
{/if} {/if}
</div> </div>
{/if} {/if}
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-1 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t( {$i18n.t(
'Warning: If you update or change your embedding model, you will need to re-import all documents.' 'Warning: If you update or change your embedding model, you will need to re-import all documents.'
)} )}
</div> </div>
</div>
{#if embeddingEngine === 'ollama' || embeddingEngine === 'openai'}
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
<div class="">
<input
bind:value={embeddingBatchSize}
type="number"
class=" bg-transparent text-center w-14 outline-none"
min="-2"
max="16000"
step="1"
/>
</div>
</div>
{/if}
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
<div class="flex items-center relative">
<Tooltip
content={RAG_FULL_CONTEXT
? 'Inject entire contents as context for comprehensive processing, this is recommended for complex queries.'
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
>
<Switch bind:state={RAG_FULL_CONTEXT} />
</Tooltip>
</div>
</div>
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
<div class="flex items-center relative">
<Switch
bind:state={querySettings.hybrid}
on:change={() => {
toggleHybridSearch();
}}
/>
</div>
</div>
{#if querySettings.hybrid === true} {#if querySettings.hybrid === true}
<div class=" "> <div class=" mb-2.5 flex flex-col w-full">
<div class=" mb-2 text-sm font-medium">{$i18n.t('Reranking Model')}</div> <div class=" mb-1 text-xs font-medium">{$i18n.t('Reranking Model')}</div>
<div class="">
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', { placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3' model: 'BAAI/bge-reranker-v2-m3'
})} })}
@ -535,7 +523,7 @@
/> />
</div> </div>
<button <button
class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-transparent text-gray-800 dark:bg-transparent dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
rerankingModelUpdateHandler(); rerankingModelUpdateHandler();
}} }}
@ -589,22 +577,89 @@
</button> </button>
</div> </div>
</div> </div>
</div>
{/if} {/if}
</div> </div>
<hr class=" border-gray-100 dark:border-gray-850" /> <div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Query')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
<div class="flex items-center relative">
<input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="number"
placeholder={$i18n.t('Enter Top K')}
bind:value={querySettings.k}
autocomplete="off"
min="0"
/>
</div>
</div>
{#if querySettings.hybrid === true}
<div class=" mb-2.5 flex flex-col w-full justify-between">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Minimum Score')}</div>
<div class="flex items-center relative">
<input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="number"
step="0.01"
placeholder={$i18n.t('Enter Score')}
bind:value={querySettings.r}
autocomplete="off"
min="0.0"
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
/>
</div>
</div>
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
)}
</div>
</div>
{/if}
<div class=" mb-2.5 flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-medium">{$i18n.t('RAG Template')}</div>
<div class="flex w-full items-center relative">
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
className="w-full"
>
<Textarea
bind:value={querySettings.template}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
</div>
</div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Content')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex flex-col w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Content Extraction Engine')}
</div>
<div class=""> <div class="">
<div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div>
<div class="flex w-full justify-between">
<div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div>
<div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={contentExtractionEngine} bind:value={contentExtractionEngine}
on:change={(e) => { on:change={(e) => {
showTikaServerUrl = e.target.value === 'tika';
showDocumentIntelligenceConfig = e.target.value === 'document_intelligence'; showDocumentIntelligenceConfig = e.target.value === 'document_intelligence';
}} }}
> >
@ -614,23 +669,30 @@
</select> </select>
</div> </div>
</div> </div>
{#if contentExtractionEngine === ''}
<div class="flex w-full mt-1">
<div class="flex w-full justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
{#if showTikaServerUrl} <div>
<Switch bind:state={pdfExtractImages} />
</div>
</div>
</div>
{:else if contentExtractionEngine === 'tika'}
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Tika Server URL')} placeholder={$i18n.t('Enter Tika Server URL')}
bind:value={tikaServerUrl} bind:value={tikaServerUrl}
/> />
</div> </div>
</div> </div>
{/if} {:else if contentExtractionEngine === 'document_intelligence'}
{#if showDocumentIntelligenceConfig}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Document Intelligence Endpoint')} placeholder={$i18n.t('Enter Document Intelligence Endpoint')}
bind:value={documentIntelligenceEndpoint} bind:value={documentIntelligenceEndpoint}
/> />
@ -643,102 +705,8 @@
{/if} {/if}
</div> </div>
<hr class=" border-gray-100 dark:border-gray-850" /> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
<div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div>
<div class="">
<div class="flex justify-between items-center text-xs">
<div class="text-xs font-medium">{$i18n.t('Enable Google Drive')}</div>
<div>
<Switch bind:state={enableGoogleDriveIntegration} />
</div>
</div>
</div>
<div class="text-sm font-medium mb-1">{$i18n.t('OneDrive')}</div>
<div class="">
<div class="flex justify-between items-center text-xs">
<div class="text-xs font-medium">{$i18n.t('Enable OneDrive')}</div>
<div>
<Switch bind:state={enableOneDriveIntegration} />
</div>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-850" />
<div class=" ">
<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
<div class=" flex gap-1.5">
<div class="flex flex-col w-full gap-1">
<div class=" text-xs font-medium w-full">{$i18n.t('Top K')}</div>
<div class="w-full">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number"
placeholder={$i18n.t('Enter Top K')}
bind:value={querySettings.k}
autocomplete="off"
min="0"
/>
</div>
</div>
{#if querySettings.hybrid === true}
<div class=" flex flex-col w-full gap-1">
<div class="text-xs font-medium w-full">
{$i18n.t('Minimum Score')}
</div>
<div class="w-full">
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number"
step="0.01"
placeholder={$i18n.t('Enter Score')}
bind:value={querySettings.r}
autocomplete="off"
min="0.0"
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
/>
</div>
</div>
{/if}
</div>
{#if querySettings.hybrid === true}
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
)}
</div>
{/if}
<div class="mt-2">
<div class=" mb-1 text-xs font-medium">{$i18n.t('RAG Template')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={querySettings.template}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</Tooltip>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-850" />
<div class=" ">
<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
<div class="flex w-full justify-between mb-1.5">
<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
@ -784,30 +752,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="my-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
<div>
<Switch bind:state={pdfExtractImages} />
</div>
</div>
</div>
</div> </div>
<hr class=" border-gray-100 dark:border-gray-850" /> <div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Files')}</div>
<div class=""> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
<div class=" flex gap-1.5"> <div class=" mb-2.5 flex w-full justify-between">
<div class="w-full"> <div class=" self-center text-xs font-medium">{$i18n.t('Max Upload Size')}</div>
<div class=" self-center text-xs font-medium min-w-fit mb-1"> <div class="flex items-center relative">
{$i18n.t('Max Upload Size')}
</div>
<div class="self-center">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.' 'The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.'
@ -815,7 +769,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={fileMaxSize} bind:value={fileMaxSize}
@ -826,11 +780,9 @@
</div> </div>
</div> </div>
<div class=" w-full"> <div class=" mb-2.5 flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit mb-1"> <div class=" self-center text-xs font-medium">{$i18n.t('Max Upload Count')}</div>
{$i18n.t('Max Upload Count')} <div class="flex items-center relative">
</div>
<div class="self-center">
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.' 'The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.'
@ -838,7 +790,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={fileMaxCount} bind:value={fileMaxCount}
@ -849,65 +801,64 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Integration')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Google Drive')}</div>
<div class="flex items-center relative">
<Switch bind:state={enableGoogleDriveIntegration} />
</div>
</div> </div>
<hr class=" border-gray-100 dark:border-gray-850" /> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('OneDrive')}</div>
<div class="flex items-center relative">
<Switch bind:state={enableOneDriveIntegration} />
</div>
</div>
</div>
<div> <div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Danger Zone')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Reset Upload Directory')}</div>
<div class="flex items-center relative">
<button <button
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class="text-xs"
on:click={() => { on:click={() => {
showResetUploadDirConfirm = true; showResetUploadDirConfirm = true;
}} }}
type="button"
> >
<div class=" self-center mr-3"> {$i18n.t('Reset')}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
clip-rule="evenodd"
/>
<path
d="M14.25 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 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
</button> </button>
</div>
</div>
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Reset Vector Storage/Knowledge')}
</div>
<div class="flex items-center relative">
<button <button
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class="text-xs"
on:click={() => { on:click={() => {
showResetConfirm = true; showResetConfirm = true;
}} }}
type="button"
> >
<div class=" self-center mr-3"> {$i18n.t('Reset')}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('Reset Vector Storage/Knowledge')}
</div>
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"