ReMe/reme_studio/app/files-workspace/memory-graph.ts
jinliyl ef3f99f019
refactor(packaging): reorganize published packages (#495)
* refactor(packaging): reorganize published packages

* fix(packaging): install AgentScope extra in wheel smoke

* docs: align package guides and documentation site

* ci(workflow): add core dependency verification step in Python package build

- Add a workflow step to verify released core dependencies by installing the wheel with core extras
- Assert the presence of the static index.html file to ensure proper package contents
- Create and use a temporary virtual environment for isolation during verification
- Keep existing artifacts upload step intact and conditional on inputs.upload_artifacts flag

* fix(ci): update package installation dependencies in Windows workflow

- Change pip install from editable reme_studio and core to only dev and as extras
- Remove installation of reme_studio and core to streamline dependency setup
- Ensure Windows CI uses the correct extras for testing environment

* fix(tests): add missing commas in toml file reads in package version tests

- Added trailing commas in the tomllib.loads calls for auto-fin and daily_paper configs
- Ensured consistent syntax to prevent potential tuple misinterpretation
- Improved readability and correctness of the test setup code

* fix(packaging): protect qwenpaw releases and test Studio health
2026-08-27 14:02:09 +08:00

191 lines
6.1 KiB
TypeScript

import type {
GraphSnapshot,
GraphSnapshotEdge,
GraphSnapshotNode,
MemoryGraphRoot,
} from "../types";
export const GRAPH_WIDTH = 1080;
export const GRAPH_HEIGHT = 680;
export const INNER_RING_RADIUS = 164;
export const OUTER_RING_RADIUS = 270;
export interface PositionedGraphNode extends GraphSnapshotNode {
degree: number;
layer: 0 | 1 | 2;
x: number;
y: number;
radius: number;
}
export interface PositionedGraph {
nodes: PositionedGraphNode[];
byId: Map<string, PositionedGraphNode>;
}
export function nodeLabel(node: GraphSnapshotNode): string {
return (
node.name || node.path.split("/").pop()?.replace(/\.md$/i, "") || node.path
);
}
export function shortNodeLabel(node: GraphSnapshotNode): string {
const label = nodeLabel(node);
return label.length > 25 ? `${label.slice(0, 22)}` : label;
}
export function graphBelowRoot(
snapshot: GraphSnapshot,
root: MemoryGraphRoot,
): GraphSnapshot {
const rootId = `virtual:${root}`;
if (!snapshot.nodes.some((node) => node.id === rootId))
return { ...snapshot, nodes: [], edges: [] };
const outgoing = new Map<string, string[]>();
snapshot.edges.forEach((edge) =>
outgoing.set(edge.source, [
...(outgoing.get(edge.source) || []),
edge.target,
]),
);
const reachable = new Set([rootId]);
const queue = [rootId];
while (queue.length) {
const current = queue.shift() as string;
(outgoing.get(current) || []).forEach((target) => {
if (reachable.has(target)) return;
reachable.add(target);
queue.push(target);
});
}
return {
...snapshot,
nodes: snapshot.nodes.filter((node) => reachable.has(node.id)),
edges: snapshot.edges.filter(
(edge) => reachable.has(edge.source) && reachable.has(edge.target),
),
};
}
function degrees(snapshot: GraphSnapshot): Map<string, number> {
const result = new Map(snapshot.nodes.map((node) => [node.id, 0]));
snapshot.edges.forEach((edge) => {
result.set(edge.source, (result.get(edge.source) || 0) + 1);
result.set(edge.target, (result.get(edge.target) || 0) + 1);
});
return result;
}
export function layoutGraph(snapshot: GraphSnapshot): PositionedGraph {
const degree = degrees(snapshot);
const root = snapshot.nodes.find((node) => node.virtual) || snapshot.nodes[0];
const nodes: PositionedGraphNode[] = snapshot.nodes.map((node) => {
const nodeDegree = degree.get(node.id) || 0;
return {
...node,
degree: nodeDegree,
layer: node.id === root?.id ? 0 : 2,
x: GRAPH_WIDTH / 2,
y: GRAPH_HEIGHT / 2,
radius: node.virtual ? 11 : Math.min(9, 4 + Math.sqrt(nodeDegree) * 1.35),
};
});
const byId = new Map(nodes.map((node) => [node.id, node]));
if (!root) return { nodes, byId };
const outgoing = new Map<string, string[]>();
snapshot.edges.forEach((edge) => {
if (byId.has(edge.source) && byId.has(edge.target)) {
outgoing.set(edge.source, [
...(outgoing.get(edge.source) || []),
edge.target,
]);
}
});
const inner = [...new Set(outgoing.get(root.id) || [])]
.filter((id) => id !== root.id && byId.has(id))
.sort(
(left, right) =>
(degree.get(right) || 0) - (degree.get(left) || 0) ||
left.localeCompare(right),
);
const innerSet = new Set(inner);
const startAngle = -Math.PI / 2;
const innerAngles = new Map<string, number>();
inner.forEach((id, index) => {
const angle =
startAngle + (index / Math.max(1, inner.length)) * Math.PI * 2;
innerAngles.set(id, angle);
const node = byId.get(id) as PositionedGraphNode;
node.layer = 1;
node.x += Math.cos(angle) * INNER_RING_RADIUS;
node.y += Math.sin(angle) * INNER_RING_RADIUS;
});
const owner = new Map(inner.map((id) => [id, id]));
const branchQueue = [...inner];
while (branchQueue.length) {
const current = branchQueue.shift() as string;
(outgoing.get(current) || []).forEach((target) => {
if (target === root.id || owner.has(target)) return;
owner.set(target, owner.get(current) as string);
branchQueue.push(target);
});
}
const normalizeAngle = (angle?: number) =>
angle === undefined
? Number.POSITIVE_INFINITY
: (angle - startAngle + Math.PI * 2) % (Math.PI * 2);
const outer = nodes.filter(
(node) => node.id !== root.id && !innerSet.has(node.id),
);
outer.sort(
(left, right) =>
normalizeAngle(innerAngles.get(owner.get(left.id) || "")) -
normalizeAngle(innerAngles.get(owner.get(right.id) || "")) ||
right.degree - left.degree ||
left.id.localeCompare(right.id),
);
outer.forEach((node, index) => {
const angle =
startAngle + (index / Math.max(1, outer.length)) * Math.PI * 2;
node.x += Math.cos(angle) * OUTER_RING_RADIUS;
node.y += Math.sin(angle) * OUTER_RING_RADIUS;
});
return { nodes, byId };
}
export function reciprocalEdgeKeys(edges: GraphSnapshotEdge[]): Set<string> {
const keys = new Set(
edges.map((edge) => `${edge.source}\u0000${edge.target}`),
);
return new Set(
[...keys].filter((key) => {
const [source, target] = key.split("\u0000");
return keys.has(`${target}\u0000${source}`);
}),
);
}
export function edgePath(
edge: GraphSnapshotEdge,
byId: Map<string, PositionedGraphNode>,
reciprocal: Set<string>,
): string {
const source = byId.get(edge.source);
const target = byId.get(edge.target);
if (!source || !target) return "";
const dx = target.x - source.x;
const dy = target.y - source.y;
const distance = Math.max(1, Math.hypot(dx, dy));
const startX = source.x + (dx / distance) * (source.radius + 2);
const startY = source.y + (dy / distance) * (source.radius + 2);
const endX = target.x - (dx / distance) * (target.radius + 6);
const endY = target.y - (dy / distance) * (target.radius + 6);
if (!reciprocal.has(`${edge.source}\u0000${edge.target}`))
return `M ${startX} ${startY} L ${endX} ${endY}`;
const curve = edge.source.localeCompare(edge.target) < 0 ? 16 : -16;
const midX = (startX + endX) / 2 - (dy / distance) * curve;
const midY = (startY + endY) / 2 + (dx / distance) * curve;
return `M ${startX} ${startY} Q ${midX} ${midY} ${endX} ${endY}`;
}