Merge 4836df97b9 into cc2aa81513
This commit is contained in:
commit
8aff19ba65
|
|
@ -5,6 +5,7 @@
|
|||
ChatMessageStatistics,
|
||||
ChatMessageThinkingBlock,
|
||||
CopyToClipboardIcon,
|
||||
LazyMarkdownContent,
|
||||
MarkdownContent,
|
||||
ModelsSelector
|
||||
} from '$lib/components/app';
|
||||
|
|
@ -243,7 +244,11 @@
|
|||
{:else if message.role === 'assistant'}
|
||||
{#if showRawOutput}
|
||||
<pre class="raw-output">{messageContent || ''}</pre>
|
||||
{:else if message.timestamp && !isLoading()}
|
||||
<!-- Completed message: use lazy loading for performance with many messages -->
|
||||
<LazyMarkdownContent content={messageContent || ''} />
|
||||
{:else}
|
||||
<!-- Streaming message: render immediately -->
|
||||
<MarkdownContent content={messageContent || ''} />
|
||||
{/if}
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Check, X } from '@lucide/svelte';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { MarkdownContent } from '$lib/components/app';
|
||||
import { LazyMarkdownContent } from '$lib/components/app';
|
||||
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import ChatMessageActions from './ChatMessageActions.svelte';
|
||||
|
|
@ -145,7 +145,7 @@
|
|||
>
|
||||
{#if currentConfig.renderUserContentAsMarkdown}
|
||||
<div bind:this={messageElement} class="text-md {isExpanded ? 'cursor-text' : ''}">
|
||||
<MarkdownContent class="markdown-system-content" content={message.content} />
|
||||
<LazyMarkdownContent class="markdown-system-content" content={message.content} />
|
||||
</div>
|
||||
{:else}
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
|
||||
import { ChatAttachmentsList, LazyMarkdownContent, MarkdownContent } from '$lib/components/app';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import ChatMessageActions from './ChatMessageActions.svelte';
|
||||
import ChatMessageEditForm from './ChatMessageEditForm.svelte';
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
>
|
||||
{#if currentConfig.renderUserContentAsMarkdown}
|
||||
<div bind:this={messageElement} class="text-md">
|
||||
<MarkdownContent
|
||||
<LazyMarkdownContent
|
||||
class="markdown-user-content text-primary-foreground"
|
||||
content={message.content}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export { default as BadgeModality } from './misc/BadgeModality.svelte';
|
|||
export { default as ConversationSelection } from './misc/ConversationSelection.svelte';
|
||||
export { default as CopyToClipboardIcon } from './misc/CopyToClipboardIcon.svelte';
|
||||
export { default as KeyboardShortcutInfo } from './misc/KeyboardShortcutInfo.svelte';
|
||||
export { default as LazyMarkdownContent } from './misc/LazyMarkdownContent.svelte';
|
||||
export { default as MarkdownContent } from './misc/MarkdownContent.svelte';
|
||||
export { default as RemoveButton } from './misc/RemoveButton.svelte';
|
||||
export { default as SearchInput } from './misc/SearchInput.svelte';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import MarkdownContent from './MarkdownContent.svelte';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
class?: string;
|
||||
/** If true, render immediately without waiting for intersection */
|
||||
eager?: boolean;
|
||||
/** Placeholder height estimate for layout stability */
|
||||
placeholderHeight?: string;
|
||||
}
|
||||
|
||||
let { content, class: className = '', eager = false, placeholderHeight = 'auto' }: Props = $props();
|
||||
|
||||
let containerRef = $state<HTMLDivElement>();
|
||||
let isVisible = $state(false);
|
||||
let hasBeenVisible = $state(false);
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
// Initialize based on eager prop
|
||||
$effect(() => {
|
||||
if (eager) {
|
||||
isVisible = true;
|
||||
hasBeenVisible = true;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (eager) {
|
||||
hasBeenVisible = true;
|
||||
isVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!containerRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
isVisible = true;
|
||||
hasBeenVisible = true;
|
||||
// Once visible, no need to observe anymore
|
||||
observer?.disconnect();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Start loading slightly before element comes into view
|
||||
rootMargin: '200px 0px',
|
||||
threshold: 0
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(containerRef);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer?.disconnect();
|
||||
});
|
||||
|
||||
// If content is very short, render eagerly (no point lazy loading)
|
||||
$effect(() => {
|
||||
if (content.length < 200 && !hasBeenVisible) {
|
||||
hasBeenVisible = true;
|
||||
isVisible = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={containerRef}
|
||||
class={className}
|
||||
style:min-height={!hasBeenVisible ? placeholderHeight : 'auto'}
|
||||
>
|
||||
{#if hasBeenVisible}
|
||||
<MarkdownContent {content} class={className} />
|
||||
{:else}
|
||||
<!-- Lightweight placeholder: show raw text truncated -->
|
||||
<div class="placeholder-content">
|
||||
{content.slice(0, 500)}{content.length > 500 ? '...' : ''}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.placeholder-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue