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,155 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import {
|
||||
RESOURCE_VIEW_DOMAINS,
|
||||
PROJECT_VIEW_DOMAINS,
|
||||
type Domain,
|
||||
type GraphNode,
|
||||
} from "./domain-colors";
|
||||
import { buildForceGraphData, getConnectedNodeIds, type PositionedNode, type ForceGraphData } from "./graph-data";
|
||||
|
||||
export type ViewMode = "resource" | "project";
|
||||
|
||||
export interface ComputationGraphState {
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (m: ViewMode) => void;
|
||||
resourceId: string;
|
||||
setResourceId: (id: string) => void;
|
||||
month: string;
|
||||
setMonth: (m: string) => void;
|
||||
projectId: string;
|
||||
setProjectId: (id: string) => void;
|
||||
resources: Array<{ id: string; displayName: string; eid: string }>;
|
||||
projects: Array<{ id: string; name: string; shortCode?: string | null }>;
|
||||
isLoading: boolean;
|
||||
activeDomains: Domain[];
|
||||
graphData: ForceGraphData;
|
||||
highlightedNodes: Set<string> | null;
|
||||
setHighlightedNodes: (s: Set<string> | null) => void;
|
||||
hoveredNode: PositionedNode | null;
|
||||
setHoveredNode: (n: PositionedNode | null) => void;
|
||||
domainFilter: Set<Domain>;
|
||||
toggleDomain: (domain: Domain) => void;
|
||||
handleNodeClick: (nodeId: string) => void;
|
||||
}
|
||||
|
||||
export function useComputationGraphData(): ComputationGraphState {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("resource");
|
||||
const [resourceId, setResourceId] = useState<string>("");
|
||||
const [month, setMonth] = useState(() => {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
});
|
||||
const [projectId, setProjectId] = useState<string>("");
|
||||
const [highlightedNodes, setHighlightedNodes] = useState<Set<string> | null>(null);
|
||||
const [hoveredNode, setHoveredNode] = useState<PositionedNode | null>(null);
|
||||
const [domainFilter, setDomainFilter] = useState<Set<Domain>>(new Set());
|
||||
|
||||
// Load selectors
|
||||
const { data: resourceData } = trpc.resource.list.useQuery(
|
||||
{ isActive: true, limit: 500 },
|
||||
{ staleTime: 60_000 },
|
||||
);
|
||||
const resources = resourceData?.resources ?? [];
|
||||
|
||||
const { data: projectData } = trpc.project.list.useQuery(
|
||||
{},
|
||||
{ staleTime: 60_000 },
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const projects: Array<{ id: string; name: string; shortCode?: string | null }> = (projectData as any)?.projects ?? (projectData as any) ?? [];
|
||||
|
||||
// Auto-select first resource/project
|
||||
useEffect(() => {
|
||||
if (!resourceId && resources.length > 0) {
|
||||
setResourceId(resources[0]!.id);
|
||||
}
|
||||
}, [resources, resourceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId && Array.isArray(projects) && projects.length > 0) {
|
||||
setProjectId(projects[0]!.id);
|
||||
}
|
||||
}, [projects, projectId]);
|
||||
|
||||
// Fetch graph data
|
||||
const { data: resourceGraphData, isLoading: resourceLoading } = trpc.computationGraph.getResourceData.useQuery(
|
||||
{ resourceId, month },
|
||||
{ enabled: viewMode === "resource" && !!resourceId, staleTime: 30_000 },
|
||||
);
|
||||
|
||||
const { data: projectGraphData, isLoading: projectLoading } = trpc.computationGraph.getProjectData.useQuery(
|
||||
{ projectId },
|
||||
{ enabled: viewMode === "project" && !!projectId, staleTime: 30_000 },
|
||||
);
|
||||
|
||||
const rawData = viewMode === "resource" ? resourceGraphData : projectGraphData;
|
||||
const isLoading = viewMode === "resource" ? resourceLoading : projectLoading;
|
||||
const activeDomains = viewMode === "resource" ? RESOURCE_VIEW_DOMAINS : PROJECT_VIEW_DOMAINS;
|
||||
|
||||
// Build graph data with positions
|
||||
const graphData = useMemo(() => {
|
||||
if (!rawData) return { nodes: [], links: [] };
|
||||
let filteredNodes = rawData.nodes as GraphNode[];
|
||||
if (domainFilter.size > 0) {
|
||||
filteredNodes = filteredNodes.filter((nd: GraphNode) => !domainFilter.has(nd.domain as Domain));
|
||||
}
|
||||
const filteredNodeIds = new Set(filteredNodes.map((nd: GraphNode) => nd.id));
|
||||
const filteredLinks = rawData.links.filter(
|
||||
(lk: { source: string; target: string }) =>
|
||||
filteredNodeIds.has(lk.source) && filteredNodeIds.has(lk.target),
|
||||
);
|
||||
return buildForceGraphData(filteredNodes, filteredLinks);
|
||||
}, [rawData, domainFilter]);
|
||||
|
||||
// Domain filter toggle
|
||||
const toggleDomain = useCallback((domain: Domain) => {
|
||||
setDomainFilter((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(domain)) {
|
||||
next.delete(domain);
|
||||
} else {
|
||||
next.add(domain);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Node click → highlight path
|
||||
const handleNodeClick = useCallback(
|
||||
(nodeId: string) => {
|
||||
if (highlightedNodes?.has(nodeId)) {
|
||||
setHighlightedNodes(null);
|
||||
} else {
|
||||
const connected = getConnectedNodeIds(nodeId, graphData.links);
|
||||
setHighlightedNodes(connected);
|
||||
}
|
||||
},
|
||||
[graphData.links, highlightedNodes],
|
||||
);
|
||||
|
||||
return {
|
||||
viewMode,
|
||||
setViewMode,
|
||||
resourceId,
|
||||
setResourceId,
|
||||
month,
|
||||
setMonth,
|
||||
projectId,
|
||||
setProjectId,
|
||||
resources,
|
||||
projects,
|
||||
isLoading,
|
||||
activeDomains,
|
||||
graphData,
|
||||
highlightedNodes,
|
||||
setHighlightedNodes,
|
||||
hoveredNode,
|
||||
setHoveredNode,
|
||||
domainFilter,
|
||||
toggleDomain,
|
||||
handleNodeClick,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user