-
{item.node.title || 'Uten tittel'}
- {#if item.node.content}
-
{item.node.content.slice(0, 140)}
- {/if}
- {#if isPublishingCollection && pubSlug}
-
- /pub/{pubSlug}/{item.node.id.slice(0, 8)}
-
+ {#if isIncompat}
+
+
+
{dropIncompat?.reason}
+ {#if dropIncompat?.suggestion}
+
{dropIncompat.suggestion}
{/if}
- {#if isPublishingCollection && accessToken}
-
- {#if pubSlug}
-
- Se
-
+ {:else if isDropTarget}
+
+
+
Slipp for in-place revisjon
+
+ {:else}
+
+
+
+ {item.node.title || 'Uten tittel'}
+ {#if isProcessing}
+ Prosesserer…
+ {/if}
+
+ {#if item.node.content}
+
{item.node.content.slice(0, 140)}
+ {/if}
+ {#if isPublishingCollection && pubSlug}
+
+ /pub/{pubSlug}/{item.node.id.slice(0, 8)}
+
+ {/if}
+ {#if itemResult}
+
+ {itemResult.message}
+
{/if}
-
- {/if}
-
+ {#if isPublishingCollection && accessToken}
+
+ {#if pubSlug}
+
+ Se
+
+ {/if}
+
+
+ {/if}
+
+ {/if}
{/each}
diff --git a/frontend/src/lib/transfer.ts b/frontend/src/lib/transfer.ts
new file mode 100644
index 0000000..14cd67b
--- /dev/null
+++ b/frontend/src/lib/transfer.ts
@@ -0,0 +1,120 @@
+/**
+ * Transfer service — drag-and-drop compatibility between tool panels.
+ *
+ * Centralizes compatibility checking and incompatibility messages
+ * for the universal transfer system (universell overføring).
+ *
+ * Ref: docs/features/universell_overfoering.md
+ * Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
+ */
+
+export type ToolType = 'ai_tool' | 'editor' | 'chat' | 'kanban' | 'calendar' | 'studio';
+
+export interface DragPayload {
+ nodeId: string;
+ nodeKind: string;
+ /** Optional: AI preset ID when dragging from AI tool panel */
+ presetId?: string;
+ sourcePanel: ToolType;
+}
+
+export interface CompatResult {
+ compatible: boolean;
+ /** Human-readable reason when incompatible */
+ reason?: string;
+ /** Suggested alternative action */
+ suggestion?: string;
+}
+
+const MIME_TYPE = 'application/x-synops-transfer';
+
+/** Encode drag payload into dataTransfer */
+export function setDragPayload(dt: DataTransfer, payload: DragPayload): void {
+ dt.setData('text/plain', payload.nodeId);
+ dt.setData(MIME_TYPE, JSON.stringify(payload));
+ dt.effectAllowed = 'copy';
+}
+
+/** Decode drag payload from dataTransfer. Falls back to plain text node ID. */
+export function getDragPayload(dt: DataTransfer): DragPayload | null {
+ const json = dt.getData(MIME_TYPE);
+ if (json) {
+ try {
+ return JSON.parse(json) as DragPayload;
+ } catch { /* fall through */ }
+ }
+ const nodeId = dt.getData('text/plain');
+ if (nodeId) {
+ return { nodeId, nodeKind: 'unknown', sourcePanel: 'editor' as ToolType };
+ }
+ return null;
+}
+
+/** Media node kinds that are incompatible with text-only tools */
+const MEDIA_KINDS = new Set(['media', 'audio', 'image', 'video']);
+
+/** Check if a node kind represents media content */
+function isMediaKind(nodeKind: string): boolean {
+ return MEDIA_KINDS.has(nodeKind);
+}
+
+/** Check if a node kind represents audio */
+function isAudioKind(nodeKind: string): boolean {
+ return nodeKind === 'audio' || nodeKind === 'media'; // media is often audio in this context
+}
+
+/**
+ * Check compatibility for dropping a node onto the AI tool panel.
+ */
+export function checkAiToolCompat(nodeKind: string, hasContent: boolean): CompatResult {
+ if (isMediaKind(nodeKind)) {
+ return {
+ compatible: false,
+ reason: 'AI-verktøyet behandler kun tekst.',
+ suggestion: nodeKind === 'audio' || nodeKind === 'media'
+ ? 'Transkriber lydfilen først, og dra transkripsjonen hit.'
+ : 'Kun tekstnoder kan prosesseres av AI-verktøyet.'
+ };
+ }
+ if (nodeKind !== 'content' && nodeKind !== 'communication') {
+ return {
+ compatible: false,
+ reason: `Nodetypen «${nodeKind}» støttes ikke av AI-verktøyet.`,
+ suggestion: 'Kun innholds- og kommunikasjonsnoder med tekst kan prosesseres.'
+ };
+ }
+ if (!hasContent) {
+ return {
+ compatible: false,
+ reason: 'Noden har ikke noe tekstinnhold å prosessere.'
+ };
+ }
+ return { compatible: true };
+}
+
+/**
+ * Check compatibility for dropping an AI preset onto a content node
+ * (tool_to_node direction — in-place revision).
+ */
+export function checkToolToNodeCompat(nodeKind: string, hasContent: boolean): CompatResult {
+ if (isMediaKind(nodeKind)) {
+ return {
+ compatible: false,
+ reason: 'AI-verktøyet kan ikke revidere mediefiler.',
+ suggestion: 'Kun tekstnoder kan revideres in-place.'
+ };
+ }
+ if (nodeKind !== 'content') {
+ return {
+ compatible: false,
+ reason: `Kun innholdsnoder kan revideres in-place.`
+ };
+ }
+ if (!hasContent) {
+ return {
+ compatible: false,
+ reason: 'Noden har ikke noe tekstinnhold å revidere.'
+ };
+ }
+ return { compatible: true };
+}
diff --git a/tasks.md b/tasks.md
index 7b8eab6..1db9607 100644
--- a/tasks.md
+++ b/tasks.md
@@ -205,8 +205,7 @@ Ref: `docs/features/ai_verktoy.md`, `docs/retninger/arbeidsflaten.md`
- [x] 18.2 AI-prosessering endepunkt: `POST /intentions/ai_process` med source_node_id, ai_preset_id, direction (node_to_tool / tool_to_node). Maskinrommet henter kilde-content og preset-prompt, mapper modellprofil → LiteLLM-alias, sender til AI Gateway. Logg forbruk i ai_usage_log.
- [x] 18.3 Direction-logikk: `tool_to_node` → lagre original som revisjon, oppdater node content. `node_to_tool` → opprett ny node med AI-output, opprett `derived_from`-edge til kilde + `processed_by`-edge til AI-preset.
- [x] 18.4 AI-verktøy panel (frontend): Svelte-komponent for arbeidsflaten. Prompt-velger med standardprompter, fritekst-felt for egendefinert prompt, modell-indikator (readonly). Drag-and-drop mottak for tekstnoder.
-- [~] 18.5 Drag-and-drop integrasjon: node → verktøy (ny node), verktøy → node (in-place revisjon). Drop-sone feedback med verktøyets farge. Inkompatibilitet for lyd/bilde-noder med forklaring.
- > Påbegynt: 2026-03-18T06:51
+- [x] 18.5 Drag-and-drop integrasjon: node → verktøy (ny node), verktøy → node (in-place revisjon). Drop-sone feedback med verktøyets farge. Inkompatibilitet for lyd/bilde-noder med forklaring.
- [ ] 18.6 Egendefinerte presets: brukere kan opprette egne AI-preset-noder med custom prompt. Dele via edges til samling/team. Modellprofil satt av admin.
## Fase 19: Arbeidsflaten — Spatial Canvas