mirror of
https://github.com/FranP-code/Reflecto.git
synced 2025-10-13 00:43:31 +00:00
feat: add magic arrow tool to generate AI-powered relations between text shapes
This commit is contained in:
101
apps/web/src/lib/tldraw/relations.ts
Normal file
101
apps/web/src/lib/tldraw/relations.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { generateText } from "./processing";
|
||||
import { KnowledgeGraphManager } from "./knowledge-graph";
|
||||
import { createAITextResult } from "./ai-shapes";
|
||||
|
||||
export function extractTextContentFromShape(editor: any, shape: any): string {
|
||||
if (!shape) return "";
|
||||
if (shape.type === "ai-text-result") return shape.props?.content ?? "";
|
||||
if (shape.type === "ai-image") return shape.props?.extractedText ?? "";
|
||||
if (shape.type === "text" || shape.type === "note") return shape.props?.text ?? "";
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function generateRelationText(textA: string, textB: string): Promise<string> {
|
||||
const userPrompt = [
|
||||
"Given two short texts, write a brief plain-text relation between them.",
|
||||
"One line only. No markdown, no lists, no headings.",
|
||||
"Return only the relation phrase/sentence.",
|
||||
"",
|
||||
`Text A: "${textA}"`,
|
||||
`Text B: "${textB}"`,
|
||||
].join("\n");
|
||||
const text = await generateText(userPrompt, 0.4);
|
||||
return (text ?? "").trim();
|
||||
}
|
||||
|
||||
export function midpointBetweenShapes(editor: any, aId: string, bId: string): { x: number; y: number } {
|
||||
const aBounds = editor.getShapePageBounds(aId);
|
||||
const bBounds = editor.getShapePageBounds(bId);
|
||||
const aShape = editor.getShape(aId);
|
||||
const bShape = editor.getShape(bId);
|
||||
const acx = aBounds ? aBounds.x + aBounds.w / 2 : (aShape?.x ?? 0);
|
||||
const acy = aBounds ? aBounds.y + aBounds.h / 2 : (aShape?.y ?? 0);
|
||||
const bcx = bBounds ? bBounds.x + bBounds.w / 2 : (bShape?.x ?? 0);
|
||||
const bcy = bBounds ? bBounds.y + bBounds.h / 2 : (bShape?.y ?? 0);
|
||||
const midx = (acx + bcx) / 2;
|
||||
const midy = (acy + bcy) / 2;
|
||||
const dx = bcx - acx;
|
||||
const dy = bcy - acy;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
// Perpendicular unit vector
|
||||
const px = -dy / len;
|
||||
const py = dx / len;
|
||||
const offset = Math.min(120, Math.max(40, len * 0.12));
|
||||
return { x: midx + px * offset, y: midy + py * offset };
|
||||
}
|
||||
|
||||
export async function insertRelationBetweenShapes(
|
||||
editor: any,
|
||||
aId: string,
|
||||
bId: string,
|
||||
relationText: string
|
||||
) {
|
||||
const aBounds = editor.getShapePageBounds(aId);
|
||||
const bBounds = editor.getShapePageBounds(bId);
|
||||
const aShape = editor.getShape(aId);
|
||||
const bShape = editor.getShape(bId);
|
||||
const acx = aBounds ? aBounds.x + aBounds.w / 2 : (aShape?.x ?? 0);
|
||||
const acy = aBounds ? aBounds.y + aBounds.h / 2 : (aShape?.y ?? 0);
|
||||
const bcx = bBounds ? bBounds.x + bBounds.w / 2 : (bShape?.x ?? 0);
|
||||
const bcy = bBounds ? bBounds.y + bBounds.h / 2 : (bShape?.y ?? 0);
|
||||
const dx = bcx - acx;
|
||||
const dy = bcy - acy;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const px = -dy / len;
|
||||
const py = dx / len;
|
||||
|
||||
let baseOffset = Math.min(120, Math.max(40, len * 0.12));
|
||||
let pos = { x: (acx + bcx) / 2 + px * baseOffset, y: (acy + bcy) / 2 + py * baseOffset };
|
||||
|
||||
let relationId = "";
|
||||
editor.batch?.(() => {
|
||||
relationId = createAITextResult(editor, {
|
||||
fromShapeId: null,
|
||||
sourceType: "analysis",
|
||||
content: relationText.trim(),
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
});
|
||||
|
||||
// Nudge relation to avoid overlap with A or B (up to 2 passes)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const relBounds = editor.getShapePageBounds(relationId);
|
||||
if (!relBounds) break;
|
||||
const overlapsA = aBounds && !(relBounds.x > aBounds.x + aBounds.w || relBounds.x + relBounds.w < aBounds.x || relBounds.y > aBounds.y + aBounds.h || relBounds.y + relBounds.h < aBounds.y);
|
||||
const overlapsB = bBounds && !(relBounds.x > bBounds.x + bBounds.w || relBounds.x + relBounds.w < bBounds.x || relBounds.y > bBounds.y + bBounds.h || relBounds.y + relBounds.h < bBounds.y);
|
||||
if (overlapsA || overlapsB) {
|
||||
baseOffset = Math.min(200, Math.floor(baseOffset * 1.5));
|
||||
pos = { x: (acx + bcx) / 2 + px * baseOffset, y: (acy + bcy) / 2 + py * baseOffset };
|
||||
editor.updateShape({ id: relationId, type: "ai-text-result", x: pos.x, y: pos.y });
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const kg = new KnowledgeGraphManager(editor);
|
||||
// Both arrows point into the relation shape: a -> relation, b -> relation
|
||||
kg.createConnection(aId, relationId, "relates_to");
|
||||
kg.createConnection(bId, relationId, "relates_to");
|
||||
});
|
||||
return relationId;
|
||||
}
|
||||
Reference in New Issue
Block a user