diff --git a/AGENTS.md b/AGENTS.md index 17e862e..19cfe6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,12 +47,14 @@ Consult these guides before working on related tasks: - Agent architecture is derived from `bots/**` and `botcomponents/**`; component ownership must be resolved per agent rather than applying every solution component to every bot. -- Agent previews use a deterministic hub-and-spoke layout: primary agents are - centered, owned agents/tools/skills share radial rings, and straight edges - attach to the nearest node side. Node color is presentation-only and does not - alter the imported topology. +- Detail pages render the imported workflow and agent topology. Gallery cards + instead show a fixed asset inventory for workflows, agents, tools, MCP + servers, and skills so dense graphs are not compressed into unreadable + thumbnails. - The browser receives compact allowlisted graph data. Raw schemas, icons, environment connection identifiers, and source blobs stay out of generated content. - Download ZIPs are deterministic rebuilds of the `solution/` contents at archive - root; gallery sidecars are excluded. + root; gallery sidecars are excluded. Downloads stay locked until the visitor + acknowledges that community solutions should be reviewed and imported only + from a trusted source. diff --git a/src/components/AssetSummaryPreview.astro b/src/components/AssetSummaryPreview.astro new file mode 100644 index 0000000..345efff --- /dev/null +++ b/src/components/AssetSummaryPreview.astro @@ -0,0 +1,94 @@ +--- +import type { GraphData } from "../lib/schema"; +import { isMcpNode, MCP_ICON_PATHS } from "../lib/graphVisuals"; + +interface Props { + agentCount: number; + workflowCount: number; + agentGraph: GraphData; + label?: string; +} + +const { + agentCount, + workflowCount, + agentGraph, + label = "Solution assets", +} = Astro.props; + +const capabilityNodes = agentGraph.nodes.filter((node) => node.type === "tool" || node.type === "connector"); +const mcpCount = capabilityNodes.filter(isMcpNode).length; +const toolCount = capabilityNodes.length - mcpCount; +const skillCount = agentGraph.nodes.filter((node) => node.type === "skill").length; +const assets = [ + { kind: "workflow", label: "Workflows", count: workflowCount, color: "var(--graph-action)" }, + { kind: "agent", label: "Agents", count: agentCount, color: "var(--graph-ai)" }, + { kind: "tool", label: "Tools", count: toolCount, color: "var(--graph-tool)" }, + { kind: "mcp", label: "MCP", count: mcpCount, color: "var(--graph-mcp)" }, + { kind: "skill", label: "Skills", count: skillCount, color: "var(--graph-skill)" }, +] as const; +const summary = assets.map((asset) => `${asset.count} ${asset.label.toLowerCase()}`).join(", "); +--- + + diff --git a/src/components/SolutionCard.astro b/src/components/SolutionCard.astro index 243be32..ed6475a 100644 --- a/src/components/SolutionCard.astro +++ b/src/components/SolutionCard.astro @@ -2,7 +2,7 @@ import type { CollectionEntry } from "astro:content"; import { authorKey, solutionMakeup } from "../lib/schema"; import { withBase } from "../lib/url"; -import TopologyPreview from "./TopologyPreview.astro"; +import AssetSummaryPreview from "./AssetSummaryPreview.astro"; interface Props { solution: CollectionEntry<"solutions">; @@ -11,7 +11,6 @@ interface Props { const { solution, compact = false } = Astro.props; const data = solution.data; -const graph = data.workflows[0]?.graph ?? data.agentGraph; const makeup = solutionMakeup(data.agentCount, data.workflowCount); const isDemo = data.tags.includes("demo"); const buildingBlocks = [ @@ -27,11 +26,11 @@ const buildingBlocks = [
-
diff --git a/src/components/TopologyPreview.astro b/src/components/TopologyPreview.astro deleted file mode 100644 index d11c156..0000000 --- a/src/components/TopologyPreview.astro +++ /dev/null @@ -1,403 +0,0 @@ ---- -import type { GraphData } from "../lib/schema"; -import { graphVisualColor, graphVisualKind, isMcpNode, MCP_ICON_PATHS } from "../lib/graphVisuals"; - -interface Props { - graph?: GraphData; - label?: string; - compact?: boolean; - variant?: "workflow" | "agent"; -} - -const { graph, label = "Solution topology", compact = false, variant = "workflow" } = Astro.props; -const nodes = graph?.nodes.filter((node) => node.type !== "canvasNote").slice(0, compact ? 9 : 18) ?? []; -const nodeIds = new Set(nodes.map((node) => node.id)); -const edges = graph?.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)) ?? []; -const nodeById = new Map(nodes.map((node) => [node.id, node])); -const xs = nodes.map((node) => node.position.x); -const ys = nodes.map((node) => node.position.y); -const minX = xs.length ? Math.min(...xs) : 0; -const maxX = xs.length ? Math.max(...xs) : 1; -const minY = ys.length ? Math.min(...ys) : 0; -const maxY = ys.length ? Math.max(...ys) : 1; -const width = 540; -const height = compact ? 116 : 210; -const pad = compact ? 18 : 28; -const gradientId = `topology-${label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${compact ? "compact" : "full"}`; -const arrowId = `${gradientId}-arrow`; -const singleAgent = variant === "agent" && nodes.length === 1 && nodes[0].type === "agent" ? nodes[0] : undefined; -const sparklePath = (x: number, y: number, size: number) => - `M ${x} ${y - size} L ${x + size * 0.28} ${y - size * 0.28} L ${x + size} ${y} L ${x + size * 0.28} ${y + size * 0.28} L ${x} ${y + size} L ${x - size * 0.28} ${y + size * 0.28} L ${x - size} ${y} L ${x - size * 0.28} ${y - size * 0.28} Z`; -const scaleX = (x: number) => - maxX === minX ? width / 2 : pad + ((x - minX) / (maxX - minX)) * (width - pad * 2); -const scaleY = (y: number) => - maxY === minY ? height / 2 : pad + ((y - minY) / (maxY - minY)) * (height - pad * 2); -const fallback = [ - { x: 35, y: height / 2 }, - { x: 145, y: height / 2 }, - { x: 270, y: height / 3 }, - { x: 270, y: (height * 2) / 3 }, - { x: 405, y: height / 2 }, - { x: 505, y: height / 2 }, -]; ---- - - - - - - - - - - - - - - { - nodes.length ? ( - <> - {!singleAgent && edges.map((edge) => { - const source = nodeById.get(edge.source); - const target = nodeById.get(edge.target); - if (!source || !target) return null; - const x1 = scaleX(source.position.x); - const y1 = scaleY(source.position.y); - const x2 = scaleX(target.position.x); - const y2 = scaleY(target.position.y); - const distance = Math.hypot(x2 - x1, y2 - y1) || 1; - const unitX = (x2 - x1) / distance; - const unitY = (y2 - y1) / distance; - const sourceInset = compact ? 11 : 15; - const targetInset = compact ? 16 : 21; - const startX = x1 + unitX * sourceInset; - const startY = y1 + unitY * sourceInset; - const endX = x2 - unitX * targetInset; - const endY = y2 - unitY * targetInset; - const color = graphVisualColor(graphVisualKind(source)); - const path = variant === "agent" - ? `M ${startX} ${startY} L ${endX} ${endY}` - : `M ${startX} ${startY} C ${startX + Math.max(24, Math.abs(endX - startX) * 0.35)} ${startY}, ${endX - Math.max(24, Math.abs(endX - startX) * 0.35)} ${endY}, ${endX} ${endY}`; - return ( - - ); - })} - {singleAgent ? (() => { - const x = width / 2; - const y = height / 2 + (compact ? 4 : 6); - const color = graphVisualColor(graphVisualKind(singleAgent)); - const headWidth = compact ? 58 : 76; - const headHeight = compact ? 40 : 52; - const headTop = y - headHeight / 2; - const visorWidth = compact ? 34 : 44; - const visorHeight = compact ? 14 : 18; - return ( - - - - - - - - - - - - ); - })() : nodes.map((node) => { - const x = scaleX(node.position.x); - const y = scaleY(node.position.y); - const kind = graphVisualKind(node); - const color = graphVisualColor(kind); - if (node.type === "agent") { - const headWidth = compact ? 22 : 28; - const headHeight = compact ? 16 : 20; - const headTop = y - headHeight / 2 + 2; - const earWidth = compact ? 3 : 4; - const visorWidth = compact ? 13 : 17; - const visorHeight = compact ? 6 : 8; - const beaconSize = compact ? 3.5 : 4.5; - return ( - - - - - - - - - - - ); - } - if (isMcpNode(node)) { - const tileSize = compact ? 30 : 38; - const iconScale = compact ? 0.105 : 0.135; - return ( - - - - {MCP_ICON_PATHS.map((path) => ( - - ))} - - - ); - } - if (node.type === "tool" || node.type === "connector") { - const moduleWidth = compact ? 11 : 14; - const moduleHeight = compact ? 15 : 19; - const gap = compact ? 4 : 5; - return ( - - - - - - - ); - } - if (node.type === "skill") { - const size = compact ? 13 : 17; - return ( - - - - - ); - } - const isTerminal = node.type === "start" || node.type === "end"; - if (isTerminal) return ( - - ); - if (kind === "decision") return ( - - ); - return ( - - ); - })} - - ) : ( - <> - {fallback.slice(0, -1).map((point, i) => { - const next = fallback[i + 1]; - return ( - - ); - })} - {fallback.map((point, i) => - i === 0 || i === fallback.length - 1 ? ( - - ) : ( - - ) - )} - - ) - } - diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 7bc5133..f2cd779 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -8,13 +8,13 @@ interface Props { } const { - title = "CAT Agents & Workflows Gallery", + title = "Copilot Studio Gallery", description = "Discover community-built Copilot Studio solutions through their agents and workflow canvases.", } = Astro.props; const fullTitle = - title === "CAT Agents & Workflows Gallery" + title === "Copilot Studio Gallery" ? title - : `${title} · CAT Agents & Workflows Gallery`; + : `${title} · Copilot Studio Gallery`; --- @@ -36,6 +36,7 @@ const fullTitle = + diff --git a/src/pages/solutions/[slug].astro b/src/pages/solutions/[slug].astro index 738028a..03518d3 100644 --- a/src/pages/solutions/[slug].astro +++ b/src/pages/solutions/[slug].astro @@ -162,9 +162,38 @@ const heroGraph = data.workflows[0]?.graph ?? data.agentGraph;

Installable package

Download this solution

Import the rebuilt ZIP through Power Platform after reviewing the agents and workflows.

- - Download solution ZIP +
+
+ +
+

Import only what you trust

+ +
+
+
+
+ Download solution ZIP +

+ This solution is provided as-is and is not covered by Microsoft Support. + Support details +


Publisher
{data.publisher ?? "Not specified"}
@@ -197,4 +226,27 @@ const heroGraph = data.workflows[0]?.graph ?? data.agentGraph; } tabs.forEach((tab) => tab.addEventListener("click", () => activate(tab.dataset.tab ?? "overview"))); activate(location.hash.slice(1) || "overview", false); + + const trustAcknowledgement = document.getElementById("trust-ack") as HTMLInputElement | null; + const download = document.querySelector("[data-download-gated]"); + + function syncDownload() { + if (!trustAcknowledgement || !download) return; + const enabled = trustAcknowledgement.checked; + download.classList.toggle("cursor-not-allowed", !enabled); + download.classList.toggle("opacity-50", !enabled); + download.classList.toggle("hover:bg-accent-hover", enabled); + download.setAttribute("aria-disabled", String(!enabled)); + download.tabIndex = enabled ? 0 : -1; + if (enabled) download.href = download.dataset.downloadUrl ?? ""; + else download.removeAttribute("href"); + } + + download?.addEventListener("click", (event) => { + if (trustAcknowledgement?.checked) return; + event.preventDefault(); + trustAcknowledgement?.focus(); + }); + trustAcknowledgement?.addEventListener("change", syncDownload); + syncDownload();