feat: project cover art with AI generation, branding rename, RBAC fix, computation graph
- Add DALL-E cover art generation for projects (Azure OpenAI + standard OpenAI)
- CoverArtSection component with generate/upload/remove/focus-point controls
- Client-side image compression (10MB input → WebP/JPEG, max 1920px)
- DALL-E settings in admin panel (deployment, endpoint, API key)
- MCP assistant tools for cover art (generate_project_cover, remove_project_cover)
- Rename "Planarchy" → "plANARCHY" across all UI-facing text (13 files)
- Fix hardcoded canEdit={true} on project detail page — now checks user role
- Computation graph visualization (2D/3D) for calculation rules
- OG image and OpenGraph metadata
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { DOMAIN_LABELS } from "~/components/analytics/computation-graph/domain-colors";
|
||||
import { createNodeSprite, createDimmedNodeSprite, getLinkColor } from "~/components/analytics/computation-graph/node-renderer";
|
||||
import type { PositionedNode } from "~/components/analytics/computation-graph/graph-data";
|
||||
import type { ComputationGraphState } from "~/components/analytics/computation-graph/useComputationGraphData";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ForceGraph3D = dynamic(() => import("react-force-graph-3d"), { ssr: false }) as any;
|
||||
|
||||
interface Props {
|
||||
state: ComputationGraphState;
|
||||
}
|
||||
|
||||
export default function ComputationGraph3DView({ state }: Props) {
|
||||
const { graphData, highlightedNodes, handleNodeClick, setHoveredNode } = state;
|
||||
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onNodeClick = useCallback((node: any) => {
|
||||
handleNodeClick((node as PositionedNode).id);
|
||||
}, [handleNodeClick]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onNodeHover = useCallback((node: any) => {
|
||||
setHoveredNode(node as PositionedNode | null);
|
||||
}, [setHoveredNode]);
|
||||
|
||||
const nodeThreeObject = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(node: any) => {
|
||||
const n = node as PositionedNode;
|
||||
if (highlightedNodes && !highlightedNodes.has(n.id)) {
|
||||
return createDimmedNodeSprite(n);
|
||||
}
|
||||
return createNodeSprite(n);
|
||||
},
|
||||
[highlightedNodes],
|
||||
);
|
||||
|
||||
const linkColorFn = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(link: any) => {
|
||||
if (!highlightedNodes) return getLinkColor(link, 0.4);
|
||||
const sourceId = typeof link.source === "object" ? link.source.id : link.source;
|
||||
const targetId = typeof link.target === "object" ? link.target.id : link.target;
|
||||
if (highlightedNodes.has(sourceId) && highlightedNodes.has(targetId)) {
|
||||
return getLinkColor(link, 0.9);
|
||||
}
|
||||
return getLinkColor(link, 0.08);
|
||||
},
|
||||
[highlightedNodes],
|
||||
);
|
||||
|
||||
const linkWidthFn = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(link: any) => {
|
||||
if (!highlightedNodes) return link.weight ?? 1;
|
||||
const sourceId = typeof link.source === "object" ? link.source.id : link.source;
|
||||
const targetId = typeof link.target === "object" ? link.target.id : link.target;
|
||||
if (highlightedNodes.has(sourceId) && highlightedNodes.has(targetId)) {
|
||||
return (link.weight ?? 1) * 2.5;
|
||||
}
|
||||
return 0.3;
|
||||
},
|
||||
[highlightedNodes],
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
setMousePos({ x: e.clientX, y: e.clientY });
|
||||
}, []);
|
||||
|
||||
if (graphData.nodes.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-zinc-500">
|
||||
{state.viewMode === "resource" ? "Select a resource and month" : "Select a project"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full" onMouseMove={handleMouseMove}>
|
||||
<ForceGraph3D
|
||||
graphData={graphData}
|
||||
nodeThreeObject={nodeThreeObject}
|
||||
nodeThreeObjectExtend={false}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeHover={onNodeHover}
|
||||
linkColor={linkColorFn}
|
||||
linkWidth={linkWidthFn}
|
||||
linkDirectionalArrowLength={4}
|
||||
linkDirectionalArrowRelPos={0.8}
|
||||
linkCurvature={0.1}
|
||||
backgroundColor="#0f172a"
|
||||
showNavInfo={false}
|
||||
warmupTicks={50}
|
||||
cooldownTicks={0}
|
||||
/>
|
||||
{/* Hover Tooltip */}
|
||||
{state.hoveredNode && (
|
||||
<div
|
||||
className="pointer-events-none fixed z-50 max-w-sm rounded-lg border border-zinc-600 bg-zinc-900 px-4 py-3 text-sm text-zinc-200 shadow-xl"
|
||||
style={{ top: mousePos.y + 16, left: mousePos.x + 16 }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block h-2.5 w-2.5 rounded-full"
|
||||
style={{ backgroundColor: state.hoveredNode.color }}
|
||||
/>
|
||||
<span className="font-semibold">{state.hoveredNode.label}</span>
|
||||
<span className="ml-auto text-xs text-zinc-500">{DOMAIN_LABELS[state.hoveredNode.domain]}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-lg font-bold">
|
||||
{state.hoveredNode.value} <span className="text-sm font-normal text-zinc-400">{state.hoveredNode.unit}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-400">{state.hoveredNode.description}</div>
|
||||
{state.hoveredNode.formula && (
|
||||
<div className="mt-1 font-mono text-xs text-blue-400">{state.hoveredNode.formula}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user