synops/frontend/src/lib/components/traits/PodcastTrait.svelte
vegard 263f63bec8 Trait-aware frontend: samlingssider med dynamiske trait-paneler (oppgave 13.2)
Samlingsnoder med `metadata.traits` rendres nå som egne sider på
/collection/[id]. Hvert trait-navn mappes til en dedikert Svelte-komponent
som viser relevant UI. Traits uten egen komponent vises med et generisk panel.

Komponenter for 9 traits: editor, chat, kanban, podcast, publishing,
rss, calendar, recording, transcription. Mottak-siden viser traits som
pills og lenker til samlingssiden.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:20:35 +00:00

80 lines
2.3 KiB
Svelte

<script lang="ts">
import type { Node } from '$lib/spacetime';
import { edgeStore, nodeStore, nodeVisibility } from '$lib/spacetime';
import { casUrl } from '$lib/api';
import AudioPlayer from '$lib/components/AudioPlayer.svelte';
import TraitPanel from './TraitPanel.svelte';
interface Props {
collection: Node;
config: Record<string, unknown>;
userId?: string;
accessToken?: string;
}
let { collection, config, userId, accessToken }: Props = $props();
/** Media nodes (episodes) belonging to this collection */
const episodes = $derived.by(() => {
const nodes: Node[] = [];
for (const edge of edgeStore.byTarget(collection.id)) {
if (edge.edgeType !== 'belongs_to') continue;
const node = nodeStore.get(edge.sourceId);
if (!node || nodeVisibility(node, userId) === 'hidden') continue;
if (node.nodeKind === 'media') {
try {
const meta = JSON.parse(node.metadata ?? '{}');
if (typeof meta.mime === 'string' && meta.mime.startsWith('audio/')) {
nodes.push(node);
}
} catch { /* skip */ }
}
}
nodes.sort((a, b) => {
const ta = a.createdAt?.microsSinceUnixEpoch ?? 0n;
const tb = b.createdAt?.microsSinceUnixEpoch ?? 0n;
return tb > ta ? 1 : tb < ta ? -1 : 0;
});
return nodes;
});
function audioSrc(node: Node): string {
try {
const meta = JSON.parse(node.metadata ?? '{}');
return casUrl(meta.cas_hash);
} catch { return ''; }
}
function audioDuration(node: Node): number | undefined {
try {
const meta = JSON.parse(node.metadata ?? '{}');
return meta.transcription?.duration_ms ? meta.transcription.duration_ms / 1000 : undefined;
} catch { return undefined; }
}
</script>
<TraitPanel name="podcast" label="Podcast" icon="🎙️">
{#snippet children()}
{#if episodes.length === 0}
<p class="text-sm text-gray-400">Ingen episoder ennå.</p>
{:else}
<ul class="space-y-3">
{#each episodes as ep (ep.id)}
<li class="rounded border border-gray-100 p-3">
<h4 class="text-sm font-medium text-gray-900">{ep.title || 'Uten tittel'}</h4>
{#if ep.content}
<p class="mt-0.5 text-xs text-gray-500">{ep.content.slice(0, 100)}</p>
{/if}
<div class="mt-2">
<AudioPlayer
src={audioSrc(ep)}
duration={audioDuration(ep)}
compact
/>
</div>
</li>
{/each}
</ul>
{/if}
{/snippet}
</TraitPanel>