Implementer transfer service med innholdstransfer og lettvekts-triage (oppgave 20.4)
Transfer service bestemmer overføringsmodus basert på verktøy-par: - innholdstransfer (ny node + source_material edge) for transformasjoner (f.eks. chat → editor: chatboble blir kilde for ny artikkel) - lettvekts-triage (ny edge/plassering) for konteksttillegg (f.eks. chat → kanban: noden vises i begge kontekster) Shift-modifier overstyrer alltid til innholdstransfer (ny node). Endringer: - transfer.ts: resolveTransferMode() med verktøy-par-matrise, executeTransfer() som kaller API for node/edge-opprettelse - BlockShell: sender e.shiftKey til onDrop-callback - Workspace handlePanelDrop: kobler sammen modus-resolving og API-kall - Docs: oppdatert universell_overfoering.md med implementasjonsstatus Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
23aaf7e5a9
commit
b5f47daa63
6 changed files with 308 additions and 18 deletions
|
|
@ -226,7 +226,39 @@ Sync-workeren persisterer til PG `message_placements`-tabellen.
|
|||
|
||||
Ny tabell `message_placement` med reducers for place/remove/move.
|
||||
|
||||
## 9. Instruks for Claude Code
|
||||
## 9. Implementasjonsstatus
|
||||
|
||||
### 9.1 Transfer service (`$lib/transfer.ts`)
|
||||
|
||||
Sentral `transferService` med:
|
||||
- **`resolveTransferMode(source, target, shiftKey)`** — bestemmer modus fra verktøy-par.
|
||||
Bruker en matrise (`DEFAULT_TRANSFER_MODE`) som mapper source→target til default-modus.
|
||||
Shift-modifier overstyrer alltid til `innholdstransfer`.
|
||||
- **`executeTransfer(accessToken, payload, intent, shiftKey)`** — utfører overføringen:
|
||||
- Innholdstransfer: `createNode()` → `createEdge(source_material)` → `createEdge(belongs_to)`
|
||||
- Lettvekts-triage: `createEdge(belongs_to)` med placement-metadata
|
||||
- **Kompatibilitetssjekker** per verktøy-type (`checkChatCompat`, `checkKanbanCompat`, etc.)
|
||||
- **`createBlockReceiver(toolType)`** — factory for `BlockReceiver`-implementasjoner
|
||||
|
||||
### 9.2 Shift-modifier
|
||||
|
||||
- `BlockShell.handleDropEvent()` sender `e.shiftKey` til `onDrop`-callback
|
||||
- Workspace `handlePanelDrop()` sender shiftKey videre til `resolveTransferMode()`
|
||||
- Shift = alltid ny node (innholdstransfer), uansett verktøy-par
|
||||
|
||||
### 9.3 Default-modus per verktøy-par
|
||||
|
||||
| Source → Target | Default-modus | Begrunnelse |
|
||||
|----------------|---------------|-------------|
|
||||
| chat → kanban | lettvekts-triage | Noden vises på brettet |
|
||||
| chat → editor | innholdstransfer | Ny artikkelnode fra chatinnhold |
|
||||
| chat → calendar | lettvekts-triage | Noden planlegges |
|
||||
| kanban → calendar | lettvekts-triage | Oppgave planlegges |
|
||||
| * → editor | innholdstransfer | Artikkelverktøyet skaper nytt innhold |
|
||||
| * → ai_tool | innholdstransfer | AI skaper nytt innhold |
|
||||
| * → chat/kanban/calendar/studio | lettvekts-triage | Noden vises i ny kontekst |
|
||||
|
||||
## 10. Instruks for Claude Code
|
||||
- **Innholdstransfer** (ny node) brukes når innholdet transformeres (chat → artikkel)
|
||||
- **Lettvekts-triage** (ny edge/plassering) brukes når kontekst legges til (chat → kanban)
|
||||
- `source_material`-edge kobler ny node tilbake til kilden ved innholdstransfer
|
||||
|
|
@ -236,3 +268,4 @@ Ny tabell `message_placement` med reducers for place/remove/move.
|
|||
- Drag-and-drop bruker HTML5 API mellom paneler, pointer events intra-canvas
|
||||
- Hold overføringslogikken i en sentral `transferService`
|
||||
- Mottaker-interfacet er obligatorisk for alle verktøy-panel-typer
|
||||
- Shift-modifier overstyrer alltid til innholdstransfer (ny node)
|
||||
|
|
|
|||
|
|
@ -271,7 +271,8 @@
|
|||
return;
|
||||
}
|
||||
}
|
||||
onDrop?.(payload);
|
||||
// Pass shiftKey so transfer service can override mode
|
||||
onDrop?.(payload, e.shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,6 @@ export interface BlockShellEvents {
|
|||
onFullscreenChange?: (isFullscreen: boolean) => void;
|
||||
/** Panel minimize state changed (double-click header to toggle) */
|
||||
onMinimizeChange?: (isMinimized: boolean) => void;
|
||||
/** Drop received on this panel */
|
||||
onDrop?: (payload: DragPayload) => void;
|
||||
/** Drop received on this panel. shiftKey indicates Shift was held (forces innholdstransfer). */
|
||||
onDrop?: (payload: DragPayload, shiftKey: boolean) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,27 @@
|
|||
/**
|
||||
* Transfer service — drag-and-drop compatibility between tool panels.
|
||||
* Transfer service — drag-and-drop compatibility and execution between tool panels.
|
||||
*
|
||||
* Centralizes compatibility checking and incompatibility messages
|
||||
* for the universal transfer system (universell overføring).
|
||||
* Centralizes compatibility checking, transfer mode resolution, and
|
||||
* transfer execution for the universal transfer system (universell overføring).
|
||||
*
|
||||
* Ref: docs/features/universell_overfoering.md
|
||||
* Two modes:
|
||||
* - **Innholdstransfer**: Creates new node + source_material edge (content transformation)
|
||||
* - **Lettvekts-triage**: Adds edge/placement to existing node (context addition)
|
||||
*
|
||||
* Default mode is determined by the source→target tool pair.
|
||||
* Shift-modifier overrides to always create a new node (innholdstransfer).
|
||||
*
|
||||
* Ref: docs/features/universell_overfoering.md § 1, 3
|
||||
* Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
|
||||
*/
|
||||
|
||||
import { createNode, createEdge, type CreateNodeResponse } from '$lib/api.js';
|
||||
import type { PlacementIntent } from '$lib/components/blockshell/types.js';
|
||||
|
||||
export type ToolType = 'ai_tool' | 'editor' | 'chat' | 'kanban' | 'calendar' | 'studio';
|
||||
|
||||
export type TransferMode = 'innholdstransfer' | 'lettvekts-triage';
|
||||
|
||||
export interface DragPayload {
|
||||
nodeId: string;
|
||||
nodeKind: string;
|
||||
|
|
@ -256,3 +268,226 @@ export function createBlockReceiver(toolType: ToolType): { canReceive: (payload:
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Transfer mode resolution
|
||||
// Ref: docs/features/universell_overfoering.md § 1.1
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Default transfer mode matrix: source → target → mode.
|
||||
*
|
||||
* Innholdstransfer (new node): content is *transformed* for a new context.
|
||||
* e.g. chat → editor: chatboble becomes source material for an article.
|
||||
*
|
||||
* Lettvekts-triage (existing node): content *appears* in an additional context.
|
||||
* e.g. chat → kanban: same node gets a board placement.
|
||||
*
|
||||
* Rule of thumb: if the target tool creates a *new kind* of content from
|
||||
* the source, it's innholdstransfer. If it just shows the same node in
|
||||
* a different view, it's lettvekts-triage.
|
||||
*/
|
||||
const DEFAULT_TRANSFER_MODE: Partial<Record<ToolType, Partial<Record<ToolType, TransferMode>>>> = {
|
||||
chat: {
|
||||
kanban: 'lettvekts-triage', // Same node appears on board
|
||||
calendar: 'lettvekts-triage', // Same node gets scheduled
|
||||
editor: 'innholdstransfer', // New article from chat content
|
||||
studio: 'lettvekts-triage', // Open audio in studio
|
||||
ai_tool: 'innholdstransfer', // AI creates new content from source
|
||||
chat: 'lettvekts-triage', // Cross-chat share
|
||||
},
|
||||
kanban: {
|
||||
chat: 'lettvekts-triage', // Share task in chat
|
||||
calendar: 'lettvekts-triage', // Schedule existing task
|
||||
editor: 'innholdstransfer', // Task becomes article source
|
||||
studio: 'lettvekts-triage', // Open in studio
|
||||
ai_tool: 'innholdstransfer', // AI processes task
|
||||
kanban: 'lettvekts-triage', // Cross-board move
|
||||
},
|
||||
editor: {
|
||||
chat: 'lettvekts-triage', // Share article in chat
|
||||
kanban: 'lettvekts-triage', // Track article as task
|
||||
calendar: 'lettvekts-triage', // Schedule article
|
||||
studio: 'lettvekts-triage', // Open media in studio
|
||||
ai_tool: 'innholdstransfer', // AI revises article
|
||||
editor: 'innholdstransfer', // Article → new article (fork)
|
||||
},
|
||||
calendar: {
|
||||
chat: 'lettvekts-triage', // Share event in chat
|
||||
kanban: 'lettvekts-triage', // Track event as task
|
||||
editor: 'innholdstransfer', // Event becomes article source
|
||||
studio: 'lettvekts-triage', // Open in studio
|
||||
ai_tool: 'innholdstransfer', // AI processes event
|
||||
calendar: 'lettvekts-triage', // Cross-calendar
|
||||
},
|
||||
studio: {
|
||||
chat: 'lettvekts-triage', // Share audio in chat
|
||||
kanban: 'lettvekts-triage', // Track audio as task
|
||||
calendar: 'lettvekts-triage', // Schedule audio
|
||||
editor: 'innholdstransfer', // Audio → article source
|
||||
ai_tool: 'innholdstransfer', // AI processes audio
|
||||
studio: 'lettvekts-triage', // Cross-studio
|
||||
},
|
||||
ai_tool: {
|
||||
chat: 'lettvekts-triage', // Share AI result in chat
|
||||
kanban: 'lettvekts-triage', // Track result as task
|
||||
calendar: 'lettvekts-triage', // Schedule result
|
||||
editor: 'innholdstransfer', // AI result → article source
|
||||
studio: 'lettvekts-triage', // Open in studio
|
||||
ai_tool: 'innholdstransfer', // Chain AI processing
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the transfer mode for a source→target tool pair.
|
||||
*
|
||||
* - Default mode comes from the tool-pair matrix above.
|
||||
* - Shift-modifier forces innholdstransfer (always create new node).
|
||||
*
|
||||
* @param source - Source tool type (where content was dragged from)
|
||||
* @param target - Target tool type (where content was dropped)
|
||||
* @param shiftKey - Whether Shift was held during drop (forces new node)
|
||||
* @returns The resolved transfer mode
|
||||
*/
|
||||
export function resolveTransferMode(
|
||||
source: ToolType,
|
||||
target: ToolType,
|
||||
shiftKey: boolean = false,
|
||||
): TransferMode {
|
||||
// Shift override: always create a new node
|
||||
if (shiftKey) return 'innholdstransfer';
|
||||
|
||||
// Look up default from matrix
|
||||
const sourceMap = DEFAULT_TRANSFER_MODE[source];
|
||||
if (sourceMap) {
|
||||
const mode = sourceMap[target];
|
||||
if (mode) return mode;
|
||||
}
|
||||
|
||||
// Fallback: if the target creates new content (editor, ai_tool), use innholdstransfer.
|
||||
// Otherwise default to lettvekts-triage (most common case).
|
||||
if (target === 'editor' || target === 'ai_tool') return 'innholdstransfer';
|
||||
return 'lettvekts-triage';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Transfer execution
|
||||
// Ref: docs/features/universell_overfoering.md § 1, 3, 4
|
||||
// =============================================================================
|
||||
|
||||
/** Result of a completed transfer */
|
||||
export interface TransferResult {
|
||||
mode: TransferMode;
|
||||
/** The node that ended up in the target (original for triage, new for innholdstransfer) */
|
||||
nodeId: string;
|
||||
/** Edge ID for the source_material edge (only for innholdstransfer) */
|
||||
sourceEdgeId?: string;
|
||||
/** Edge ID for the placement/context edge (for lettvekts-triage) */
|
||||
placementEdgeId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a transfer: either create a new node with source_material edge
|
||||
* (innholdstransfer) or add an edge/placement to the existing node
|
||||
* (lettvekts-triage).
|
||||
*
|
||||
* @param accessToken - Auth token for API calls
|
||||
* @param payload - The drag payload (source node info)
|
||||
* @param intent - The placement intent from the receiver (target info)
|
||||
* @param shiftKey - Whether Shift was held (overrides mode to innholdstransfer)
|
||||
*/
|
||||
export async function executeTransfer(
|
||||
accessToken: string,
|
||||
payload: DragPayload,
|
||||
intent: PlacementIntent,
|
||||
shiftKey: boolean = false,
|
||||
): Promise<TransferResult> {
|
||||
const mode = resolveTransferMode(payload.sourcePanel, intent.contextType as ToolType, shiftKey);
|
||||
|
||||
if (mode === 'innholdstransfer') {
|
||||
return executeInnholdstransfer(accessToken, payload, intent);
|
||||
} else {
|
||||
return executeLettvektsTriage(accessToken, payload, intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Innholdstransfer: Create a new node and link it back to the source
|
||||
* with a source_material edge.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create new content node (inherits title from source, empty content)
|
||||
* 2. Create source_material edge: new node → source node
|
||||
* 3. If target has a context, create belongs_to edge to context
|
||||
*/
|
||||
async function executeInnholdstransfer(
|
||||
accessToken: string,
|
||||
payload: DragPayload,
|
||||
intent: PlacementIntent,
|
||||
): Promise<TransferResult> {
|
||||
// 1. Create a new content node in the target context
|
||||
const nodeResp: CreateNodeResponse = await createNode(accessToken, {
|
||||
node_kind: 'content',
|
||||
visibility: 'hidden',
|
||||
metadata: {
|
||||
transfer_source: payload.sourcePanel,
|
||||
transfer_target: intent.contextType,
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Create source_material edge: new node → original source
|
||||
const edgeResp = await createEdge(accessToken, {
|
||||
source_id: nodeResp.node_id,
|
||||
target_id: payload.nodeId,
|
||||
edge_type: 'source_material',
|
||||
metadata: {
|
||||
context: 'referenced',
|
||||
excerpt: '', // Will be populated by UI or AI later
|
||||
},
|
||||
});
|
||||
|
||||
// 3. Create belongs_to edge to target context if contextId is set
|
||||
let placementEdgeId: string | undefined;
|
||||
if (intent.contextId) {
|
||||
const belongsResp = await createEdge(accessToken, {
|
||||
source_id: nodeResp.node_id,
|
||||
target_id: intent.contextId,
|
||||
edge_type: 'belongs_to',
|
||||
metadata: intent.position ? { placement: intent.position } : {},
|
||||
});
|
||||
placementEdgeId = belongsResp.edge_id;
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'innholdstransfer',
|
||||
nodeId: nodeResp.node_id,
|
||||
sourceEdgeId: edgeResp.edge_id,
|
||||
placementEdgeId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lettvekts-triage: Add the existing node to a new context via edge.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create belongs_to edge from source node to target context
|
||||
* (with position metadata for kanban column, calendar date, etc.)
|
||||
*/
|
||||
async function executeLettvektsTriage(
|
||||
accessToken: string,
|
||||
payload: DragPayload,
|
||||
intent: PlacementIntent,
|
||||
): Promise<TransferResult> {
|
||||
const edgeResp = await createEdge(accessToken, {
|
||||
source_id: payload.nodeId,
|
||||
target_id: intent.contextId,
|
||||
edge_type: 'belongs_to',
|
||||
metadata: intent.position ? { placement: intent.position } : {},
|
||||
});
|
||||
|
||||
return {
|
||||
mode: 'lettvekts-triage',
|
||||
nodeId: payload.nodeId,
|
||||
placementEdgeId: edgeResp.edge_id,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
import MixerTrait from '$lib/components/traits/MixerTrait.svelte';
|
||||
import GenericTrait from '$lib/components/traits/GenericTrait.svelte';
|
||||
import AiToolPanel from '$lib/components/AiToolPanel.svelte';
|
||||
import { createBlockReceiver, type DragPayload } from '$lib/transfer';
|
||||
import { createBlockReceiver, executeTransfer, resolveTransferMode, type DragPayload } from '$lib/transfer';
|
||||
import type { BlockReceiver } from '$lib/components/blockshell/types';
|
||||
|
||||
const session = $derived($page.data.session as Record<string, unknown> | undefined);
|
||||
|
|
@ -344,14 +344,36 @@
|
|||
return receiverCache.get(trait);
|
||||
}
|
||||
|
||||
function handlePanelDrop(trait: string, payload: DragPayload) {
|
||||
// Drop handling will be fully implemented in task 20.4 (transfer service).
|
||||
// For now, log the intent for debugging.
|
||||
function handlePanelDrop(trait: string, payload: DragPayload, shiftKey: boolean = false) {
|
||||
const receiver = getReceiverForTrait(trait);
|
||||
if (receiver?.receive) {
|
||||
const intent = receiver.receive(payload);
|
||||
console.log(`[universell-overføring] ${payload.sourcePanel} → ${trait}:`, intent);
|
||||
if (!receiver?.receive) {
|
||||
console.warn(`[universell-overføring] Ingen mottaker for ${trait}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const intent = receiver.receive(payload);
|
||||
const mode = resolveTransferMode(
|
||||
payload.sourcePanel,
|
||||
trait as import('$lib/transfer').ToolType,
|
||||
shiftKey,
|
||||
);
|
||||
// Override intent mode with centrally resolved mode
|
||||
intent.mode = mode;
|
||||
|
||||
if (!accessToken) {
|
||||
console.error('[universell-overføring] Mangler accessToken — kan ikke utføre overføring');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[universell-overføring] ${payload.sourcePanel} → ${trait} (${mode}${shiftKey ? ', shift' : ''}):`, intent);
|
||||
|
||||
executeTransfer(accessToken, payload, intent, shiftKey)
|
||||
.then((result) => {
|
||||
console.log(`[universell-overføring] Fullført:`, result);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`[universell-overføring] Feil:`, err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -552,7 +574,7 @@
|
|||
onResize={(w, h) => handlePanelResize(trait, w, h)}
|
||||
onClose={() => handlePanelClose(trait)}
|
||||
onMinimizeChange={(m) => handlePanelMinimize(trait, m)}
|
||||
onDrop={(payload) => handlePanelDrop(trait, payload)}
|
||||
onDrop={(payload, shiftKey) => handlePanelDrop(trait, payload, shiftKey)}
|
||||
>
|
||||
{#if knownTraits.has(trait)}
|
||||
{#if trait === 'editor'}
|
||||
|
|
|
|||
3
tasks.md
3
tasks.md
|
|
@ -226,8 +226,7 @@ Ref: `docs/features/universell_overfoering.md`, `docs/retninger/arbeidsflaten.md
|
|||
- [x] 20.1 message_placements tabell: PG-migrasjon + SpacetimeDB-modul med `place_message`, `remove_placement`, `move_on_canvas` reducers. Synk STDB→PG. Ref: `docs/features/universell_overfoering.md` § 2.
|
||||
- [x] 20.2 source_material edge-type: legg til i edge-skjema + maskinrommet-validering. Støtt kontekst-metadata (quoted, summarized, referenced) og excerpt-felt. Ref: `docs/retninger/arbeidsflaten.md` § "source_material-edge".
|
||||
- [x] 20.3 BlockReceiver interface: implementer `canReceive()`, `receive()`, `renderDropZone()` i alle trait-komponenter (Chat, Kanban, Kalender, Editor, Studio). Kompatibilitetsmatrise bestemmer godkjente drops. Ref: `docs/features/universell_overfoering.md` § 4–5.
|
||||
- [~] 20.4 Transfer service: `innholdstransfer`-modus (ny node + source_material edge) og `lettvekts-triage` (eksisterende node + ny edge/placement). Bestem modus fra verktøy-par. Shift-modifier for override. Ref: `docs/features/universell_overfoering.md` § 1, 3.
|
||||
> Påbegynt: 2026-03-18T08:14
|
||||
- [x] 20.4 Transfer service: `innholdstransfer`-modus (ny node + source_material edge) og `lettvekts-triage` (eksisterende node + ny edge/placement). Bestem modus fra verktøy-par. Shift-modifier for override. Ref: `docs/features/universell_overfoering.md` § 1, 3.
|
||||
- [ ] 20.5 Panelrework — Chat: gjør ChatTrait til fullverdig BlockShell-panel med BlockReceiver, fullskjerm-toggle, og responsivt design innenfor begrenset container.
|
||||
- [ ] 20.6 Panelrework — Kanban: gjør KanbanTrait til BlockShell-panel med drag-and-drop aksept fra andre paneler, fullskjerm, responsivt.
|
||||
- [ ] 20.7 Panelrework — Kalender: gjør CalendarTrait til BlockShell-panel med drop-aksept for scheduling, fullskjerm, responsivt.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue