refac
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
export let forkHandler: Function | null = null;
|
||||
|
||||
export let readOnly = false;
|
||||
export let preview = false;
|
||||
export let editCodeBlock = true;
|
||||
|
||||
export let topPadding = false;
|
||||
@@ -543,6 +544,7 @@
|
||||
{forkHandler}
|
||||
{triggerScroll}
|
||||
{readOnly}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
{onInsertToNote}
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
{#if extracted.detailsContent}
|
||||
<!-- Render structural blocks (tool calls, reasoning, etc.) through Markdown -->
|
||||
<div class="markdown-prose">
|
||||
<Markdown {id} content={extracted.detailsContent} {done} />
|
||||
<Markdown {id} content={extracted.detailsContent} {preview} {done} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if extracted.plainContent}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
export let messageDone = true;
|
||||
export let allowEmbeds = true;
|
||||
export let preview = false;
|
||||
|
||||
let open = $settings?.expandDetails ?? false;
|
||||
|
||||
@@ -112,7 +113,9 @@
|
||||
<div {id} class="w-full">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<button
|
||||
class="w-fit py-0.5 text-left text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer"
|
||||
class="w-fit py-0.5 text-left {preview
|
||||
? 'text-xs'
|
||||
: 'text-[0.9375rem]'} text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer"
|
||||
aria-label={$i18n.t('Toggle details')}
|
||||
aria-expanded={open}
|
||||
on:click={() => {
|
||||
|
||||
@@ -97,6 +97,10 @@
|
||||
.trim();
|
||||
};
|
||||
|
||||
$: detailButtonClassName = `w-fit py-0.5 ${
|
||||
preview ? 'text-xs' : 'text-[0.9375rem]'
|
||||
} text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition`;
|
||||
|
||||
$: displayTokens = getDisplayTokens(tokens);
|
||||
$: singlePlainBlock =
|
||||
displayTokens.length === 1 &&
|
||||
@@ -278,6 +282,7 @@
|
||||
id={`${id}-${tokenIdx}`}
|
||||
tokens={token.tokens}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{onTaskClick}
|
||||
{sourceIds}
|
||||
@@ -313,6 +318,7 @@
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{onTaskClick}
|
||||
{sourceIds}
|
||||
@@ -348,6 +354,7 @@
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{onTaskClick}
|
||||
{sourceIds}
|
||||
@@ -360,6 +367,7 @@
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{onTaskClick}
|
||||
{sourceIds}
|
||||
@@ -375,6 +383,7 @@
|
||||
id={`${id}-${tokenIdx}-detail-group`}
|
||||
tokens={token.items}
|
||||
messageDone={done}
|
||||
{preview}
|
||||
{allowEmbeds}
|
||||
>
|
||||
<div slot="content">
|
||||
@@ -389,7 +398,7 @@
|
||||
grouped={true}
|
||||
open={$settings?.expandDetails ?? false}
|
||||
className="w-full"
|
||||
buttonClassName="w-fit py-0.5 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
buttonClassName={detailButtonClassName}
|
||||
/>
|
||||
{:else if textContent.length > 0}
|
||||
<Collapsible
|
||||
@@ -398,7 +407,7 @@
|
||||
attributes={detailToken?.attributes}
|
||||
messageDone={done}
|
||||
className="w-full"
|
||||
buttonClassName="w-fit py-0.5 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
buttonClassName={detailButtonClassName}
|
||||
dir="auto"
|
||||
>
|
||||
<div class="mb-1.5" slot="content">
|
||||
@@ -407,6 +416,7 @@
|
||||
tokens={marked.lexer(decode(detailToken.text))}
|
||||
attributes={detailToken?.attributes}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{onTaskClick}
|
||||
{sourceIds}
|
||||
@@ -422,7 +432,7 @@
|
||||
attributes={detailToken?.attributes}
|
||||
messageDone={done}
|
||||
className="w-full"
|
||||
buttonClassName="w-fit py-0.5 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
buttonClassName={detailButtonClassName}
|
||||
dir="auto"
|
||||
/>
|
||||
{/if}
|
||||
@@ -440,6 +450,7 @@
|
||||
resultContent={getDetailTextContent(token)}
|
||||
open={$settings?.expandDetails ?? false}
|
||||
className="w-full space-y-2"
|
||||
buttonClassName={detailButtonClassName}
|
||||
/>
|
||||
{:else if textContent.length > 0}
|
||||
<Collapsible
|
||||
@@ -448,6 +459,7 @@
|
||||
attributes={token?.attributes}
|
||||
messageDone={done}
|
||||
className="w-full space-y-2"
|
||||
buttonClassName={detailButtonClassName}
|
||||
dir="auto"
|
||||
>
|
||||
<div class=" mb-1.5" slot="content">
|
||||
@@ -456,6 +468,7 @@
|
||||
tokens={marked.lexer(decode(token.text))}
|
||||
attributes={token?.attributes}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{onTaskClick}
|
||||
{sourceIds}
|
||||
@@ -471,6 +484,7 @@
|
||||
attributes={token?.attributes}
|
||||
messageDone={done}
|
||||
className="w-full space-y-2"
|
||||
buttonClassName={detailButtonClassName}
|
||||
dir="auto"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
export let forkHandler: Function | null = null;
|
||||
export let triggerScroll;
|
||||
export let readOnly = false;
|
||||
export let preview = false;
|
||||
export let editCodeBlock = true;
|
||||
export let topPadding = false;
|
||||
export let onInsertToNote: ((content: string) => void) | null = null;
|
||||
@@ -72,6 +73,7 @@
|
||||
{editMessage}
|
||||
{deleteMessage}
|
||||
{readOnly}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
{onInsertToNote}
|
||||
@@ -100,6 +102,7 @@
|
||||
{addMessages}
|
||||
{forkHandler}
|
||||
{readOnly}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
/>
|
||||
@@ -126,6 +129,7 @@
|
||||
{addMessages}
|
||||
{forkHandler}
|
||||
{readOnly}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
{onInsertToNote}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
export let isLastMessage;
|
||||
export let readOnly = false;
|
||||
export let preview = false;
|
||||
export let editCodeBlock = true;
|
||||
|
||||
export let setInputText: Function = () => {};
|
||||
@@ -251,7 +252,11 @@
|
||||
>
|
||||
{#if $settings?.displayMultiModelResponsesInTabs ?? false}
|
||||
<div class="w-full">
|
||||
<div class=" flex w-full mb-4.5 border-b border-gray-200 dark:border-gray-850">
|
||||
<div
|
||||
class=" flex w-full mb-4.5 border-b border-gray-200 dark:border-gray-850 {preview
|
||||
? 'hidden'
|
||||
: ''}"
|
||||
>
|
||||
<div
|
||||
class="flex gap-2 scrollbar-none overflow-x-auto w-fit text-center font-normal bg-transparent pt-1 text-sm"
|
||||
on:wheel|preventDefault={(e) => {
|
||||
@@ -322,6 +327,7 @@
|
||||
{addMessages}
|
||||
{forkHandler}
|
||||
{readOnly}
|
||||
{preview}
|
||||
{topPadding}
|
||||
{onInsertToNote}
|
||||
/>
|
||||
@@ -338,14 +344,17 @@
|
||||
groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
|
||||
|
||||
<div
|
||||
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
|
||||
?.modelIdx == modelIdx
|
||||
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`
|
||||
: `border-gray-100/30 dark:border-gray-850/30 border-dashed ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`} transition-all p-5 rounded-2xl"
|
||||
class="snap-center w-full max-w-full transition-all {preview
|
||||
? ''
|
||||
: `m-1 border p-5 rounded-2xl ${
|
||||
history.messages[messageId]?.modelIdx == modelIdx
|
||||
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`
|
||||
: `border-gray-100/30 dark:border-gray-850/30 border-dashed ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`
|
||||
}`}"
|
||||
on:click={async () => {
|
||||
onGroupClick(_messageId, modelIdx);
|
||||
}}
|
||||
@@ -380,6 +389,7 @@
|
||||
{addMessages}
|
||||
{forkHandler}
|
||||
{readOnly}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
{onInsertToNote}
|
||||
@@ -392,7 +402,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !readOnly}
|
||||
{#if !preview && !readOnly}
|
||||
{#if !Object.keys(groupedMessageIds).find((modelIdx) => {
|
||||
const { messageIds } = groupedMessageIds[modelIdx];
|
||||
const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
|
||||
export let isLastMessage = true;
|
||||
export let readOnly = false;
|
||||
export let preview = false;
|
||||
export let editCodeBlock = true;
|
||||
export let topPadding = false;
|
||||
export let onInsertToNote: ((content: string) => void) | null = null;
|
||||
@@ -665,13 +666,15 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-auto w-0 pl-1 relative">
|
||||
<Name>
|
||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
||||
<span id="response-message-model-name" class="line-clamp-1 text-black dark:text-white">
|
||||
{model?.name ?? message.model}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Name>
|
||||
{#if !preview}
|
||||
<Name>
|
||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
||||
<span id="response-message-model-name" class="line-clamp-1 text-black dark:text-white">
|
||||
{model?.name ?? message.model}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Name>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class="chat-{message.role} w-full min-w-full">
|
||||
@@ -822,7 +825,7 @@
|
||||
!readOnly &&
|
||||
($settings?.showFloatingActionButtons ?? true)}
|
||||
save={!readOnly}
|
||||
preview={!readOnly}
|
||||
preview={preview || !readOnly}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
done={($settings?.chatFadeStreamingText ?? true)
|
||||
@@ -891,7 +894,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !edit}
|
||||
{#if preview && message.timestamp}
|
||||
<div class="mt-0.5 flex justify-start whitespace-nowrap text-gray-600 dark:text-gray-500">
|
||||
<Tooltip
|
||||
className="flex self-center"
|
||||
content={formatMessageTimestampFull(message.timestamp * 1000)}
|
||||
placement="bottom"
|
||||
>
|
||||
<time
|
||||
datetime={new Date(message.timestamp * 1000).toISOString()}
|
||||
class="ml-1 shrink-0 whitespace-nowrap text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
|
||||
>
|
||||
{formatMessageTimestamp(message.timestamp * 1000)}
|
||||
</time>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{:else if !edit}
|
||||
<div
|
||||
bind:this={buttonsContainerElement}
|
||||
class="flex items-center justify-start overflow-x-auto whitespace-nowrap buttons text-gray-600 dark:text-gray-500 mt-0.5 [&>*]:shrink-0"
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
const getDetailTitle = (detailToken: OutputDetailToken): any => detailToken.summary;
|
||||
const getDetailAttributes = (detailToken: OutputDetailToken): any => detailToken.attributes;
|
||||
|
||||
$: detailButtonClassName = `w-fit py-0.5 ${
|
||||
preview ? 'text-xs' : 'text-[0.9375rem]'
|
||||
} text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition`;
|
||||
|
||||
$: displayItems = buildOutputDisplayItems(output) as OutputDisplayItem[];
|
||||
</script>
|
||||
|
||||
@@ -64,6 +68,7 @@
|
||||
id={`${id}-${displayItem.id}`}
|
||||
tokens={displayItem.tokens}
|
||||
messageDone={done}
|
||||
{preview}
|
||||
>
|
||||
<div slot="content">
|
||||
{#each displayItem.tokens as detailToken, detailIndex}
|
||||
@@ -75,7 +80,7 @@
|
||||
grouped={true}
|
||||
open={$settings?.expandDetails ?? false}
|
||||
className="w-full"
|
||||
buttonClassName="w-fit py-0.5 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
buttonClassName={detailButtonClassName}
|
||||
/>
|
||||
{:else if detailToken.text?.length > 0}
|
||||
<Collapsible
|
||||
@@ -84,7 +89,7 @@
|
||||
attributes={getDetailAttributes(detailToken)}
|
||||
messageDone={done}
|
||||
className="w-full"
|
||||
buttonClassName="w-fit py-0.5 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
buttonClassName={detailButtonClassName}
|
||||
>
|
||||
<div class="mb-1.5" slot="content">
|
||||
<div class="markdown-prose">
|
||||
@@ -92,6 +97,7 @@
|
||||
id={`${id}-${displayItem.id}-${detailIndex}-detail`}
|
||||
content={detailToken.text}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
/>
|
||||
</div>
|
||||
@@ -105,7 +111,7 @@
|
||||
attributes={getDetailAttributes(detailToken)}
|
||||
messageDone={done}
|
||||
className="w-full"
|
||||
buttonClassName="w-fit py-0.5 text-[0.9375rem] text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
buttonClassName={detailButtonClassName}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -120,6 +126,7 @@
|
||||
resultContent={detailToken.text}
|
||||
open={$settings?.expandDetails ?? false}
|
||||
className="w-full space-y-2"
|
||||
buttonClassName={detailButtonClassName}
|
||||
/>
|
||||
{:else if detailToken.text?.length > 0}
|
||||
<Collapsible
|
||||
@@ -128,6 +135,7 @@
|
||||
attributes={getDetailAttributes(detailToken)}
|
||||
messageDone={done}
|
||||
className="w-full space-y-2"
|
||||
buttonClassName={detailButtonClassName}
|
||||
>
|
||||
<div class="mb-1.5" slot="content">
|
||||
<div class="markdown-prose">
|
||||
@@ -135,6 +143,7 @@
|
||||
id={`${id}-${displayItem.id}-detail`}
|
||||
content={detailToken.text}
|
||||
{done}
|
||||
{preview}
|
||||
{editCodeBlock}
|
||||
/>
|
||||
</div>
|
||||
@@ -148,6 +157,7 @@
|
||||
attributes={getDetailAttributes(detailToken)}
|
||||
messageDone={done}
|
||||
className="w-full space-y-2"
|
||||
buttonClassName={detailButtonClassName}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
export let isFirstMessage: boolean;
|
||||
export let readOnly: boolean;
|
||||
export let preview = false;
|
||||
export let editCodeBlock = true;
|
||||
export let topPadding = false;
|
||||
|
||||
@@ -415,7 +416,8 @@
|
||||
>
|
||||
<time
|
||||
datetime={new Date(message.timestamp * 1000).toISOString()}
|
||||
class="invisible group-hover:visible {($settings?.chatBubble ?? true)
|
||||
class="{preview ? '' : 'invisible group-hover:visible'} {($settings?.chatBubble ??
|
||||
true)
|
||||
? 'mr-1'
|
||||
: 'ml-1 shrink-0 whitespace-nowrap'} text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
|
||||
>
|
||||
@@ -424,7 +426,7 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
{#if !preview && !($settings?.chatBubble ?? true)}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" dir="ltr">
|
||||
<button
|
||||
@@ -518,7 +520,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !readOnly}
|
||||
{#if !preview && !readOnly}
|
||||
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||
<button
|
||||
class="{($settings?.highContrastMode ?? false)
|
||||
@@ -546,7 +548,7 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if message?.content}
|
||||
{#if !preview && message?.content}
|
||||
<Tooltip content={$i18n.t('Copy')} placement="bottom">
|
||||
<button
|
||||
class="{($settings?.highContrastMode ?? false)
|
||||
@@ -575,7 +577,7 @@
|
||||
{/if}
|
||||
|
||||
{#if $_user?.role === 'admin' || ($_user?.permissions?.chat?.delete_message ?? false)}
|
||||
{#if !readOnly && (!isFirstMessage || siblings.length > 1)}
|
||||
{#if !preview && !readOnly && (!isFirstMessage || siblings.length > 1)}
|
||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||
<button
|
||||
class="{($settings?.highContrastMode ?? false)
|
||||
@@ -608,7 +610,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $settings?.chatBubble ?? true}
|
||||
{#if !preview && ($settings?.chatBubble ?? true)}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" dir="ltr">
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy, tick } from 'svelte';
|
||||
import { LinkPreview } from 'bits-ui';
|
||||
|
||||
import { getChatById } from '$lib/apis/chats';
|
||||
import Messages from '$lib/components/chat/Messages.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { user } from '$lib/stores';
|
||||
import { createMessagesList } from '$lib/utils';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
|
||||
export let chatId = '';
|
||||
export let title = '';
|
||||
export let openPreview = false;
|
||||
export let side: 'left' | 'right' | 'bottom' | 'top' = 'right';
|
||||
export let align: 'start' | 'end' | 'center' = 'start';
|
||||
export let sideOffset = 12;
|
||||
|
||||
let messagesContainerElement: HTMLElement | null = null;
|
||||
|
||||
let selectedModels = [''];
|
||||
let history: any = null;
|
||||
let previewMessages: any[] = [];
|
||||
let previewReady = false;
|
||||
let loading = false;
|
||||
let error = '';
|
||||
let requestedChatId = '';
|
||||
let loadToken = 0;
|
||||
|
||||
const scrollPreviewToBottom = async () => {
|
||||
await tick();
|
||||
requestAnimationFrame(() => {
|
||||
if (messagesContainerElement) {
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (messagesContainerElement) {
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (messagesContainerElement) {
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
}
|
||||
}, 80);
|
||||
};
|
||||
|
||||
const loadChatPreview = async (id: string) => {
|
||||
if (!id || loading || requestedChatId === id) return;
|
||||
|
||||
const token = ++loadToken;
|
||||
requestedChatId = id;
|
||||
loading = true;
|
||||
error = '';
|
||||
previewReady = false;
|
||||
previewMessages = [];
|
||||
history = null;
|
||||
selectedModels = [''];
|
||||
|
||||
const chat = await getChatById(localStorage.token, id).catch(() => null);
|
||||
if (token !== loadToken) return;
|
||||
|
||||
if (chat?.chat?.history) {
|
||||
selectedModels = Array.isArray(chat.chat.models)
|
||||
? chat.chat.models
|
||||
: [chat.chat.models ?? ''];
|
||||
history = chat.chat.history;
|
||||
previewMessages = createMessagesList(chat.chat.history, chat.chat.history.currentId);
|
||||
previewReady = true;
|
||||
scrollPreviewToBottom();
|
||||
} else if (chat) {
|
||||
previewMessages = [];
|
||||
previewReady = true;
|
||||
} else {
|
||||
error = $i18n.t('Failed to load chat preview');
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
|
||||
$: if (openPreview && chatId && requestedChatId !== chatId) {
|
||||
loadChatPreview(chatId);
|
||||
}
|
||||
|
||||
$: if (openPreview && previewReady) {
|
||||
scrollPreviewToBottom();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
loadToken += 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if openPreview}
|
||||
<LinkPreview.Portal>
|
||||
<LinkPreview.Content
|
||||
class="z-[9999] hidden max-h-[min(17.5rem,calc(100vh-1.5rem))] w-[20rem] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-2xl bg-white shadow-[0_16px_40px_-28px_rgba(0,0,0,0.55)] ring-1 ring-black/5 transition md:block dark:bg-gray-850 dark:text-white dark:ring-white/10"
|
||||
{side}
|
||||
{align}
|
||||
{sideOffset}
|
||||
>
|
||||
<div class="border-b border-gray-50/60 px-3 py-2 dark:border-gray-800/25">
|
||||
<div class="truncate text-[13px] font-medium leading-5 text-gray-900 dark:text-gray-100">
|
||||
{title || $i18n.t('Chat')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
bind:this={messagesContainerElement}
|
||||
class="max-h-[min(15rem,calc(100vh-4rem))] overflow-y-auto bg-white scrollbar-hover @container dark:bg-gray-850"
|
||||
>
|
||||
{#if loading}
|
||||
<div
|
||||
class="flex w-full items-center justify-center py-8 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div
|
||||
class="flex w-full items-center justify-center px-6 py-6 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{:else if previewReady}
|
||||
<Messages
|
||||
className="flex w-full pt-2 pb-0 [&_.message-listitem]:!mb-1 [&_.message-listitem]:!max-w-none [&_.message-listitem]:!px-3 [&_.pb-18]:!pb-1.5 [&_.markdown-prose]:!text-xs [&_.markdown-prose]:!leading-snug [&_.whitespace-pre-wrap]:!text-xs [&_.whitespace-pre-wrap]:!leading-snug [&_.text-\[0\.9375rem\]]:!text-xs [&_.text-sm]:!text-xs [&_.tool-call-body_pre]:!text-[11px] [&_.rounded-3xl]:!rounded-2xl [&_.chat-user_.rounded-3xl]:!bg-gray-50 dark:[&_.chat-user_.rounded-3xl]:!bg-gray-800 [&_.px-4]:!px-3 [&_.py-3]:!py-2 [&_.py-1\.5]:!py-1"
|
||||
chatId={`chat-hover-preview-${chatId}`}
|
||||
user={$user}
|
||||
prompt=""
|
||||
readOnly={true}
|
||||
preview={true}
|
||||
{selectedModels}
|
||||
atSelectedModel={null}
|
||||
{history}
|
||||
autoScroll={true}
|
||||
messagesCount={null}
|
||||
sendMessage={() => {}}
|
||||
continueResponse={() => {}}
|
||||
regenerateResponse={() => {}}
|
||||
mergeResponses={() => {}}
|
||||
chatActionHandler={() => {}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</LinkPreview.Content>
|
||||
</LinkPreview.Portal>
|
||||
{/if}
|
||||
@@ -9,6 +9,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto, invalidate, invalidateAll } from '$app/navigation';
|
||||
import { onMount, getContext, createEventDispatcher, tick } from 'svelte';
|
||||
import { LinkPreview } from 'bits-ui';
|
||||
import {
|
||||
archiveChatById,
|
||||
cloneChatById,
|
||||
@@ -37,6 +38,7 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ChatHoverPreview from './ChatHoverPreview.svelte';
|
||||
import ChatIcon from './icons/Chat.svelte';
|
||||
import MoreHorizontalIcon from './icons/MoreHorizontal.svelte';
|
||||
import SparklesIcon from './icons/Sparkles.svelte';
|
||||
@@ -90,6 +92,7 @@
|
||||
let chat = null;
|
||||
|
||||
let mouseOver = false;
|
||||
let openPreview = false;
|
||||
|
||||
// Local state: tracks the last updatedAt seen while the user was viewing
|
||||
// this chat. Survives prop refreshes from sidebar data re-fetches that
|
||||
@@ -247,6 +250,7 @@
|
||||
|
||||
const onDragStart = (event) => {
|
||||
event.stopPropagation();
|
||||
openPreview = false;
|
||||
|
||||
event.dataTransfer.setDragImage(invisibleDragImage, 0, 0);
|
||||
|
||||
@@ -329,6 +333,7 @@
|
||||
const renameHandler = async () => {
|
||||
chatTitle = title;
|
||||
confirmEdit = true;
|
||||
openPreview = false;
|
||||
|
||||
await tick();
|
||||
|
||||
@@ -444,11 +449,18 @@
|
||||
</DragGhost>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
id="sidebar-chat-group"
|
||||
bind:this={itemElement}
|
||||
class=" w-full {className} relative group"
|
||||
draggable={!confirmEdit && !readonly}
|
||||
on:mouseenter={() => {
|
||||
mouseOver = true;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
mouseOver = false;
|
||||
}}
|
||||
>
|
||||
{#if confirmEdit}
|
||||
<div
|
||||
@@ -490,90 +502,99 @@
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
id="sidebar-chat-item"
|
||||
class=" w-full flex justify-between rounded-xl px-2 py-[6px] {id === $chatId || confirmEdit
|
||||
? ($settings?.highContrastMode ?? false)
|
||||
? 'bg-black/[0.035] dark:bg-white/[0.06] selected'
|
||||
: 'bg-black/[0.035] dark:bg-white/[0.045] selected'
|
||||
: selected
|
||||
? ($settings?.highContrastMode ?? false)
|
||||
? 'bg-black/[0.035] dark:bg-white/[0.055] selected'
|
||||
: 'bg-black/[0.035] dark:bg-white/[0.045] selected'
|
||||
: ' hover:bg-gray-50 dark:hover:bg-gray-900 group-hover:bg-gray-50 dark:group-hover:bg-gray-900'} whitespace-nowrap text-ellipsis transition"
|
||||
href="/c/{id}"
|
||||
on:click={() => {
|
||||
dispatch('select');
|
||||
|
||||
if ($selectedFolder) {
|
||||
selectedFolder.set(null);
|
||||
}
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
|
||||
// Optimistically mark as read in UI when clicked
|
||||
unread = false;
|
||||
lastReadAt = Date.now() / 1000;
|
||||
}}
|
||||
on:dblclick={async (e) => {
|
||||
if (readonly) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
doubleClicked = true;
|
||||
renameHandler();
|
||||
}}
|
||||
on:mouseenter={(e) => {
|
||||
mouseOver = true;
|
||||
}}
|
||||
on:mouseleave={(e) => {
|
||||
mouseOver = false;
|
||||
}}
|
||||
on:focus={(e) => {}}
|
||||
draggable="false"
|
||||
<LinkPreview.Root
|
||||
openDelay={300}
|
||||
closeDelay={0}
|
||||
disabled={$mobile || confirmEdit || dragged}
|
||||
bind:open={openPreview}
|
||||
>
|
||||
{#if ownerUserId}
|
||||
<Tooltip content={ownerName || 'Unknown'}>
|
||||
<img
|
||||
src="/api/v1/users/{ownerUserId}/profile/image"
|
||||
alt=""
|
||||
class="size-3.5 rounded-full shrink-0 object-cover mr-1.5"
|
||||
/>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
<LinkPreview.Trigger
|
||||
id="sidebar-chat-item"
|
||||
class=" w-full flex justify-between rounded-xl px-2 py-[6px] {id === $chatId || confirmEdit
|
||||
? ($settings?.highContrastMode ?? false)
|
||||
? 'bg-black/[0.035] dark:bg-white/[0.06] selected'
|
||||
: 'bg-black/[0.035] dark:bg-white/[0.045] selected'
|
||||
: selected
|
||||
? ($settings?.highContrastMode ?? false)
|
||||
? 'bg-black/[0.035] dark:bg-white/[0.055] selected'
|
||||
: 'bg-black/[0.035] dark:bg-white/[0.045] selected'
|
||||
: ' hover:bg-gray-50 dark:hover:bg-gray-900 group-hover:bg-gray-50 dark:group-hover:bg-gray-900'} whitespace-nowrap text-ellipsis transition"
|
||||
href="/c/{id}"
|
||||
onclick={() => {
|
||||
openPreview = false;
|
||||
dispatch('select');
|
||||
|
||||
<!-- Loading spinner for active chat (left side) -->
|
||||
{#if active}
|
||||
<div class="shrink-0 self-center pr-2">
|
||||
<Spinner className="size-3" />
|
||||
</div>
|
||||
{/if}
|
||||
if ($selectedFolder) {
|
||||
selectedFolder.set(null);
|
||||
}
|
||||
|
||||
<div class="flex self-center flex-1 w-full min-w-0">
|
||||
{#if unread}
|
||||
<div class="shrink-0 self-center pr-2.5 flex transition-opacity duration-300">
|
||||
<div class="size-1.5 bg-sky-500 rounded-full" />
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
|
||||
// Optimistically mark as read in UI when clicked
|
||||
unread = false;
|
||||
lastReadAt = Date.now() / 1000;
|
||||
}}
|
||||
ondblclick={async (e) => {
|
||||
if (readonly) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
doubleClicked = true;
|
||||
renameHandler();
|
||||
}}
|
||||
draggable="false"
|
||||
>
|
||||
{#if ownerUserId}
|
||||
<Tooltip content={ownerName || 'Unknown'}>
|
||||
<img
|
||||
src="/api/v1/users/{ownerUserId}/profile/image"
|
||||
alt=""
|
||||
class="size-3.5 rounded-full shrink-0 object-cover mr-1.5"
|
||||
/>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<!-- Loading spinner for active chat (left side) -->
|
||||
{#if active}
|
||||
<div class="shrink-0 self-center pr-2">
|
||||
<Spinner className="size-3" />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
dir="auto"
|
||||
class="text-left self-center overflow-hidden w-full h-[20px] truncate {unread
|
||||
? 'font-normal text-gray-800 dark:text-gray-200'
|
||||
: ''} {showInlineActions && !readonly ? 'pr-12' : ''}"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time ago indicator -->
|
||||
{#if (updatedAt ?? createdAt) && !showInlineActions}
|
||||
<div class="shrink-0 self-center text-[10px] text-gray-400 dark:text-gray-500 pl-2">
|
||||
{formatTimeAgo(updatedAt ?? createdAt)}
|
||||
<div class="flex self-center flex-1 w-full min-w-0">
|
||||
{#if unread}
|
||||
<div class="shrink-0 self-center pr-2.5 flex transition-opacity duration-300">
|
||||
<div class="size-1.5 bg-sky-500 rounded-full"></div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
dir="auto"
|
||||
class="text-left self-center overflow-hidden w-full h-[20px] truncate {unread
|
||||
? 'font-normal text-gray-800 dark:text-gray-200'
|
||||
: ''} {showInlineActions && !readonly ? 'pr-12' : ''}"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<!-- Time ago indicator -->
|
||||
{#if (updatedAt ?? createdAt) && !showInlineActions}
|
||||
<div class="shrink-0 self-center text-[10px] text-gray-400 dark:text-gray-500 pl-2">
|
||||
{formatTimeAgo((updatedAt ?? createdAt) as number)}
|
||||
</div>
|
||||
{/if}
|
||||
</LinkPreview.Trigger>
|
||||
|
||||
<ChatHoverPreview
|
||||
chatId={id}
|
||||
title={chatTitle || title}
|
||||
{openPreview}
|
||||
side="right"
|
||||
align="center"
|
||||
/>
|
||||
</LinkPreview.Root>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
Reference in New Issue
Block a user