ai / src /lib /components /layout /SearchModal.svelte
github-actions[bot]
GitHub deploy: ed668884346b7a2a626dc61bfc22b31d28f8be5e
55bd140
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext, onDestroy, onMount, tick } from 'svelte';
const i18n = getContext('i18n');
import Modal from '$lib/components/common/Modal.svelte';
import SearchInput from './Sidebar/SearchInput.svelte';
import {
getChatById,
getChatList,
getChatListBySearchText,
searchMessages,
reindexUserChats,
type MessageSearchResult
} from '$lib/apis/chats';
import Spinner from '../common/Spinner.svelte';
import dayjs from '$lib/dayjs';
import calendar from 'dayjs/plugin/calendar';
import Loader from '../common/Loader.svelte';
import { createMessagesList } from '$lib/utils';
import { config, user } from '$lib/stores';
import Messages from '../chat/Messages.svelte';
import { goto } from '$app/navigation';
import PencilSquare from '../icons/PencilSquare.svelte';
import PageEdit from '../icons/PageEdit.svelte';
dayjs.extend(calendar);
export let show = false;
export let onClose = () => {};
let actions = [
{
label: $i18n.t('Start a new conversation'),
onClick: async () => {
await goto(`/${query ? `?q=${query}` : ''}`);
show = false;
onClose();
},
icon: PencilSquare
}
];
let query = '';
let page = 1;
let searchType = 'message'; // 'message' or 'chat' - default to message
let chatList = null;
let messageSearchResponse: SearchResponse | null = null;
let chatListLoading = false;
let allChatsLoaded = false;
let searchDebounceTimeout;
let selectedIdx = null;
let selectedChat = null;
let selectedModels = [''];
let history = null;
let messages = null;
// Search controls
let searchMode = 'smart'; // 'smart', 'exact', 'advanced'
let sortBy = 'relevance'; // 'relevance', 'date'
let groupByConversation = false;
let showFilters = false;
let showPreview = false; // Preview toggle - default off
// Filters
let dateFilter = 'all'; // 'all', 'today', 'week', 'month', 'custom'
let roleFilter = 'all'; // 'all', 'user', 'assistant'
let selectedTags = [];
let selectedFolder = null;
// Reindex state
let isReindexing = false;
let reindexProgress = { indexed: 0, total: 0 };
// Pagination for message results
let messageResultsPage = 1;
const RESULTS_PER_PAGE = 10;
$: displayedMessageResults = messageSearchResponse?.hits.slice(0, messageResultsPage * RESULTS_PER_PAGE) || [];
$: hasMoreResults = messageSearchResponse && displayedMessageResults.length < messageSearchResponse.hits.length;
$: if (!chatListLoading && chatList) {
loadChatPreview(selectedIdx);
}
const loadChatPreview = async (selectedIdx) => {
if (!chatList || chatList.length === 0 || selectedIdx === null) {
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
return;
}
const selectedChatIdx = selectedIdx - actions.length;
if (selectedChatIdx < 0 || selectedChatIdx >= chatList.length) {
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
return;
}
const chatId = chatList[selectedChatIdx].id;
const chat = await getChatById(localStorage.token, chatId).catch(async (error) => {
return null;
});
if (chat) {
if (chat?.chat?.history) {
selectedModels =
(chat?.chat?.models ?? undefined) !== undefined
? chat?.chat?.models
: [chat?.chat?.models ?? ''];
history = chat?.chat?.history;
messages = createMessagesList(chat?.chat?.history, chat?.chat?.history?.currentId);
// scroll to the bottom of the messages container
await tick();
const messagesContainerElement = document.getElementById('chat-preview');
if (messagesContainerElement) {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
} else {
messages = [];
}
} else {
toast.error($i18n.t('Failed to load chat preview'));
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
return;
}
};
const loadMessageChatPreview = async (chatId, messageId) => {
const chat = await getChatById(localStorage.token, chatId).catch(async (error) => {
return null;
});
if (chat) {
if (chat?.chat?.history) {
selectedModels =
(chat?.chat?.models ?? undefined) !== undefined
? chat?.chat?.models
: [chat?.chat?.models ?? ''];
selectedChat = chat;
// Find the leaf of the branch containing the message
let leafId = messageId;
const messagesDict = chat.chat.history.messages;
while (messagesDict[leafId]?.childrenIds?.length > 0) {
leafId = messagesDict[leafId].childrenIds.at(-1);
}
history = {
...chat.chat.history,
currentId: leafId
};
messages = createMessagesList(history, leafId);
// Scroll to the target message
await tick();
const messagesContainerElement = document.getElementById('chat-preview');
if (messagesContainerElement) {
// Try to find and scroll to the specific message
setTimeout(() => {
const messageElement = messagesContainerElement.querySelector(`#message-${messageId}`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
}, 100);
}
} else {
messages = [];
}
}
};
const searchHandler = async () => {
if (!show) {
return;
}
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
}
page = 1;
chatList = null;
messageSearchResponse = null;
chatListLoading = true;
if (query === '') {
if (searchType === 'chat') {
chatList = await getChatList(localStorage.token, page);
} else {
// For message search with empty query, just show empty state
messageSearchResponse = { hits: [], totalHits: 0, processingTimeMs: 0, page: 1, limit: 60, query: '' };
}
chatListLoading = false;
} else {
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
}
searchDebounceTimeout = setTimeout(async () => {
if (searchType === 'chat') {
chatList = await getChatListBySearchText(localStorage.token, query, page);
} else {
// Reset pagination
messageResultsPage = 1;
// Build filter parameters
let filters = {};
if (roleFilter !== 'all') {
filters.role = roleFilter;
}
if (dateFilter !== 'all') {
const now = Math.floor(Date.now() / 1000);
const dayInSeconds = 86400;
let timestampFilter;
if (dateFilter === 'today') {
timestampFilter = now - dayInSeconds;
} else if (dateFilter === 'week') {
timestampFilter = now - (7 * dayInSeconds);
} else if (dateFilter === 'month') {
timestampFilter = now - (30 * dayInSeconds);
}
if (timestampFilter) {
filters.timestamp = Math.floor(timestampFilter);
}
}
if (selectedTags.length > 0) {
filters.tags = selectedTags;
}
if (selectedFolder) {
filters.folderId = selectedFolder;
}
// Modify query for exact match mode
let searchQuery = query;
if (searchMode === 'exact') {
searchQuery = `"${query}"`;
}
// Fetch ALL results from backend
messageSearchResponse = await searchMessages(
localStorage.token,
searchQuery,
page,
sortBy,
filters
).catch((e) => {
console.error('Message search failed:', e);
return null;
});
}
if ((chatList ?? []).length === 0) {
allChatsLoaded = true;
} else {
allChatsLoaded = false;
}
chatListLoading = false;
}, 300);
}
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
if ((chatList ?? []).length === 0) {
allChatsLoaded = true;
} else {
allChatsLoaded = false;
}
};
const loadMoreChats = async () => {
chatListLoading = true;
page += 1;
let newChatList = [];
if (query) {
newChatList = await getChatListBySearchText(localStorage.token, query, page);
} else {
newChatList = await getChatList(localStorage.token, page);
}
// once the bottom of the list has been reached (no results) there is no need to continue querying
allChatsLoaded = newChatList.length === 0;
if (newChatList.length > 0) {
chatList = [...chatList, ...newChatList];
}
chatListLoading = false;
};
$: if (show) {
searchHandler();
}
const onKeyDown = (e) => {
const searchOptions = document.getElementById('search-options-container');
if (searchOptions || !show) {
return;
}
if (e.code === 'Escape') {
show = false;
onClose();
} else if (e.code === 'Enter') {
const item = document.querySelector(`[data-arrow-selected="true"]`);
if (item) {
item?.click();
show = false;
}
return;
} else if (e.code === 'ArrowDown') {
const searchInput = document.getElementById('search-input');
if (searchInput) {
// check if focused on the search input
if (document.activeElement === searchInput) {
searchInput.blur();
selectedIdx = 0;
return;
}
}
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length);
} else if (e.code === 'ArrowUp') {
if (selectedIdx === 0) {
const searchInput = document.getElementById('search-input');
if (searchInput) {
// check if focused on the search input
if (document.activeElement !== searchInput) {
searchInput.focus();
selectedIdx = 0;
return;
}
}
}
selectedIdx = Math.max(selectedIdx - 1, 0);
}
const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
};
onMount(() => {
actions = [
...actions,
...(($config?.features?.enable_notes ?? false) &&
($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))
? [
{
label: $i18n.t('Create a new note'),
onClick: async () => {
await goto(`/notes?content=${query}`);
show = false;
onClose();
},
icon: PageEdit
}
]
: [])
];
document.addEventListener('keydown', onKeyDown);
});
onDestroy(() => {
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
}
document.removeEventListener('keydown', onKeyDown);
});
const handleReindex = async () => {
isReindexing = true;
reindexProgress = { indexed: 0, total: 0 };
try {
const result = await reindexUserChats(localStorage.token);
reindexProgress = result;
toast.success($i18n.t(`Re-indexed ${result.indexed} out of ${result.total} chats successfully`));
// Refresh search results
await searchHandler();
} catch (e) {
toast.error($i18n.t('Failed to re-index chats: {{error}}', { error: e.message || e }));
} finally {
isReindexing = false;
}
};
</script>
<Modal size="xl" bind:show>
<div class="py-3 dark:text-gray-300 text-gray-700">
<div class="px-4 pb-1.5">
<div class="flex items-center justify-center mb-2 text-sm">
<button
class="px-3 py-1 rounded-l-lg {searchType === 'message'
? 'bg-gray-200 dark:bg-gray-700'
: 'bg-gray-100 dark:bg-gray-800'}"
on:click={() => {
searchType = 'message';
searchHandler();
}}
>
{$i18n.t('Messages')}
</button>
<button
class="px-3 py-1 rounded-r-lg {searchType === 'chat'
? 'bg-gray-200 dark:bg-gray-700'
: 'bg-gray-100 dark:bg-gray-800'}"
on:click={() => {
searchType = 'chat';
searchHandler();
}}
>
{$i18n.t('Chats')}
</button>
</div>
{#if searchType === 'message'}
<!-- Compact filter controls -->
<div class="flex items-center gap-2 mb-2 text-xs flex-wrap">
<!-- Search Mode Toggle -->
<div class="flex rounded-md overflow-hidden border border-gray-200 dark:border-gray-700">
<button
class="px-2 py-1 {searchMode === 'smart'
? 'bg-blue-500 text-white'
: 'bg-gray-50 dark:bg-gray-800'}"
on:click={() => {
searchMode = 'smart';
searchHandler();
}}
>
Smart
</button>
<button
class="px-2 py-1 {searchMode === 'exact'
? 'bg-blue-500 text-white'
: 'bg-gray-50 dark:bg-gray-800'}"
on:click={() => {
searchMode = 'exact';
searchHandler();
}}
>
Exact
</button>
</div>
<!-- Sort Toggle -->
<div class="flex rounded-md overflow-hidden border border-gray-200 dark:border-gray-700">
<button
class="px-2 py-1 {sortBy === 'relevance'
? 'bg-blue-500 text-white'
: 'bg-gray-50 dark:bg-gray-800'}"
on:click={() => {
sortBy = 'relevance';
searchHandler();
}}
>
Relevance
</button>
<button
class="px-2 py-1 {sortBy === 'date'
? 'bg-blue-500 text-white'
: 'bg-gray-50 dark:bg-gray-800'}"
on:click={() => {
sortBy = 'date';
searchHandler();
}}
>
Date
</button>
</div>
<!-- Group Toggle -->
<button
class="px-2 py-1 rounded-md border border-gray-200 dark:border-gray-700 {groupByConversation
? 'bg-blue-500 text-white'
: 'bg-gray-50 dark:bg-gray-800'}"
on:click={() => {
groupByConversation = !groupByConversation;
}}
>
Group
</button>
<!-- Filters Toggle -->
<button
class="px-2 py-1 rounded-md border border-gray-200 dark:border-gray-700 {showFilters
? 'bg-blue-500 text-white'
: 'bg-gray-50 dark:bg-gray-800'}"
on:click={() => {
showFilters = !showFilters;
}}
>
Filters {showFilters ? '▼' : '▶'}
</button>
<!-- Preview Toggle -->
<button
class="px-2 py-1 rounded-md border border-gray-200 dark:border-gray-700 {showPreview
? 'bg-blue-500 text-white'
: 'bg-gray-50 dark:bg-gray-800'}"
on:click={() => {
showPreview = !showPreview;
}}
>
Preview
</button>
<!-- Reindex Button -->
<button
class="px-2 py-1 rounded-md border border-gray-200 dark:border-gray-700 bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
on:click={handleReindex}
disabled={isReindexing}
title="Re-index all your chats to update search"
>
{#if isReindexing}
<Spinner className="size-3" />
{reindexProgress.indexed}/{reindexProgress.total}
{:else}
⟳ Re-index
{/if}
</button>
</div>
{#if showFilters}
<div class="p-2 bg-gray-50 dark:bg-gray-850 rounded-md mb-2 text-xs space-y-2">
<!-- Date Filter -->
<div class="flex items-center gap-2">
<label class="text-gray-600 dark:text-gray-400 min-w-[60px]">Date:</label>
<select
bind:value={dateFilter}
on:change={searchHandler}
class="flex-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-2 py-1"
>
<option value="all">All Time</option>
<option value="today">Today</option>
<option value="week">Last 7 Days</option>
<option value="month">Last 30 Days</option>
</select>
</div>
<!-- Role Filter -->
<div class="flex items-center gap-2">
<label class="text-gray-600 dark:text-gray-400 min-w-[60px]">Role:</label>
<select
bind:value={roleFilter}
on:change={searchHandler}
class="flex-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-2 py-1"
>
<option value="all">All Messages</option>
<option value="user">User Only</option>
<option value="assistant">AI Only</option>
</select>
</div>
</div>
{/if}
{/if}
<SearchInput
bind:value={query}
on:input={searchHandler}
placeholder={$i18n.t('Search')}
showClearButton={true}
onFocus={() => {
selectedIdx = null;
messages = null;
}}
onKeydown={(e) => {
console.log('e', e);
if (e.code === 'Enter' && (chatList ?? []).length > 0) {
const item = document.querySelector(`[data-arrow-selected="true"]`);
if (item) {
item?.click();
}
show = false;
return;
} else if (e.code === 'ArrowDown') {
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length);
} else if (e.code === 'ArrowUp') {
selectedIdx = Math.max(selectedIdx - 1, 0);
} else {
selectedIdx = 0;
}
const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}}
/>
</div>
<!-- <hr class="border-gray-50 dark:border-gray-850 my-1" /> -->
<div class="flex px-4 pb-1">
<div
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1 pr-2"
>
<div class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2 px-2">
{$i18n.t('Actions')}
</div>
{#each actions as action, idx (action.label)}
<button
class=" w-full flex items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
idx
? 'bg-gray-50 dark:bg-gray-850'
: ''}"
data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
dragabble="false"
on:mouseenter={() => {
selectedIdx = idx;
}}
on:click={async () => {
await action.onClick();
}}
>
<div class="pr-2">
<svelte:component this={action.icon} />
</div>
<div class=" flex-1 text-left">
<div class="text-ellipsis line-clamp-1 w-full">
{$i18n.t(action.label)}
</div>
</div>
</button>
{/each}
{#if searchType === 'chat'}
{#if chatList}
<hr class="border-gray-50 dark:border-gray-850 my-3" />
{#if chatList.length === 0}
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 py-4">
{$i18n.t('No results found')}
</div>
{/if}
{#each chatList as chat, idx (chat.id)}
{#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
<div
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
? ''
: 'pt-5'} pb-2 px-2"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
{$i18n.t('Today')}
{$i18n.t('Yesterday')}
{$i18n.t('Previous 7 days')}
{$i18n.t('Previous 30 days')}
{$i18n.t('January')}
{$i18n.t('February')}
{$i18n.t('March')}
{$i18n.t('April')}
{$i18n.t('May')}
{$i18n.t('June')}
{$i18n.t('July')}
{$i18n.t('August')}
{$i18n.t('September')}
{$i18n.t('October')}
{$i18n.t('November')}
{$i18n.t('December')}
-->
</div>
{/if}
<a
class=" w-full flex justify-between items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
idx + actions.length
? 'bg-gray-50 dark:bg-gray-850'
: ''}"
href="/c/{chat.id}"
draggable="false"
data-arrow-selected={selectedIdx === idx + actions.length ? 'true' : undefined}
on:mouseenter={() => {
selectedIdx = idx + actions.length;
}}
on:click={async () => {
await goto(`/c/${chat.id}`);
show = false;
onClose();
}}
>
<div class=" flex-1">
<div class="text-ellipsis line-clamp-1 w-full">
{chat?.title}
</div>
</div>
<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
{dayjs(chat?.updated_at * 1000).calendar()}
</div>
</a>
{/each}
{#if !allChatsLoaded}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadMoreChats();
}
}}
>
<div
class="w-full flex justify-center py-4 text-xs animate-pulse items-center gap-2"
>
<Spinner className=" size-4" />
<div class=" ">{$i18n.t('Loading...')}</div>
</div>
</Loader>
{/if}
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />
</div>
{/if}
{:else if searchType === 'message'}
{#if messageSearchResponse}
<hr class="border-gray-50 dark:border-gray-850 my-3" />
{#if messageSearchResponse.hits.length === 0}
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 py-4">
{$i18n.t('No results found')}
</div>
{:else}
<div
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2 px-2 flex justify-between items-center"
>
<span class="font-semibold">Message Results</span>
<span class="text-gray-600 dark:text-gray-400">
<span class="font-semibold text-gray-700 dark:text-gray-300">{messageSearchResponse.estimatedTotalHits || messageSearchResponse.totalHits}</span>
{messageSearchResponse.estimatedTotalHits ? ' ~' : ''} hits in
<span class="font-semibold text-gray-700 dark:text-gray-300">{messageSearchResponse.processingTimeMs}</span>ms
</span>
</div>
{#if groupByConversation}
{@const groupedResults = displayedMessageResults.reduce((groups, item) => {
const key = item.chatId;
if (!groups[key]) {
groups[key] = {
chatId: key,
chatTitle: item.chatTitle || 'Untitled Chat',
messages: []
};
}
groups[key].messages.push(item);
return groups;
}, {})}
{@const groupsSortedByFirstMessage = Object.values(groupedResults).sort((a, b) => {
// When sorting by date, use the timestamp; otherwise use score
// Groups are ordered by their first message (backend already sorted)
const firstA = a.messages[0];
const firstB = b.messages[0];
if (sortBy === 'date') {
return firstB.timestamp - firstA.timestamp; // Newest first
}
return firstB._rankingScore - firstA._rankingScore; // Best match first
})}
{#each groupsSortedByFirstMessage as group (group.chatId)}
<!-- Don't re-sort messages - trust backend order -->
{@const sortedMessages = group.messages}
<div class="mb-3">
<div class="text-xs font-semibold text-gray-700 dark:text-gray-300 px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded-t-lg">
{group.chatTitle} <span class="text-gray-500">({group.messages.length})</span>
</div>
{#each sortedMessages as item (item.id)}
<a
class="w-full block text-sm py-2 px-3 border-l-2 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-850 hover:border-blue-500"
href="/c/{item.chatId}?mid={item.messageId}"
on:mouseenter={() => {
selectedIdx = item.id;
if (showPreview) {
loadMessageChatPreview(item.chatId, item.messageId);
}
}}
on:click={() => {
show = false;
onClose();
}}
data-arrow-selected={selectedIdx === item.id ? 'true' : undefined}
>
<div class="text-ellipsis line-clamp-2 w-full font-medium text-gray-700 dark:text-gray-300 message-highlight"
style="white-space: pre-wrap; word-break: break-word;">
{@html item._formatted?.content || ''}
</div>
<div class="text-xs text-gray-500 mt-0.5 flex justify-between items-center">
<span>{item.role === 'user' ? 'You' : 'AI'}{new Date(item.timestamp * 1000).toLocaleDateString()}</span>
<span>Score: {item._rankingScore.toFixed(2)}</span>
</div>
</a>
{/each}
</div>
{/each}
{:else}
{#each displayedMessageResults as item, idx (item.id)}
<a
class="w-full block rounded-xl text-sm py-2 px-3 my-1 border border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-850 hover:border-blue-500"
href="/c/{item.chatId}?mid={item.messageId}"
on:mouseenter={() => {
selectedIdx = actions.length + idx;
if (showPreview) {
loadMessageChatPreview(item.chatId, item.messageId);
}
}}
on:click={() => {
show = false;
onClose();
}}
data-arrow-selected={selectedIdx === actions.length + idx ? 'true' : undefined}
>
<div class="text-xs text-gray-500 mb-1">{item.chatTitle || 'Untitled Chat'}</div>
<div class="text-ellipsis line-clamp-2 w-full font-medium text-gray-700 dark:text-gray-300 message-highlight"
style="white-space: pre-wrap; word-break: break-word;">
{@html item._formatted?.content || ''}
</div>
<div class="text-xs text-gray-500 mt-1 flex justify-between items-center">
<span>{item.role === 'user' ? 'You' : 'AI'}{new Date(item.timestamp * 1000).toLocaleDateString()}</span>
<span>Score: {item._rankingScore.toFixed(2)}</span>
</div>
</a>
{/each}
{#if hasMoreResults}
<button
class="w-full text-center py-3 text-sm text-blue-500 hover:bg-gray-50 dark:hover:bg-gray-850 rounded-lg"
on:click={() => {
messageResultsPage += 1;
}}
>
Load More ({messageSearchResponse.hits.length - displayedMessageResults.length} remaining)
</button>
{/if}
{/if}
{/if}
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />
</div>
{/if}
{/if}
</div>
<div
id="chat-preview"
class="{showPreview ? 'md:flex' : 'hidden'} md:flex-1 w-full overflow-y-auto h-96 md:h-[40rem] scrollbar-hidden"
>
{#if messages === null}
<div
class="w-full h-full flex justify-center items-center text-gray-500 dark:text-gray-400 text-sm"
>
{$i18n.t('Select a conversation to preview')}
</div>
{:else}
<div class="w-full h-full flex flex-col">
<Messages
className="h-full flex pt-4 pb-8 w-full"
chatId={`chat-preview-${selectedChat?.id ?? ''}`}
user={$user}
readOnly={true}
{selectedModels}
bind:history
bind:messages
autoScroll={true}
sendMessage={() => {}}
continueResponse={() => {}}
regenerateResponse={() => {}}
/>
</div>
{/if}
</div>
</div>
</div>
</Modal>
<style>
:global(.message-highlight mark) {
background: rgba(59, 130, 246, 0.2) !important;
color: inherit;
padding: 2px 4px;
border-radius: 2px;
font-weight: 600;
}
</style>