Svelte-komponent for AI-prosessering i arbeidsflaten: - AiToolPanel.svelte: Viser ai_preset-noder fra STDB som velgbare verktøy, med modell-indikator (flash/standard) og prompt-forhåndsvisning - Drag-and-drop mottak for tekstnoder med visuell feedback - Validerer kompatibilitet (kun content/communication-noder) - Kaller /intentions/ai_process via ny aiProcess()-funksjon i api.ts - Integrert i collection-siden, tilgjengelig for alle innloggede brukere - EditorTrait: innholdsnoder er nå draggable for AI-prosessering - Fritekst-felt for egendefinert instruksjon (backend-støtte kommer i 18.6) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
199 lines
7.8 KiB
Svelte
199 lines
7.8 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/stores';
|
|
import { connectionState, nodeStore, edgeStore, nodeVisibility } from '$lib/spacetime';
|
|
import type { Node } from '$lib/spacetime';
|
|
|
|
// Trait components
|
|
import EditorTrait from '$lib/components/traits/EditorTrait.svelte';
|
|
import ChatTrait from '$lib/components/traits/ChatTrait.svelte';
|
|
import KanbanTrait from '$lib/components/traits/KanbanTrait.svelte';
|
|
import PodcastTrait from '$lib/components/traits/PodcastTrait.svelte';
|
|
import PublishingTrait from '$lib/components/traits/PublishingTrait.svelte';
|
|
import RssTrait from '$lib/components/traits/RssTrait.svelte';
|
|
import CalendarTrait from '$lib/components/traits/CalendarTrait.svelte';
|
|
import RecordingTrait from '$lib/components/traits/RecordingTrait.svelte';
|
|
import TranscriptionTrait from '$lib/components/traits/TranscriptionTrait.svelte';
|
|
import StudioTrait from '$lib/components/traits/StudioTrait.svelte';
|
|
import MixerTrait from '$lib/components/traits/MixerTrait.svelte';
|
|
import GenericTrait from '$lib/components/traits/GenericTrait.svelte';
|
|
import TraitAdmin from '$lib/components/traits/TraitAdmin.svelte';
|
|
import NodeUsage from '$lib/components/NodeUsage.svelte';
|
|
import AiToolPanel from '$lib/components/AiToolPanel.svelte';
|
|
|
|
const session = $derived($page.data.session as Record<string, unknown> | undefined);
|
|
const nodeId = $derived(session?.nodeId as string | undefined);
|
|
const accessToken = $derived(session?.accessToken as string | undefined);
|
|
let showTraitAdmin = $state(false);
|
|
const connected = $derived(connectionState.current === 'connected');
|
|
const collectionId = $derived($page.params.id ?? '');
|
|
|
|
const collectionNode = $derived(connected ? nodeStore.get(collectionId) : undefined);
|
|
|
|
/** Parse full metadata */
|
|
const parsedMetadata = $derived.by((): Record<string, unknown> => {
|
|
if (!collectionNode) return {};
|
|
try {
|
|
return JSON.parse(collectionNode.metadata ?? '{}') as Record<string, unknown>;
|
|
} catch { return {}; }
|
|
});
|
|
|
|
/** Parse traits from collection metadata */
|
|
const traits = $derived.by((): Record<string, Record<string, unknown>> => {
|
|
const meta = parsedMetadata;
|
|
if (meta.traits && typeof meta.traits === 'object' && !Array.isArray(meta.traits)) {
|
|
return meta.traits as Record<string, Record<string, unknown>>;
|
|
}
|
|
return {};
|
|
});
|
|
|
|
const traitNames = $derived(Object.keys(traits));
|
|
|
|
/** Traits with dedicated components */
|
|
const knownTraits = new Set([
|
|
'editor', 'chat', 'kanban', 'podcast', 'publishing',
|
|
'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer'
|
|
]);
|
|
|
|
/** Traits that have a dedicated component */
|
|
const renderedTraits = $derived(traitNames.filter(t => knownTraits.has(t)));
|
|
|
|
/** Traits without a dedicated component — shown with generic panel */
|
|
const genericTraits = $derived(traitNames.filter(t => !knownTraits.has(t)));
|
|
|
|
/** Count of child nodes */
|
|
const childCount = $derived.by(() => {
|
|
if (!connected || !collectionId) return 0;
|
|
let count = 0;
|
|
for (const edge of edgeStore.byTarget(collectionId)) {
|
|
if (edge.edgeType === 'belongs_to') count++;
|
|
}
|
|
return count;
|
|
});
|
|
</script>
|
|
|
|
<div class="min-h-screen bg-gray-50">
|
|
<!-- Header -->
|
|
<header class="border-b border-gray-200 bg-white">
|
|
<div class="mx-auto flex max-w-4xl items-center justify-between px-4 py-3">
|
|
<div class="flex items-center gap-3">
|
|
<a href="/" class="text-sm text-gray-400 hover:text-gray-600">← Mottak</a>
|
|
<h1 class="text-lg font-semibold text-gray-900">
|
|
{collectionNode?.title || 'Samling'}
|
|
</h1>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
{#if connected}
|
|
<span class="text-xs text-green-600">Tilkoblet</span>
|
|
{:else}
|
|
<span class="text-xs text-gray-400">{connectionState.current}</span>
|
|
{/if}
|
|
{#if traitNames.length > 0}
|
|
<span class="text-xs text-gray-400">{traitNames.length} traits</span>
|
|
{/if}
|
|
<span class="text-xs text-gray-400">{childCount} noder</span>
|
|
{#if connected && collectionNode && accessToken}
|
|
<button
|
|
onclick={() => { showTraitAdmin = !showTraitAdmin; }}
|
|
class="rounded-lg px-2 py-1 text-xs font-medium transition-colors {showTraitAdmin ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}"
|
|
>
|
|
Traits
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="mx-auto max-w-4xl px-4 py-6">
|
|
<!-- Trait admin panel -->
|
|
{#if showTraitAdmin && accessToken}
|
|
<div class="mb-6">
|
|
<TraitAdmin
|
|
{accessToken}
|
|
collectionId={collectionId}
|
|
{traits}
|
|
metadata={parsedMetadata}
|
|
onclose={() => { showTraitAdmin = false; }}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if !connected}
|
|
<p class="text-sm text-gray-400">Venter på tilkobling…</p>
|
|
{:else if !collectionNode}
|
|
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-sm text-yellow-800">
|
|
<p class="font-medium">Samling ikke funnet</p>
|
|
<p class="mt-1">Samlingsnoden med ID {collectionId} finnes ikke eller er ikke tilgjengelig.</p>
|
|
<a href="/" class="mt-2 inline-block text-blue-600 hover:underline">Tilbake til mottak</a>
|
|
</div>
|
|
{:else if traitNames.length === 0}
|
|
<div class="rounded-lg border border-gray-200 bg-white p-6 text-center">
|
|
<p class="text-sm text-gray-500">Denne samlingen har ingen aktive traits.</p>
|
|
<p class="mt-1 text-xs text-gray-400">Traits bestemmer hvilke verktøy og visninger som er tilgjengelige.</p>
|
|
{#if accessToken && !showTraitAdmin}
|
|
<button
|
|
onclick={() => { showTraitAdmin = true; }}
|
|
class="mt-3 rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"
|
|
>
|
|
Legg til traits
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<!-- Active traits as pills -->
|
|
<div class="mb-6 flex flex-wrap gap-2">
|
|
{#each traitNames as trait (trait)}
|
|
<span class="rounded-full bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-700">
|
|
{trait}
|
|
</span>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Trait panels -->
|
|
<div class="space-y-4">
|
|
{#each renderedTraits as trait (trait)}
|
|
{#if trait === 'editor'}
|
|
<EditorTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} collectionMetadata={parsedMetadata} />
|
|
{:else if trait === 'chat'}
|
|
<ChatTrait collection={collectionNode} config={traits[trait]} userId={nodeId} />
|
|
{:else if trait === 'kanban'}
|
|
<KanbanTrait collection={collectionNode} config={traits[trait]} userId={nodeId} />
|
|
{:else if trait === 'podcast'}
|
|
<PodcastTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
|
{:else if trait === 'publishing'}
|
|
<PublishingTrait collection={collectionNode} config={traits[trait]} />
|
|
{:else if trait === 'rss'}
|
|
<RssTrait collection={collectionNode} config={traits[trait]} />
|
|
{:else if trait === 'calendar'}
|
|
<CalendarTrait collection={collectionNode} config={traits[trait]} userId={nodeId} />
|
|
{:else if trait === 'recording'}
|
|
<RecordingTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
|
{:else if trait === 'transcription'}
|
|
<TranscriptionTrait collection={collectionNode} config={traits[trait]} />
|
|
{:else if trait === 'studio'}
|
|
<StudioTrait collection={collectionNode} config={traits[trait]} userId={nodeId} />
|
|
{:else if trait === 'mixer'}
|
|
<MixerTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
|
{/if}
|
|
{/each}
|
|
|
|
{#each genericTraits as trait (trait)}
|
|
<GenericTrait name={trait} config={traits[trait]} />
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- AI-verktøy panel -->
|
|
{#if connected && accessToken}
|
|
<div class="mt-6">
|
|
<AiToolPanel {accessToken} userId={nodeId} />
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Ressursforbruk for denne noden (oppgave 15.9) -->
|
|
{#if accessToken && collectionId}
|
|
<div class="mt-6">
|
|
<NodeUsage nodeId={collectionId} {accessToken} />
|
|
</div>
|
|
{/if}
|
|
</main>
|
|
</div>
|