diff --git a/frontend/src/lib/components/AiToolPanel.svelte b/frontend/src/lib/components/AiToolPanel.svelte index 1ba8218..0f2eb14 100644 --- a/frontend/src/lib/components/AiToolPanel.svelte +++ b/frontend/src/lib/components/AiToolPanel.svelte @@ -7,12 +7,15 @@ * * Retning bestemmes av interaksjon: * - Dra en node HIT → node_to_tool (ny node med AI-output) - * - Dra verktøyet PÅ en node → tool_to_node (in-place revisjon) - * For nå støtter panelet kun node_to_tool (drop-mottak). + * - Dra et preset PÅ en node → tool_to_node (in-place revisjon) + * + * Drop-sonen bruker presetets farge for visuell feedback. + * Inkompatible noder (lyd, bilde) får rød sone med forklaring. */ import type { Node } from '$lib/spacetime'; import { nodeStore } from '$lib/spacetime'; import { aiProcess } from '$lib/api'; + import { getDragPayload, checkAiToolCompat, setDragPayload } from '$lib/transfer'; interface Props { accessToken?: string; @@ -25,6 +28,7 @@ let selectedPresetId = $state(null); let customPrompt = $state(''); let dragOver = $state(false); + let dragIncompat = $state<{ reason: string; suggestion?: string } | null>(null); let processing = $state(false); let lastResult = $state<{ success: boolean; message: string } | null>(null); let droppedNodeId = $state(null); @@ -32,7 +36,6 @@ // --- Derived: AI-preset noder fra STDB --- const presets = $derived.by(() => { const all = nodeStore.byKind('ai_preset'); - // Sorter: standard først, deretter custom, alfabetisk innenfor return all.sort((a, b) => { const metaA = parseMetadata(a); const metaB = parseMetadata(b); @@ -49,6 +52,9 @@ const selectedMeta = $derived(selectedPreset ? parseMetadata(selectedPreset) : null); + /** The color of the selected preset, used for drop-zone feedback */ + const presetColor = $derived((selectedMeta?.color as string) ?? '#9333ea'); + const modelLabel = $derived.by(() => { if (!selectedMeta) return ''; switch (selectedMeta.model_profile) { @@ -75,7 +81,6 @@ function presetIcon(meta: Record): string { const icon = meta.icon as string; - // Map heroicon names to unicode/emoji approximations const iconMap: Record = { sparkles: '✨', check_badge: '✅', @@ -89,56 +94,78 @@ return iconMap[icon] ?? '🤖'; } - // --- Drag-and-drop --- + // --- Drag-and-drop: receiving nodes --- function handleDragOver(e: DragEvent) { e.preventDefault(); - if (e.dataTransfer) { - e.dataTransfer.dropEffect = 'copy'; + if (!e.dataTransfer) return; + + // Check compatibility during drag-over for visual feedback + const payload = getDragPayload(e.dataTransfer); + if (payload) { + const node = nodeStore.get(payload.nodeId); + if (node) { + const compat = checkAiToolCompat(node.nodeKind, !!node.content?.trim()); + if (!compat.compatible) { + dragIncompat = { reason: compat.reason!, suggestion: compat.suggestion }; + e.dataTransfer.dropEffect = 'none'; + dragOver = true; + return; + } + } } + + dragIncompat = null; + e.dataTransfer.dropEffect = 'copy'; dragOver = true; } function handleDragLeave(e: DragEvent) { - // Only leave if actually leaving the drop zone const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { dragOver = false; + dragIncompat = null; } } function handleDrop(e: DragEvent) { e.preventDefault(); dragOver = false; + dragIncompat = null; - const nodeId = e.dataTransfer?.getData('text/plain'); - if (!nodeId) return; + if (!e.dataTransfer) return; + const payload = getDragPayload(e.dataTransfer); + if (!payload) return; - const node = nodeStore.get(nodeId); + const node = nodeStore.get(payload.nodeId); if (!node) return; - // Sjekk kompatibilitet: kun tekstnoder - if (node.nodeKind !== 'content' && node.nodeKind !== 'communication') { + // Check compatibility + const compat = checkAiToolCompat(node.nodeKind, !!node.content?.trim()); + if (!compat.compatible) { lastResult = { success: false, - message: `AI-verktøyet behandler kun tekst. "${node.title ?? 'Noden'}" er av type ${node.nodeKind}.` + message: [compat.reason, compat.suggestion].filter(Boolean).join(' ') }; return; } - if (!node.content?.trim()) { - lastResult = { - success: false, - message: 'Noden har ikke noe tekstinnhold å prosessere.' - }; - return; - } - - droppedNodeId = nodeId; + droppedNodeId = payload.nodeId; lastResult = null; } + // --- Drag-and-drop: dragging presets OUT (for tool_to_node) --- + function handlePresetDragStart(e: DragEvent, presetId: string) { + if (!e.dataTransfer) return; + setDragPayload(e.dataTransfer, { + nodeId: presetId, + nodeKind: 'ai_preset', + presetId, + sourcePanel: 'ai_tool' + }); + } + // --- Prosessering --- async function processNode() { if (!accessToken || !selectedPresetId || !droppedNodeId) return; @@ -188,7 +215,7 @@
- +
+

Dra et verktøy til en tekstnode for in-place revisjon

{/if}
@@ -248,19 +279,21 @@

- +
{#if droppedNode} @@ -275,10 +308,10 @@
+ {:else if dragOver && dragIncompat} + +

{dragIncompat.reason}

+ {#if dragIncompat.suggestion} +

{dragIncompat.suggestion}

+ {/if} {:else if dragOver} -

Slipp her for AI-prosessering

+

Slipp her for AI-prosessering

{:else}

Dra en tekstnode hit

Støtter innholds- og kommunikasjonsnoder

diff --git a/frontend/src/lib/components/traits/ChatTrait.svelte b/frontend/src/lib/components/traits/ChatTrait.svelte index 00283a6..1810008 100644 --- a/frontend/src/lib/components/traits/ChatTrait.svelte +++ b/frontend/src/lib/components/traits/ChatTrait.svelte @@ -1,6 +1,7 @@ @@ -40,7 +50,11 @@ {:else}