diff --git a/frontend/src/components/dag/index.ts b/frontend/src/components/dag/index.ts index cdc671f..3642f36 100644 --- a/frontend/src/components/dag/index.ts +++ b/frontend/src/components/dag/index.ts @@ -1,12 +1,14 @@ import { QueryNode } from './nodes/QueryNode'; import { CategoryHeaderNode } from './nodes/CategoryHeaderNode'; import { AttributeNode } from './nodes/AttributeNode'; +import { GroupNode } from './nodes/GroupNode'; export const nodeTypes = { query: QueryNode, categoryHeader: CategoryHeaderNode, attribute: AttributeNode, + group: GroupNode, }; -export { QueryNode, CategoryHeaderNode, AttributeNode }; +export { QueryNode, CategoryHeaderNode, AttributeNode, GroupNode }; export { useDAGLayout } from './useDAGLayout'; diff --git a/frontend/src/components/dag/nodes/AttributeNode.tsx b/frontend/src/components/dag/nodes/AttributeNode.tsx index 6dc26ac..2a6e43e 100644 --- a/frontend/src/components/dag/nodes/AttributeNode.tsx +++ b/frontend/src/components/dag/nodes/AttributeNode.tsx @@ -18,19 +18,25 @@ export const AttributeNode = memo(({ data }: AttributeNodeProps) => { onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ - padding: '6px 12px', - borderRadius: 6, + padding: '8px 16px', + borderRadius: 8, background: fillColor, - border: `${isHovered ? 3 : 2}px solid ${strokeColor}`, + border: '1px solid transparent', + boxShadow: isHovered + ? `0 4px 12px ${strokeColor}66` + : `0 2px 4px ${strokeColor}33`, color: '#fff', fontSize: `${fontSize}px`, fontWeight: 500, textAlign: 'center', cursor: 'pointer', - transition: 'border-width 0.2s ease, filter 0.2s ease', - filter: isHovered ? 'brightness(1.12)' : 'none', + transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', + transform: isHovered ? 'translateY(-2px)' : 'translateY(0)', whiteSpace: 'nowrap', userSelect: 'none', + width: 240, + overflow: 'hidden', + textOverflow: 'ellipsis', }} > {label} diff --git a/frontend/src/components/dag/nodes/CategoryHeaderNode.tsx b/frontend/src/components/dag/nodes/CategoryHeaderNode.tsx index 8674769..36eab02 100644 --- a/frontend/src/components/dag/nodes/CategoryHeaderNode.tsx +++ b/frontend/src/components/dag/nodes/CategoryHeaderNode.tsx @@ -17,17 +17,22 @@ export const CategoryHeaderNode = memo(({ data }: CategoryHeaderNodeProps) => { style={{ display: 'flex', alignItems: 'center', - gap: 4, - padding: '4px 10px', - borderRadius: 4, - background: color, - border: isFixed ? 'none' : `2px dashed ${isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.3)'}`, - color: '#fff', - fontSize: '13px', - fontWeight: 'bold', + justifyContent: 'center', + gap: 6, + padding: '8px 16px', + borderRadius: 20, + background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)', + // border: `1px solid ${color}`, // Removed border to reduce abruptness + color: color, + fontSize: '14px', + fontWeight: 700, textAlign: 'center', whiteSpace: 'nowrap', userSelect: 'none', + boxShadow: '0 2px 8px rgba(0,0,0,0.05)', + width: 260, + overflow: 'hidden', + textOverflow: 'ellipsis', }} > {label} @@ -35,10 +40,12 @@ export const CategoryHeaderNode = memo(({ data }: CategoryHeaderNodeProps) => { AI diff --git a/frontend/src/components/dag/nodes/GroupNode.tsx b/frontend/src/components/dag/nodes/GroupNode.tsx new file mode 100644 index 0000000..0dc615e --- /dev/null +++ b/frontend/src/components/dag/nodes/GroupNode.tsx @@ -0,0 +1,57 @@ +import { memo } from 'react'; + +interface GroupNodeProps { + data: { + label: string; + color: string; + width: number; + height: number; + isDark: boolean; + }; +} + +export const GroupNode = memo(({ data }: GroupNodeProps) => { + const { label, color, width, height, isDark } = data; + + return ( +
+ {label && ( +
+ {label} +
+ )} +
+ ); +}); + +GroupNode.displayName = 'GroupNode'; diff --git a/frontend/src/components/dag/useDAGLayout.ts b/frontend/src/components/dag/useDAGLayout.ts index 7bbac4f..c681ef4 100644 --- a/frontend/src/components/dag/useDAGLayout.ts +++ b/frontend/src/components/dag/useDAGLayout.ts @@ -72,7 +72,7 @@ export function useDAGLayout( nodesByCategory[node.category].push(node); } - // Sort nodes within each category by order, then re-index locally + // Sort nodes within each category by order for (const cat of Object.keys(nodesByCategory)) { nodesByCategory[cat].sort((a, b) => a.order - b.order); } @@ -83,24 +83,66 @@ export function useDAGLayout( 1 ); const maxTotalHeight = maxNodesInColumn * (nodeHeight + nodeSpacing) - nodeSpacing; - const contentStartY = headerHeight + headerGap; + const contentStartY = headerHeight + headerGap + 20; // Added extra top padding - // Layout constants - UNIFORM spacing for all columns - const colStep = 160; // Distance between column left edges (uniform for all) + // Layout constants - INCREASED spacing + const colStep = 340; // Reduced from 400 + const nodeWidth = 240; + const headerWidth = 260; + const groupWidth = 300; // Reduced from 360 to be less "too wide" - // Helper function for column X position + // Helper function for column X position (Center of the column) const getColumnX = (colIndex: number) => colIndex * colStep; + // 0. Add Group Nodes (Backgrounds) - Rendered first + sortedCategories.forEach((cat, colIndex) => { + const categoryNodes = nodesByCategory[cat.name] || []; + const columnX = getColumnX(colIndex + 1); // +1 because column 0 is query + + // Calculate group height to include header and attributes + const nodesCount = categoryNodes.length; + const nodesHeight = Math.max( + nodesCount * (nodeHeight + nodeSpacing) - nodeSpacing, + 0 + ); + + // Start group above the header (header is at y=0) + const groupStartY = -20; + const groupPaddingBottom = 20; + + // Total height = distance from startY to contentStart + nodes height + bottom padding + const groupHeight = (contentStartY - groupStartY) + nodesHeight + groupPaddingBottom; + + nodes.push({ + id: `group-${cat.name}`, + type: 'group', + position: { + x: columnX - groupWidth / 2, // Centered + y: groupStartY + }, + data: { + label: '', // Label is handled by header node + color: categoryColors[cat.name]?.fill || '#666', + width: groupWidth, + height: Math.max(groupHeight, 100), // Ensure min height + isDark, + }, + draggable: false, + selectable: false, + zIndex: -1, + }); + }); + // 1. Add Query Node (column 0, centered vertically) const queryY = contentStartY + (maxTotalHeight - nodeHeight) / 2; nodes.push({ id: 'query-node', type: 'query', - position: { x: getColumnX(0), y: queryY }, + position: { x: getColumnX(0) - 60, y: queryY }, // Assuming query node width ~120 data: { label: data.query, isDark, - fontSize, + fontSize: fontSize + 2, }, draggable: false, selectable: false, @@ -108,11 +150,11 @@ export function useDAGLayout( // 2. Add Category Headers (starting from column 1) sortedCategories.forEach((cat, colIndex) => { - const columnX = getColumnX(colIndex + 1); // +1 because column 0 is query + const columnX = getColumnX(colIndex + 1); nodes.push({ id: `header-${cat.name}`, type: 'categoryHeader', - position: { x: columnX, y: 0 }, + position: { x: columnX - headerWidth / 2, y: 0 }, // Centered data: { label: cat.name, color: categoryColors[cat.name]?.fill || '#666', @@ -121,20 +163,21 @@ export function useDAGLayout( }, draggable: false, selectable: false, + zIndex: 10, }); }); // 3. Add Attribute Nodes sortedCategories.forEach((cat, colIndex) => { const categoryNodes = nodesByCategory[cat.name] || []; - const columnX = getColumnX(colIndex + 1); // +1 because column 0 is query + const columnX = getColumnX(colIndex + 1); categoryNodes.forEach((node, rowIndex) => { const y = contentStartY + rowIndex * (nodeHeight + nodeSpacing); nodes.push({ id: node.id, type: 'attribute', - position: { x: columnX, y }, + position: { x: columnX - nodeWidth / 2, y }, // Centered data: { label: node.name, fillColor: categoryColors[node.category]?.fill || '#666', @@ -143,6 +186,7 @@ export function useDAGLayout( }, draggable: false, selectable: false, + zIndex: 5, }); }); }); diff --git a/frontend/src/components/transformation/index.ts b/frontend/src/components/transformation/index.ts index 4cb91c5..3f45d4e 100644 --- a/frontend/src/components/transformation/index.ts +++ b/frontend/src/components/transformation/index.ts @@ -5,6 +5,7 @@ import { CategoryNode } from './nodes/CategoryNode'; import { OriginalAttributeNode } from './nodes/OriginalAttributeNode'; import { DividerNode } from './nodes/DividerNode'; import { QueryNode } from '../dag/nodes/QueryNode'; +import { GroupNode } from '../dag/nodes/GroupNode'; export const transformationNodeTypes = { query: QueryNode, @@ -14,6 +15,7 @@ export const transformationNodeTypes = { description: DescriptionNode, originalAttribute: OriginalAttributeNode, divider: DividerNode, + group: GroupNode, }; export { diff --git a/frontend/src/components/transformation/nodes/DescriptionNode.tsx b/frontend/src/components/transformation/nodes/DescriptionNode.tsx index 3d82903..0262f88 100644 --- a/frontend/src/components/transformation/nodes/DescriptionNode.tsx +++ b/frontend/src/components/transformation/nodes/DescriptionNode.tsx @@ -18,22 +18,24 @@ export const DescriptionNode = memo(({ data }: DescriptionNodeProps) => { onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ - padding: '10px 14px', - borderRadius: 8, + padding: '16px', + borderRadius: 12, background: isDark - ? 'linear-gradient(135deg, rgba(82, 196, 26, 0.2) 0%, rgba(82, 196, 26, 0.1) 100%)' - : 'linear-gradient(135deg, rgba(82, 196, 26, 0.15) 0%, rgba(82, 196, 26, 0.05) 100%)', - border: `2px solid ${isHovered ? '#52c41a' : '#52c41a80'}`, + ? 'linear-gradient(135deg, rgba(82, 196, 26, 0.15) 0%, rgba(82, 196, 26, 0.05) 100%)' + : 'linear-gradient(135deg, rgba(82, 196, 26, 0.08) 0%, rgba(255, 255, 255, 0.8) 100%)', + border: `1px solid ${isHovered ? '#52c41a66' : '#52c41a33'}`, color: isDark ? '#fff' : '#333', - fontSize: '12px', + fontSize: '13px', width: 400, minHeight: 50, cursor: 'pointer', - transition: 'all 0.2s ease', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', userSelect: 'none', boxShadow: isHovered - ? `0 4px 12px ${isDark ? 'rgba(82, 196, 26, 0.4)' : 'rgba(82, 196, 26, 0.25)'}` - : `0 2px 6px ${isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)'}`, + ? `0 8px 20px ${isDark ? 'rgba(82, 196, 26, 0.2)' : 'rgba(82, 196, 26, 0.15)'}` + : `0 4px 12px ${isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.05)'}`, + backdropFilter: 'blur(4px)', + transform: isHovered ? 'translateY(-2px)' : 'translateY(0)', }} >
{ position: 'relative', display: 'flex', flexDirection: 'column', - gap: 4, - padding: '6px 12px', - paddingRight: '36px', - marginTop: 18, - borderRadius: 6, + gap: 6, + padding: '10px 16px', + paddingRight: '40px', + marginTop: 20, + borderRadius: 12, background: isDark - ? 'linear-gradient(135deg, rgba(24, 144, 255, 0.15) 0%, rgba(255, 255, 255, 0.1) 100%)' - : 'linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(0, 0, 0, 0.03) 100%)', - border: `2px solid ${color}`, - borderWidth: isHovered ? 3 : 2, - color: isDark ? '#fff' : '#333', - fontSize: '13px', + ? 'linear-gradient(135deg, rgba(24, 144, 255, 0.15) 0%, rgba(24, 144, 255, 0.05) 100%)' + : 'linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(255, 255, 255, 0.8) 100%)', + border: `1px solid ${color}44`, + boxShadow: isHovered + ? `0 8px 20px ${color}33` + : `0 4px 12px ${color}1a`, + color: isDark ? '#fff' : '#1f1f1f', + fontSize: '14px', fontWeight: 500, - textAlign: 'center', + textAlign: 'left', cursor: 'pointer', - transition: 'all 0.2s ease', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', whiteSpace: 'nowrap', userSelect: 'none', - filter: isHovered ? 'brightness(1.1)' : 'none', - boxShadow: isDark - ? '0 2px 8px rgba(24, 144, 255, 0.2)' - : '0 2px 8px rgba(24, 144, 255, 0.15)', + transform: isHovered ? 'translateY(-2px)' : 'translateY(0)', + backdropFilter: 'blur(4px)', }} > {/* Expert Badge - positioned above the node */}
{expertName}
{/* Keyword Label */} -
{label}
+
{label}
- {/* NEW Badge - positioned below the node */} + {/* NEW Badge - positioned inside */} NEW diff --git a/frontend/src/components/transformation/nodes/KeywordNode.tsx b/frontend/src/components/transformation/nodes/KeywordNode.tsx index da1ec3d..9e32129 100644 --- a/frontend/src/components/transformation/nodes/KeywordNode.tsx +++ b/frontend/src/components/transformation/nodes/KeywordNode.tsx @@ -18,42 +18,42 @@ export const KeywordNode = memo(({ data }: KeywordNodeProps) => { onMouseLeave={() => setIsHovered(false)} style={{ position: 'relative', - padding: '6px 12px', - paddingRight: '36px', - borderRadius: 6, + padding: '10px 16px', + paddingRight: '40px', + borderRadius: 12, background: isDark - ? 'linear-gradient(135deg, rgba(250, 173, 20, 0.15) 0%, rgba(255, 255, 255, 0.1) 100%)' - : 'linear-gradient(135deg, rgba(250, 173, 20, 0.1) 0%, rgba(0, 0, 0, 0.03) 100%)', - border: `2px solid ${color}`, - borderWidth: isHovered ? 3 : 2, - color: isDark ? '#fff' : '#333', - fontSize: '13px', + ? 'linear-gradient(135deg, rgba(250, 173, 20, 0.15) 0%, rgba(250, 173, 20, 0.05) 100%)' + : 'linear-gradient(135deg, rgba(250, 173, 20, 0.08) 0%, rgba(255, 255, 255, 0.8) 100%)', + border: `1px solid ${color}44`, + boxShadow: isHovered + ? `0 8px 20px ${color}33` + : `0 4px 12px ${color}1a`, + color: isDark ? '#fff' : '#1f1f1f', + fontSize: '14px', fontWeight: 500, - textAlign: 'center', + textAlign: 'left', cursor: 'pointer', - transition: 'all 0.2s ease', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', whiteSpace: 'nowrap', userSelect: 'none', - filter: isHovered ? 'brightness(1.1)' : 'none', - boxShadow: isDark - ? '0 2px 8px rgba(250, 173, 20, 0.2)' - : '0 2px 8px rgba(250, 173, 20, 0.15)', + transform: isHovered ? 'translateY(-2px)' : 'translateY(0)', + backdropFilter: 'blur(4px)', }} > - {label} +
{label}
NEW diff --git a/frontend/src/components/transformation/nodes/OriginalAttributeNode.tsx b/frontend/src/components/transformation/nodes/OriginalAttributeNode.tsx index d0a2f3d..5d3acd5 100644 --- a/frontend/src/components/transformation/nodes/OriginalAttributeNode.tsx +++ b/frontend/src/components/transformation/nodes/OriginalAttributeNode.tsx @@ -14,21 +14,19 @@ export const OriginalAttributeNode = memo(({ data }: OriginalAttributeNodeProps) return (
{label} diff --git a/frontend/src/components/transformation/useExpertTransformationLayout.ts b/frontend/src/components/transformation/useExpertTransformationLayout.ts index 8627551..e0f3a70 100644 --- a/frontend/src/components/transformation/useExpertTransformationLayout.ts +++ b/frontend/src/components/transformation/useExpertTransformationLayout.ts @@ -45,7 +45,7 @@ export function useExpertTransformationLayout( // Layout constants const colStep = 140; - const categoryRowGap = 120; + const categoryRowGap = 160; // increased gap const minItemGap = 12; const expertKeywordGap = 24; // gap between expert keywords @@ -169,6 +169,31 @@ export function useExpertTransformationLayout( // Category Y position (centered within its group) const categoryY = currentY + layout.totalHeight / 2 - 20; + // Add Group Node (Background) + const groupPaddingX = 60; + const groupPaddingY = 40; + const groupHeight = layout.totalHeight + (groupPaddingY * 2); + const groupWidth = descriptionX + 450; // Cover everything up to description end + + nodes.push({ + id: `group-${catIndex}`, + type: 'group', + position: { + x: categoryX - groupPaddingX, + y: currentY - groupPaddingY + }, + data: { + label: result.category, // Add label to group + color: color, + width: groupWidth + groupPaddingX, + height: groupHeight, + isDark, + }, + draggable: false, + selectable: false, + zIndex: -1, + }); + // Add category node nodes.push({ id: categoryId, @@ -263,9 +288,9 @@ export function useExpertTransformationLayout( // (more lenient than exact keyword match since LLM may return slightly different text) const matchingDesc = result.descriptions.find( (d) => d.expert_id === kw.expertId && - (d.keyword === kw.keyword || - d.keyword.includes(kw.keyword) || - kw.keyword.includes(d.keyword)) + (d.keyword === kw.keyword || + d.keyword.includes(kw.keyword) || + kw.keyword.includes(d.keyword)) ); // Always show a description node (use fallback if not found) diff --git a/frontend/src/components/transformation/useTransformationLayout.ts b/frontend/src/components/transformation/useTransformationLayout.ts index 64b95f1..0120c61 100644 --- a/frontend/src/components/transformation/useTransformationLayout.ts +++ b/frontend/src/components/transformation/useTransformationLayout.ts @@ -46,7 +46,7 @@ export function useTransformationLayout( // Layout constants const colStep = 140; - const categoryRowGap = 120; // large gap between different categories + const categoryRowGap = 160; // increased gap between different categories const minItemGap = 12; // minimum gap between transformation items const origAttrRowStep = 36; // step for original attributes (same visual rhythm) @@ -127,10 +127,11 @@ export function useTransformationLayout( data: { label: data.query, isDark, - fontSize, + fontSize: fontSize + 2, }, draggable: false, selectable: false, + zIndex: 10, }); // Track current Y position @@ -145,6 +146,31 @@ export function useTransformationLayout( // Category Y position (centered within its group) const categoryY = currentY + layout.totalHeight / 2 - 20; + // Add Group Node (Background) + const groupPaddingX = 60; + const groupPaddingY = 40; + const groupHeight = layout.totalHeight + (groupPaddingY * 2); + const groupWidth = descriptionX + 450; // Cover everything up to description end + + nodes.push({ + id: `group-${catIndex}`, + type: 'group', + position: { + x: categoryX - groupPaddingX, + y: currentY - groupPaddingY + }, + data: { + label: result.category, // Add label to group + color: color, + width: groupWidth + groupPaddingX, + height: groupHeight, + isDark, + }, + draggable: false, + selectable: false, + zIndex: -1, + }); + // Add category node nodes.push({ id: categoryId, @@ -158,6 +184,7 @@ export function useTransformationLayout( }, draggable: false, selectable: false, + zIndex: 5, }); // Edge from query to category @@ -168,6 +195,9 @@ export function useTransformationLayout( type: 'smoothstep', style: { stroke: color, strokeWidth: 2, opacity: 0.6 }, animated: false, + label: 'Classified as', + labelStyle: { fill: isDark ? '#aaa' : '#666', fontSize: 10 }, + labelBgStyle: { fill: isDark ? '#1f1f1f' : '#fff', fillOpacity: 0.8 }, }); // Add original attribute nodes (distributed to match keyword spacing) @@ -186,6 +216,7 @@ export function useTransformationLayout( }, draggable: false, selectable: false, + zIndex: 5, }); // Edge from category to original attribute @@ -216,6 +247,7 @@ export function useTransformationLayout( }, draggable: false, selectable: false, + zIndex: 5, }); // Edge from category to keyword @@ -231,6 +263,9 @@ export function useTransformationLayout( strokeDasharray: '5,5', }, animated: true, + label: 'Generates', + labelStyle: { fill: '#faad14', fontSize: 10, fontWeight: 600 }, + labelBgStyle: { fill: isDark ? '#1f1f1f' : '#fff', fillOpacity: 0.8 }, }); // Find matching description @@ -250,6 +285,7 @@ export function useTransformationLayout( }, draggable: false, selectable: false, + zIndex: 5, }); // Edge from keyword to description @@ -260,6 +296,9 @@ export function useTransformationLayout( type: 'smoothstep', style: { stroke: '#52c41a', strokeWidth: 2, opacity: 0.6 }, animated: false, + label: 'Describes', + labelStyle: { fill: '#52c41a', fontSize: 10, fontWeight: 600 }, + labelBgStyle: { fill: isDark ? '#1f1f1f' : '#fff', fillOpacity: 0.8 }, }); } }); @@ -267,25 +306,7 @@ export function useTransformationLayout( // Move Y position for next category currentY += layout.totalHeight; - // Add divider line between categories (except after last one) - if (catIndex < data.results.length - 1) { - const dividerY = currentY + categoryRowGap / 2 - 1; - nodes.push({ - id: `divider-${catIndex}`, - type: 'divider', - position: { x: queryX, y: dividerY }, - data: { - isDark, - }, - draggable: false, - selectable: false, - style: { - width: descriptionX + 400, - zIndex: -1, - }, - }); - } - + // Add extra gap between categories currentY += categoryRowGap; }); diff --git a/frontend/src/styles/mindmap.css b/frontend/src/styles/mindmap.css index a39fc49..a2dc063 100644 --- a/frontend/src/styles/mindmap.css +++ b/frontend/src/styles/mindmap.css @@ -24,7 +24,21 @@ display: none; } + /* Background pattern */ .react-flow__background { opacity: 0.5; } + +/* Group node backgrounds - ensure they render behind other nodes */ +.react-flow__node-group { + z-index: -1 !important; + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; +} + +.group-node-background { + pointer-events: none; +} \ No newline at end of file