diff --git a/packages/memory-graph/CHANGELOG.md b/packages/memory-graph/CHANGELOG.md index 1e5ada91..c6bee6cf 100644 --- a/packages/memory-graph/CHANGELOG.md +++ b/packages/memory-graph/CHANGELOG.md @@ -2,6 +2,28 @@ ## Visual & Layout Improvements (2025-12-21) +### Document Type Icons on Cards +**Feature:** Document cards now display type-specific icons centered on the card for better visual identification. + +**Supported Document Types:** +- 📄 TXT - Text documents (default for unsupported types) +- 📑 PDF - PDF files +- 📝 MD - Markdown files +- 📘 DOC/DOCX - Word documents +- 📄 RTF - Rich Text Format files +- 📊 CSV - Comma-separated values (grid icon) +- {} JSON - JSON files (curly braces) + +**Implementation:** +- Icon rendering uses Canvas 2D API for performance +- Icons scale with card size (40% of card height) +- Only rendered when zoomed in (skipped in simplified rendering mode) +- Centered on document cards for better visual balance + +**Files Changed:** +- `src/utils/document-icons.ts` - New utility with canvas-based icon drawing functions +- `src/components/graph-canvas.tsx:18,471-487` - Icon rendering integration + ### Updated Color Scheme for Better Visual Clarity **Changes:** - Refined color palette for improved contrast and readability diff --git a/packages/memory-graph/src/components/graph-canvas.tsx b/packages/memory-graph/src/components/graph-canvas.tsx index 441c9b27..243c82cc 100644 --- a/packages/memory-graph/src/components/graph-canvas.tsx +++ b/packages/memory-graph/src/components/graph-canvas.tsx @@ -15,6 +15,7 @@ import type { GraphNode, MemoryEntry, } from "@/types" +import { drawDocumentIcon } from "@/utils/document-icons" import { canvasWrapper } from "./canvas-common.css" export const GraphCanvas = memo( @@ -64,12 +65,30 @@ export const GraphCanvas = memo( const screenY = node.y * zoom + panY const nodeSize = node.size * zoom - const dx = x - screenX - const dy = y - screenY - const distance = Math.sqrt(dx * dx + dy * dy) + if (node.type === "document") { + // Rectangular hit detection for documents (matches visual size) + const docWidth = nodeSize * 1.4 + const docHeight = nodeSize * 0.9 + const halfW = docWidth / 2 + const halfH = docHeight / 2 - if (distance <= nodeSize / 2) { - return node.id + if ( + x >= screenX - halfW && + x <= screenX + halfW && + y >= screenY - halfH && + y <= screenY + halfH + ) { + return node.id + } + } else { + // Circular hit detection for memory nodes + const dx = x - screenX + const dy = y - screenY + const distance = Math.sqrt(dx * dx + dy * dy) + + if (distance <= nodeSize / 2) { + return node.id + } } } return null @@ -433,7 +452,9 @@ export const GraphCanvas = memo( ctx.strokeStyle = colors.accent.primary ctx.lineWidth = 3 ctx.setLineDash([6, 4]) - const ringPadding = 10 + // Add equal padding on all sides (15% of average dimension) + const avgDimension = (docWidth + docHeight) / 2 + const ringPadding = avgDimension * 0.1 ctx.beginPath() ctx.roundRect( screenX - docWidth / 2 - ringPadding, @@ -446,6 +467,21 @@ export const GraphCanvas = memo( ctx.setLineDash([]) ctx.restore() } + + // Draw document type icon (centered) + if (!useSimplifiedRendering) { + const doc = node.data as DocumentWithMemories + const iconSize = docHeight * 0.4 // Icon size relative to card height + + drawDocumentIcon( + ctx, + screenX, + screenY, + iconSize, + doc.type || "text", + "rgba(255, 255, 255, 0.8)", + ) + } } else { // Enhanced memory styling with status indicators const mem = node.data as MemoryEntry @@ -581,18 +617,23 @@ export const GraphCanvas = memo( ctx.globalAlpha = 0.6 ctx.beginPath() - const glowSize = nodeSize * 0.7 if (node.type === "document") { + // Use actual document dimensions for glow + const docWidth = nodeSize * 1.4 + const docHeight = nodeSize * 0.9 + // Make glow 10% larger than document + const avgDimension = (docWidth + docHeight) / 2 + const glowPadding = avgDimension * 0.1 ctx.roundRect( - screenX - glowSize, - screenY - glowSize / 1.4, - glowSize * 2, - glowSize * 1.4, + screenX - docWidth / 2 - glowPadding, + screenY - docHeight / 2 - glowPadding, + docWidth + glowPadding * 2, + docHeight + glowPadding * 2, 15, ) } else { // Hexagonal glow for memory nodes - const glowRadius = glowSize + const glowRadius = nodeSize * 0.7 const sides = 6 for (let i = 0; i < sides; i++) { const angle = (i * 2 * Math.PI) / sides - Math.PI / 2 diff --git a/packages/memory-graph/src/utils/document-icons.ts b/packages/memory-graph/src/utils/document-icons.ts new file mode 100644 index 00000000..2e93c22a --- /dev/null +++ b/packages/memory-graph/src/utils/document-icons.ts @@ -0,0 +1,237 @@ +/** + * Canvas-based document type icon rendering utilities + * Simplified to match supported file types: PDF, TXT, MD, DOCX, DOC, RTF, CSV, JSON + */ + +export type DocumentIconType = + | "text" + | "pdf" + | "md" + | "markdown" + | "docx" + | "doc" + | "rtf" + | "csv" + | "json" + +/** + * Draws a document type icon on canvas + * @param ctx - Canvas 2D rendering context + * @param x - X position (center of icon) + * @param y - Y position (center of icon) + * @param size - Icon size (width/height) + * @param type - Document type + * @param color - Icon color (default: white) + */ +export function drawDocumentIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, + type: string, + color = "rgba(255, 255, 255, 0.9)", +): void { + ctx.save() + ctx.fillStyle = color + ctx.strokeStyle = color + ctx.lineWidth = Math.max(1, size / 12) + ctx.lineCap = "round" + ctx.lineJoin = "round" + + switch (type) { + case "pdf": + drawPdfIcon(ctx, x, y, size) + break + case "md": + case "markdown": + drawMarkdownIcon(ctx, x, y, size) + break + case "doc": + case "docx": + drawWordIcon(ctx, x, y, size) + break + case "rtf": + drawRtfIcon(ctx, x, y, size) + break + case "csv": + drawCsvIcon(ctx, x, y, size) + break + case "json": + drawJsonIcon(ctx, x, y, size) + break + case "txt": + case "text": + default: + drawTextIcon(ctx, x, y, size) + break + } + + ctx.restore() +} + +// Individual icon drawing functions + +function drawTextIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +): void { + // Simple document outline with lines + const w = size * 0.7 + const h = size * 0.85 + const cornerFold = size * 0.2 + + ctx.beginPath() + ctx.moveTo(x - w / 2, y - h / 2) + ctx.lineTo(x + w / 2 - cornerFold, y - h / 2) + ctx.lineTo(x + w / 2, y - h / 2 + cornerFold) + ctx.lineTo(x + w / 2, y + h / 2) + ctx.lineTo(x - w / 2, y + h / 2) + ctx.closePath() + ctx.stroke() + + // Text lines + const lineSpacing = size * 0.15 + const lineWidth = size * 0.4 + ctx.beginPath() + ctx.moveTo(x - lineWidth / 2, y - lineSpacing) + ctx.lineTo(x + lineWidth / 2, y - lineSpacing) + ctx.moveTo(x - lineWidth / 2, y) + ctx.lineTo(x + lineWidth / 2, y) + ctx.moveTo(x - lineWidth / 2, y + lineSpacing) + ctx.lineTo(x + lineWidth / 2, y + lineSpacing) + ctx.stroke() +} + +function drawPdfIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +): void { + // Document with "PDF" text + const w = size * 0.7 + const h = size * 0.85 + + ctx.beginPath() + ctx.rect(x - w / 2, y - h / 2, w, h) + ctx.stroke() + + // "PDF" letters (simplified) + ctx.font = `bold ${size * 0.35}px sans-serif` + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.fillText("PDF", x, y) +} + +function drawMarkdownIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +): void { + // Document with "MD" text + const w = size * 0.7 + const h = size * 0.85 + + ctx.beginPath() + ctx.rect(x - w / 2, y - h / 2, w, h) + ctx.stroke() + + // "MD" letters + ctx.font = `bold ${size * 0.3}px sans-serif` + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.fillText("MD", x, y) +} + +function drawWordIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +): void { + // Document with "DOC" text + const w = size * 0.7 + const h = size * 0.85 + + ctx.beginPath() + ctx.rect(x - w / 2, y - h / 2, w, h) + ctx.stroke() + + // "DOC" letters + ctx.font = `bold ${size * 0.28}px sans-serif` + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.fillText("DOC", x, y) +} + +function drawRtfIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +): void { + // Document with "RTF" text + const w = size * 0.7 + const h = size * 0.85 + + ctx.beginPath() + ctx.rect(x - w / 2, y - h / 2, w, h) + ctx.stroke() + + // "RTF" letters + ctx.font = `bold ${size * 0.3}px sans-serif` + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.fillText("RTF", x, y) +} + +function drawCsvIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +): void { + // Grid table for CSV + const w = size * 0.7 + const h = size * 0.85 + + ctx.strokeRect(x - w / 2, y - h / 2, w, h) + + // Grid lines (2x2) + ctx.beginPath() + // Vertical line + ctx.moveTo(x, y - h / 2) + ctx.lineTo(x, y + h / 2) + // Horizontal line + ctx.moveTo(x - w / 2, y) + ctx.lineTo(x + w / 2, y) + ctx.stroke() +} + +function drawJsonIcon( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +): void { + // Curly braces for JSON + const w = size * 0.6 + const h = size * 0.8 + + // Left brace + ctx.beginPath() + ctx.moveTo(x - w / 4, y - h / 2) + ctx.quadraticCurveTo(x - w / 2, y - h / 3, x - w / 2, y) + ctx.quadraticCurveTo(x - w / 2, y + h / 3, x - w / 4, y + h / 2) + ctx.stroke() + + // Right brace + ctx.beginPath() + ctx.moveTo(x + w / 4, y - h / 2) + ctx.quadraticCurveTo(x + w / 2, y - h / 3, x + w / 2, y) + ctx.quadraticCurveTo(x + w / 2, y + h / 3, x + w / 4, y + h / 2) + ctx.stroke() +}