feat: 1-to-1 message render + web data-lake backend

This commit is contained in:
hh committed 2026-05-31 01:45:05 +02:00
1 parent f0afb7ec5b
commit 75425d1bee
110 files changed
+10199 -54

No files matched your search

@@ -0,0 +1,212 @@
<script lang="ts">
import { Dialog } from "bits-ui";
import { getMediaVersions, listMessageVersions } from "$lib/api/endpoints";
import type { MediaVersion, MessageVersion } from "$lib/api/types";
import MediaVersionThumb from "$lib/components/MediaVersionThumb.svelte";
import Icon from "$lib/components/ui/Icon.svelte";
import Spinner from "$lib/components/ui/Spinner.svelte";
import { formatFull } from "$lib/format/datetime";
import { toasts } from "$lib/stores/toasts.svelte";
interface Props {
chatId: number;
messageId: number | null;
open: boolean;
}
let { open = $bindable(), chatId, messageId }: Props = $props();
let versions = $state<MessageVersion[]>([]);
let mediaVersions = $state<MediaVersion[]>([]);
let loading = $state(false);
$effect(() => {
if (open && messageId !== null) {
loading = true;
mediaVersions = [];
const id = messageId;
Promise.all([
listMessageVersions(chatId, id),
getMediaVersions(chatId, id),
])
.then(([text, media]) => {
versions = text;
mediaVersions = media;
})
.catch(() => toasts.error("Failed to load edit history"))
.finally(() => {
loading = false;
});
}
});
</script>
<Dialog.Root bind:open>
<Dialog.Portal>
<Dialog.Overlay class="dialog-overlay" />
<Dialog.Content class="dialog-content">
<header class="dialog-head">
<Dialog.Title class="dialog-title">Edit history</Dialog.Title>
<Dialog.Close class="dialog-close" aria-label="Close">
<Icon name="close" size="1.25rem" />
</Dialog.Close>
</header>
<div class="versions">
{#if loading}
<div class="centered"><Spinner /></div>
{:else if versions.length === 0 && mediaVersions.length === 0}
<p class="empty">No versions recorded.</p>
{:else}
{#if mediaVersions.length > 0}
<div class="media-versions">
<span class="media-label">Media versions</span>
<div class="media-strip">
{#each mediaVersions as media (media.id)}
<MediaVersionThumb version={media} />
{/each}
</div>
</div>
{/if}
{#each versions as version, index (version.observed_at)}
<div class="version">
<div class="version-meta">
<span class="version-label">Version {index + 1}</span>
<span class="version-date"
>{formatFull(version.observed_at)}</span
>
</div>
<div class="version-text">{version.text ?? "(no text)"}</div>
</div>
{/each}
{/if}
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<style lang="scss">
:global(.dialog-overlay) {
position: fixed;
inset: 0;
z-index: var(--z-modal);
background-color: rgba(0, 0, 0, 0.5);
}
:global(.dialog-content) {
position: fixed;
z-index: var(--z-modal);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
width: min(32rem, 92vw);
max-height: 80vh;
border-radius: var(--border-radius-default);
background-color: var(--color-background);
box-shadow: 0 0.5rem 2rem var(--color-default-shadow);
outline: none;
}
.dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-borders);
}
:global(.dialog-title) {
margin: 0;
font-size: 1.125rem;
font-weight: var(--font-weight-medium);
}
:global(.dialog-close) {
cursor: pointer;
display: flex;
padding: 0.375rem;
border: 0;
border-radius: 50%;
color: var(--color-text-secondary);
background-color: transparent;
&:hover {
background-color: var(--color-chat-hover);
}
}
.versions {
overflow-y: auto;
padding: 0.75rem 1.25rem 1.25rem;
}
.media-versions {
padding-bottom: 0.75rem;
margin-bottom: 0.25rem;
border-bottom: 1px solid var(--color-borders);
}
.media-label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.8125rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
}
.media-strip {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.version {
padding: 0.75rem 0;
border-bottom: 1px solid var(--color-borders);
&:last-child {
border-bottom: 0;
}
}
.version-meta {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.version-label {
font-size: 0.8125rem;
font-weight: var(--font-weight-medium);
color: var(--color-primary);
}
.version-date {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.version-text {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.centered {
display: flex;
justify-content: center;
padding: 2rem 0;
}
.empty {
padding: 1rem 0;
color: var(--color-text-secondary);
text-align: center;
}
</style>