kepler-chat/src/routes/chat/[id]/message.svelte
2025-06-17 23:06:09 +01:00

106 lines
2.7 KiB
Svelte

<script lang="ts">
import { cn } from '$lib/utils/utils';
import { tv } from 'tailwind-variants';
import type { Doc } from '$lib/backend/convex/_generated/dataModel';
import { CopyButton } from '$lib/components/ui/copy-button';
import '../../../markdown.css';
import MarkdownRenderer from './markdown-renderer.svelte';
import { ImageModal } from '$lib/components/ui/image-modal';
const style = tv({
base: 'prose rounded-lg p-2',
variants: {
role: {
user: 'bg-primary !text-primary-foreground self-end',
assistant: 'text-foreground',
},
},
});
type Props = {
message: Doc<'messages'>;
};
let { message }: Props = $props();
let imageModal = $state<{ open: boolean; imageUrl: string; fileName: string }>({
open: false,
imageUrl: '',
fileName: ''
});
function openImageModal(imageUrl: string, fileName: string) {
imageModal = {
open: true,
imageUrl,
fileName
};
}
function closeImageModal() {
imageModal = {
open: false,
imageUrl: '',
fileName: ''
};
}
</script>
{#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0)}
<div class={cn('group flex max-w-[80%] flex-col gap-1', { 'self-end': message.role === 'user' })}>
{#if message.images && message.images.length > 0}
<div class="flex flex-wrap gap-2 mb-2">
{#each message.images as image}
<button
type="button"
onclick={() => openImageModal(image.url, image.fileName || 'image')}
class="rounded-lg"
>
<img
src={image.url}
alt={image.fileName || 'Uploaded'}
class="max-w-xs rounded-lg hover:opacity-80 transition-opacity"
/>
</button>
{/each}
</div>
{/if}
<div class={style({ role: message.role })}>
<svelte:boundary>
<MarkdownRenderer content={message.content} />
{#snippet failed(error)}
<div class="text-destructive">
<span>Error rendering markdown:</span>
<pre class="!bg-sidebar"><code>{error instanceof Error ? error.message : String(error)}</code></pre>
</div>
{/snippet}
</svelte:boundary>
</div>
<div
class={cn(
'flex place-items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100',
{
'justify-end': message.role === 'user',
}
)}
>
<CopyButton class="size-7" text={message.content} />
{#if message.model_id !== undefined}
<span class="text-muted-foreground text-xs">{message.model_id}</span>
{/if}
{#if message.cost_usd !== undefined}
<span class="text-muted-foreground text-xs">
${message.cost_usd.toFixed(6)}
</span>
{/if}
</div>
</div>
<ImageModal
bind:open={imageModal.open}
imageUrl={imageModal.imageUrl}
fileName={imageModal.fileName}
onClose={closeImageModal}
/>
{/if}