Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The site uses Astro, Tailwind CSS, React Flow, and static GitHub Pages deploymen
connected-agent delegation.
- Interactive workflow previews rendered from each export's saved designer graph.
- Deterministic downloadable ZIP rebuilt from the submitted exploded solution.
- Contributor directory and JSON catalog endpoint.
- Contributor directory, individual contributor profiles, and JSON catalog endpoint.
- Pull-request validation for metadata, solution structure, graph availability,
unsafe filesystem entries, and generated output.

Expand Down
75 changes: 75 additions & 0 deletions public/copilot-studio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions scripts/import-submissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { cp, lstat, mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import test from "node:test";
import AdmZip from "adm-zip";
import { isMcpNode } from "../src/lib/graphVisuals";
import {
SubmissionImportError,
deriveSolutionFields,
Expand Down Expand Up @@ -116,6 +117,23 @@ test("maps XML components and normalizes tools, skills, and delegation", () => {
assert.deepEqual(normalized.edges.map((edge) => edge.label), ["uses", "includes"]);
});

test("distinguishes MCP servers from ordinary tools and connectors", () => {
assert.equal(isMcpNode({
id: "tool:mcp",
name: "Warehouse MCP Server",
type: "tool",
position: { x: 0, y: 0 },
details: { operation: "InvokeMCP" },
}), true);
assert.equal(isMcpNode({
id: "connector:email",
name: "Send email",
type: "connector",
position: { x: 0, y: 0 },
details: { operation: "send" },
}), false);
});

test("centers primary agents and arranges capabilities around them", () => {
const graph = layoutAgentGraph([
{ id: "agent:primary", name: "Primary", type: "agent", position: { x: 0, y: 0 }, details: {} },
Expand Down
38 changes: 20 additions & 18 deletions scripts/import-submissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,20 @@ export function layoutAgentGraph(nodes: GraphNode[], edges: GraphEdge[]): GraphD
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
});
const pointInLane = (
center: { x: number; y: number },
outwardAngle: number,
distance: number,
gap: number,
index: number,
count: number,
) => {
const lateralOffset = (index - (count - 1) / 2) * gap;
return {
x: center.x + Math.cos(outwardAngle) * distance - Math.sin(outwardAngle) * lateralOffset,
y: center.y + Math.sin(outwardAngle) * distance + Math.cos(outwardAngle) * lateralOffset,
};
};

const placeAgent = (
agentId: string,
Expand All @@ -420,39 +434,27 @@ export function layoutAgentGraph(nodes: GraphNode[], edges: GraphEdge[]): GraphD
.filter((id) => nodeById.has(id) && !positions.has(id));
const components = children.filter((id) => !agentIds.has(id));
const connectedAgents = children.filter((id) => agentIds.has(id));
const ringAngle = (id: string): number => {
const index = children.indexOf(id);
if (children.length === 1) return -Math.PI / 2;
if (children.length === 2) return Math.PI * index;
return -Math.PI / 2 + (Math.PI * 2 * index) / children.length;
};

components.forEach((id, index) => {
const angle = depth === 0
? ringAngle(id)
: outwardAngle - Math.PI / 3 + (Math.PI * 2 / 3) * ((index + 1) / (components.length + 1));
const point = pointOnCircle(center, depth === 0 ? 220 : 170, angle);
const angle = depth === 0 ? Math.PI / 2 : outwardAngle;
const point = pointInLane(center, angle, depth === 0 ? 360 : 320, 340, index, components.length);
positions.set(id, centerPosition(point.x, point.y));
});

connectedAgents.forEach((id, index) => {
const angle = depth === 0
? ringAngle(id)
: connectedAgents.length === 1
? outwardAngle
: outwardAngle - Math.PI / 3 + (Math.PI * 2 / 3) * ((index + 1) / (connectedAgents.length + 1));
const point = pointOnCircle(center, depth === 0 ? 360 : 300, angle);
const angle = depth === 0 ? -Math.PI / 2 : outwardAngle;
const point = pointInLane(center, angle, depth === 0 ? 500 : 520, 560, index, connectedAgents.length);
placeAgent(id, point, depth + 1, angle);
});
};

roots.forEach((root, index) => {
const centerX = (index - (roots.length - 1) / 2) * 1_350;
const centerX = (index - (roots.length - 1) / 2) * 1_800;
placeAgent(root.id, { x: centerX, y: 0 }, 0);
});

agents.filter((agent) => !positions.has(agent.id)).forEach((agent, index) => {
placeAgent(agent.id, { x: index * 1_350, y: 1_100 }, 0);
placeAgent(agent.id, { x: index * 1_800, y: 1_200 }, 0);
});

const unplaced = ordered.filter((node) => !positions.has(node.id));
Expand Down
25 changes: 18 additions & 7 deletions src/components/SolutionCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ const buildingBlocks = [
];
---

<a
href={withBase(`/solutions/${solution.id}`)}
class="product-card group flex h-full flex-col overflow-hidden rounded-[1.125rem] border border-border bg-surface"
>
<article class="product-card group relative flex h-full flex-col overflow-hidden rounded-[1.125rem] border border-border bg-surface">
<a href={withBase(`/solutions/${solution.id}`)} class="absolute inset-0 z-10" aria-label={`View ${data.name}`}>
<span class="sr-only">View {data.name}</span>
</a>
<div class:list={["studio-canvas relative m-3 mb-0 overflow-hidden rounded-xl border border-border/70 px-3 py-2 text-muted", compact ? "h-28" : "h-36"]}>
<div class="studio-grid absolute inset-0 opacity-70"></div>
<div class="relative h-full transition-transform duration-300 group-hover:scale-[1.02]">
<TopologyPreview graph={graph} label={`${data.name} topology`} compact />
<TopologyPreview
graph={graph}
label={`${data.name} topology`}
variant={data.workflowCount ? "workflow" : "agent"}
compact
/>
</div>
</div>
<div class="flex flex-1 flex-col p-4 pt-3">
Expand All @@ -53,11 +58,17 @@ const buildingBlocks = [
{isDemo && <span class="rounded-md border border-dashed border-border px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-soft">Demo</span>}
</div>
<div class="mt-4 flex items-center justify-between gap-3 border-t border-border pt-3 text-xs text-soft">
<span class="truncate" data-author-key={authorKey(data.authorGithub, data.author)}>by {data.author}</span>
<a
href={withBase(`/contributors/${authorKey(data.authorGithub, data.author)}`)}
class="relative z-20 truncate hover:text-accent"
data-author-key={authorKey(data.authorGithub, data.author)}
>
by {data.author}
</a>
<span class="flex shrink-0 items-center gap-3">
<span title={`${data.agentCount} agents`}>{data.agentCount} agent{data.agentCount === 1 ? "" : "s"}</span>
<span title={`${data.workflowCount} workflows`}>{data.workflowCount} workflow{data.workflowCount === 1 ? "" : "s"}</span>
</span>
</div>
</div>
</a>
</article>
Loading
Loading