diff --git a/web/src/lib/chat/spacetime.svelte.ts b/web/src/lib/chat/spacetime.svelte.ts index 72f631b..0b80f5d 100644 --- a/web/src/lib/chat/spacetime.svelte.ts +++ b/web/src/lib/chat/spacetime.svelte.ts @@ -128,7 +128,7 @@ export function createSpacetimeChat( }) .subscribe([ `SELECT * FROM chat_message WHERE channel_id = '${channelId}'`, - `SELECT mr.* FROM message_reaction mr JOIN chat_message cm ON cm.id = mr.message_id WHERE cm.channel_id = '${channelId}'` + `SELECT * FROM message_reaction` ]); }) .onDisconnect(() => { @@ -153,7 +153,7 @@ export function createSpacetimeChat( conn.db.chat_message.onUpdate((_ctx: EventContext, _oldRow, newRow) => { if (destroyed || newRow.channelId !== channelId) return; messages = messages.map(m => - m.id === newRow.id ? { ...m, body: newRow.body } : m + m.id === newRow.id ? { ...m, body: newRow.body, edited_at: new Date().toISOString() } : m ); }); diff --git a/web/src/lib/components/MessageBox.svelte b/web/src/lib/components/MessageBox.svelte index 2e9c16a..e68d707 100644 --- a/web/src/lib/components/MessageBox.svelte +++ b/web/src/lib/components/MessageBox.svelte @@ -148,46 +148,84 @@ } let isAiProcessing = $derived(message.metadata?.ai_processing === true); - let isAiProcessed = $derived(message.metadata?.ai_processed === true); - // Revisjons-toggle for AI-behandlede meldinger - let showingOriginal = $state(false); - let originalBody = $state(null); - let loadingRevision = $state(false); + // --- Revisjonshistorikk (generell — gjelder alle redigerte meldinger) --- - async function toggleRevision() { - if (showingOriginal) { - showingOriginal = false; - return; - } - if (originalBody === null) { - loadingRevision = true; - try { - const res = await fetch(`/api/messages/${message.id}/revisions`); - if (res.ok) { - const data = await res.json(); - originalBody = data.revisions?.[0]?.body ?? null; - } - } finally { - loadingRevision = false; - } - } - showingOriginal = true; + interface Revision { + id: string; + body: string; + created_at: string; } - // Markdown → HTML for AI-behandlet innhold - function renderBody(body: string, aiProcessed: boolean): string { - if (!aiProcessed) return body; - // AI returnerer markdown — konverter til HTML - const isHtml = /<[a-z][\s\S]*>/i.test(body); + let revisions = $state([]); + let revisionIndex = $state(-1); // -1 = nåværende versjon + let loadingRevisions = $state(false); + let revisionsLoaded = $state(false); + + async function loadRevisions() { + if (revisionsLoaded) return; + loadingRevisions = true; + try { + const res = await fetch(`/api/messages/${message.id}/revisions`); + if (res.ok) { + const data = await res.json(); + revisions = data.revisions ?? []; + } + } finally { + loadingRevisions = false; + revisionsLoaded = true; + } + } + + async function toggleRevisions(e: MouseEvent) { + e.stopPropagation(); + if (revisionIndex >= 0) { + // Tilbake til nåværende + revisionIndex = -1; + return; + } + await loadRevisions(); + if (revisions.length > 0) { + revisionIndex = 0; // Vis nyeste revisjon (original) + } + } + + function prevRevision(e: MouseEvent) { + e.stopPropagation(); + if (revisionIndex < revisions.length - 1) revisionIndex++; + } + + function nextRevision(e: MouseEvent) { + e.stopPropagation(); + if (revisionIndex > 0) revisionIndex--; + else revisionIndex = -1; // Tilbake til nåværende + } + + let showingRevision = $derived(revisionIndex >= 0); + let totalVersions = $derived(revisions.length + 1); // revisjoner + nåværende + let currentVersionNumber = $derived( + revisionIndex < 0 + ? totalVersions + : totalVersions - revisionIndex - 1 + ); + + // --- Body-rendering med markdown-deteksjon --- + + function renderBody(body: string): string { + // Sjekk om body allerede er HTML (fra Tiptap-editor) + const isHtml = /<(?:p|div|br|span|h[1-6]|ul|ol|li|a|strong|em|code)\b/i.test(body); if (isHtml) return body; + // Ellers: behandle som markdown return marked.parse(body, { async: false }) as string; } - let displayBody = $derived( - showingOriginal && originalBody !== null - ? originalBody - : renderBody(message.body, isAiProcessed) + let activeBody = $derived( + showingRevision ? revisions[revisionIndex]?.body ?? message.body : message.body + ); + let displayBody = $derived(renderBody(activeBody)); + + let isEdited = $derived( + !!(message.edited_at || message.metadata?.ai_processed) ); @@ -274,18 +312,29 @@ {#if isAiProcessing}
✨ AI behandler…
{/if} - {#if isAiProcessed && !editing} - {/if} {/if} @@ -564,23 +613,17 @@ text-decoration: underline; } - /* === AI footer (badge + revision toggle) === */ - .messagebox__ai-footer { + /* === Revisjonshistorikk === */ + .messagebox__revision-footer { display: flex; align-items: center; - gap: 0.5rem; - margin-top: 0.3rem; + gap: 0.4rem; + margin-top: 0.2rem; } - .messagebox__ai-badge { + .messagebox__revision-toggle { font-size: 0.65rem; - color: #facc15; - opacity: 0.7; - } - - .messagebox__revision-btn { - font-size: 0.65rem; - color: #7dd3fc; + color: #8b92a5; background: none; border: none; cursor: pointer; @@ -588,16 +631,42 @@ font-family: inherit; } - .messagebox__revision-btn:hover { - color: #bae6fd; - text-decoration: underline; + .messagebox__revision-toggle:hover { + color: #7dd3fc; } - .messagebox__revision-btn:disabled { - color: #8b92a5; + .messagebox__revision-toggle:disabled { cursor: wait; } + .messagebox__revision-nav { + font-size: 0.65rem; + color: #8b92a5; + background: none; + border: 1px solid #2d3148; + border-radius: 3px; + cursor: pointer; + padding: 0 0.3rem; + font-family: inherit; + line-height: 1.4; + } + + .messagebox__revision-nav:hover:not(:disabled) { + color: #7dd3fc; + border-color: #7dd3fc; + } + + .messagebox__revision-nav:disabled { + opacity: 0.3; + cursor: default; + } + + .messagebox__ai-badge { + font-size: 0.65rem; + color: #facc15; + opacity: 0.7; + } + /* Tiptap HTML rendering */ .messagebox__body :global(p) { margin: 0; diff --git a/web/src/lib/types/message.ts b/web/src/lib/types/message.ts index 2274c3c..36f7060 100644 --- a/web/src/lib/types/message.ts +++ b/web/src/lib/types/message.ts @@ -22,6 +22,13 @@ export interface MessageData { all_day: boolean; color: string | null; } | null; + edited_at?: string | null; + revision_count?: number; + metadata?: { + ai_processing?: boolean; + ai_processed?: boolean; + ai_action?: string; + } | null; } export interface ReactionSummary { @@ -42,5 +49,6 @@ export interface MessageBoxCallbacks { onReply?: (messageId: string) => void; onConvertToKanban?: (messageId: string) => void; onConvertToCalendar?: (messageId: string) => void; + onMagic?: (messageId: string) => void; currentUserId?: string; }