4 * and their relationships (citations, references), visualizing them as a network graph.
5 * Runs entirely within a single Val Town endpoint.
6 * Uses React Flow for visualization and OpenAI for analysis.
7 *
8 * Structure adapted from the Multi-Agent AI Support Simulation example.
13// unless complex client-side processing based on types is needed.
14
15// Represents a policy, law, section, etc., as a node in the graph (React Flow format)
16type PolicyNode = {
17 id: string;
18 data: { label: string; type?: string; details?: string };
19 position: { x: number; y: number }; // Position is important for layout
20 // Add other React Flow node properties if needed (style, type, etc.)
21};
22
23// Represents a connection (citation, reference) between two nodes (React Flow format)
24type PolicyEdge = {
25 id: string;
27 target: string; // Target node ID
28 label?: string; // Relationship type (e.g., "cites", "amends")
29 // Add other React Flow edge properties if needed (animated, style, type, markerEnd, etc.)
30};
31
53
54 // --- Embedded CSS ---
55 // Combining manual translations of previous Tailwind classes and React Flow base styles
56 const embeddedCss = `
57 /* Basic Reset & Body */
277
278
279 /* --- React Flow Base CSS - Embedded --- */
280 /* From: https://github.com/xyflow/xyflow/blob/main/packages/react/dist/style.css */
281 /* (Content copied and pasted here for embedding) */
282 .react-flow {
283 position: relative;
284 width: 100%;
286 overflow: hidden;
287 }
288 .react-flow__viewport {
289 transform-origin: 0 0;
290 width: 100%;
292 z-index: 1;
293 }
294 .react-flow__pane {
295 cursor: grab;
296 width: 100%;
301 min-width: 100%;
302 }
303 .react-flow__pane.draggable {
304 cursor: grab;
305 }
306 .react-flow__pane.selection {
307 cursor: pointer;
308 }
309 .react-flow__pane.dragging {
310 cursor: grabbing;
311 }
312 .react-flow__edge-path,
313 .react-flow__connection-path {
314 stroke: #b1b1b7;
315 stroke-width: 1;
316 fill: none;
317 }
318 .react-flow__edge.selected .react-flow__edge-path,
319 .react-flow__edge:focus .react-flow__edge-path,
320 .react-flow__edge:focus-visible .react-flow__edge-path {
321 stroke: #555;
322 }
323 .react-flow__edge.animated path {
324 stroke-dasharray: 5;
325 animation: dashdraw 0.5s linear infinite;
330 }
331 }
332 .react-flow__edge-textwrapper {
333 display: flex;
334 align-items: center;
336 height: 100%;
337 }
338 .react-flow__edge-textbg {
339 fill: #fff;
340 }
341 .react-flow__edge .react-flow__edge-text {
342 pointer-events: none;
343 -webkit-user-select: none;
346 font-size: 10px;
347 }
348 .react-flow__connection {
349 pointer-events: none;
350 }
351 .react-flow__connection .react-flow__connection-path {
352 stroke: #555;
353 stroke-width: 2;
354 }
355 .react-flow__nodes {
356 pointer-events: none;
357 }
358 .react-flow__node {
359 cursor: grab;
360 pointer-events: all;
370 transform-origin: 0 0;
371 }
372 .react-flow__node.selected,
373 .react-flow__node:focus,
374 .react-flow__node:focus-visible {
375 outline: none;
376 }
377 .react-flow__node-default,
378 .react-flow__node-input,
379 .react-flow__node-output,
380 .react-flow__node-group {
381 padding: 10px;
382 border-radius: 3px;
386 border: 1px solid #1a192b;
387 }
388 .react-flow__node-default.selectable:hover,
389 .react-flow__node-input.selectable:hover,
390 .react-flow__node-output.selectable:hover,
391 .react-flow__node-group.selectable:hover {
392 box-shadow: 0 0 0 0.5px #1a192b;
393 }
394 .react-flow__node-default.selected,
395 .react-flow__node-input.selected,
396 .react-flow__node-output.selected,
397 .react-flow__node-group.selected,
398 .react-flow__node-default:focus,
399 .react-flow__node-input:focus,
400 .react-flow__node-output:focus,
401 .react-flow__node-group:focus,
402 .react-flow__node-default:focus-visible,
403 .react-flow__node-input:focus-visible,
404 .react-flow__node-output:focus-visible,
405 .react-flow__node-group:focus-visible {
406 box-shadow: 0 0 0 0.5px #1a192b, 0 0 0 2px #fff, 0 0 0 3.5px #1a192b;
407 }
408 .react-flow__node-input .react-flow__handle {
409 border-radius: 0 2px 2px 0;
410 border: none;
411 border-left: 1px solid #1a192b;
412 }
413 .react-flow__node-output .react-flow__handle {
414 border-radius: 2px 0 0 2px;
415 border: none;
416 border-right: 1px solid #1a192b;
417 }
418 .react-flow__handle {
419 position: absolute;
420 pointer-events: none;
428 border: 1px solid #1a192b;
429 }
430 .react-flow__handle-connecting {
431 cursor: grabbing;
432 }
433 .react-flow__handle-valid {
434 cursor: crosshair;
435 }
436 .react-flow__handle.connectionindicator {
437 pointer-events: all;
438 cursor: crosshair;
439 }
440 .react-flow__handle-bottom {
441 top: auto;
442 left: 50%;
444 transform: translate(-50%, 0);
445 }
446 .react-flow__handle-top {
447 left: 50%;
448 top: -4px;
449 transform: translate(-50%, 0);
450 }
451 .react-flow__handle-left {
452 top: 50%;
453 left: -4px;
454 transform: translate(0, -50%);
455 }
456 .react-flow__handle-right {
457 right: -4px;
458 top: 50%;
459 transform: translate(0, -50%);
460 }
461 .react-flow__edges {
462 overflow: visible;
463 position: absolute;
469 height: 100%;
470 }
471 .react-flow__edge {
472 pointer-events: visibleStroke;
473 cursor: pointer;
474 }
475 .react-flow__edgeupdater {
476 cursor: move;
477 pointer-events: all;
478 }
479 .react-flow__edge .react-flow__edge-text {
480 fill: #333;
481 }
482 .react-flow__edge.selected .react-flow__edge-textbg,
483 .react-flow__edge:focus .react-flow__edge-textbg,
484 .react-flow__edge:focus-visible .react-flow__edge-textbg {
485 fill: #fff;
486 }
487 .react-flow__edge.selected .react-flow__edge-text,
488 .react-flow__edge:focus .react-flow__edge-text,
489 .react-flow__edge:focus-visible .react-flow__edge-text {
490 fill: #000;
491 }
492 .react-flow .react-flow__edge-interaction {
493 stroke-width: 10;
494 stroke: transparent;
495 fill: none;
496 }
497 .react-flow .react-flow__edge-interaction:hover {
498 stroke: rgba(0, 0, 0, 0.05);
499 }
500 .react-flow__selection {
501 z-index: 5;
502 position: absolute;
503 pointer-events: none;
504 }
505 .react-flow__selectionrect {
506 fill: rgba(0, 89, 179, 0.08);
507 stroke: #0059b3;
508 stroke-width: 1px;
509 }
510 .react-flow__nodesselection {
511 z-index: 5;
512 position: absolute;
518 transform-origin: 0 0;
519 }
520 .react-flow__nodesselection-rect {
521 fill: rgba(0, 89, 179, 0.04);
522 stroke: #0059b3;
524 stroke-dasharray: 5;
525 }
526 .react-flow__background {
527 position: absolute;
528 width: 100%;
532 z-index: 0;
533 }
534 .react-flow__background.dots pattern {
535 fill: #91919a;
536 }
537 .react-flow__background.lines path {
538 stroke: #eee;
539 }
540 .react-flow__minimap {
541 position: absolute;
542 z-index: 6;
547 box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.1);
548 }
549 .react-flow__minimap-mask {
550 fill: rgba(240, 240, 240, 0.6);
551 cursor: grab;
552 }
553 .react-flow__minimap-mask:active {
554 cursor: grabbing;
555 }
556 .react-flow__minimap-node {
557 fill: #e2e2e2;
558 stroke: none;
559 }
560 .react-flow__controls {
561 display: flex;
562 box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08);
568 left: 10px;
569 }
570 .react-flow__controls-button {
571 border: none;
572 background-color: #fff;
579 align-items: center;
580 }
581 .react-flow__controls-button:first-of-type {
582 border: none;
583 border-top-left-radius: 5px;
584 border-bottom-left-radius: 5px;
585 }
586 .react-flow__controls-button:last-of-type {
587 border-top-right-radius: 5px;
588 border-bottom-right-radius: 5px;
589 }
590 .react-flow__controls-button:hover {
591 background-color: #f4f4f4;
592 }
593 .react-flow__controls-button:disabled {
594 fill-opacity: 0.4;
595 cursor: not-allowed;
596 }
597 .react-flow__controls-button:disabled:hover {
598 background-color: #fff;
599 }
600 .react-flow__controls-button svg {
601 width: 12px;
602 height: 12px;
603 display: block;
604 }
605 .react-flow__controls-interactive {
606 cursor: default !important;
607 pointer-events: none !important;
608 }
609 .react-flow__attribution {
610 font-size: 10px;
611 background: rgba(255, 255, 255, 0.5);
616 bottom: 10px;
617 }
618 .react-flow__attribution a {
619 text-decoration: none;
620 color: #333;
621 }
622 .react-flow__resize-control {
623 z-index: 4;
624 position: absolute;
629 border-radius: 1px;
630 }
631 .react-flow__resize-control.handle {
632 border-radius: 50%;
633 width: 10px;
634 height: 10px;
635 }
636 .react-flow__resize-control.line {
637 border-radius: 0;
638 }
639 .react-flow__resize-control.top,
640 .react-flow__resize-control.bottom {
641 width: 100%;
642 cursor: ns-resize;
643 }
644 .react-flow__resize-control.left,
645 .react-flow__resize-control.right {
646 height: 100%;
647 cursor: ew-resize;
648 }
649 .react-flow__resize-control.top {
650 top: -4px;
651 left: 0;
652 }
653 .react-flow__resize-control.bottom {
654 bottom: -4px;
655 left: 0;
656 }
657 .react-flow__resize-control.left {
658 top: 0;
659 left: -4px;
660 }
661 .react-flow__resize-control.right {
662 top: 0;
663 right: -4px;
664 }
665 .react-flow__resize-control.top-left {
666 top: -4px;
667 left: -4px;
668 cursor: nwse-resize;
669 }
670 .react-flow__resize-control.top-right {
671 top: -4px;
672 right: -4px;
673 cursor: nesw-resize;
674 }
675 .react-flow__resize-control.bottom-left {
676 bottom: -4px;
677 left: -4px;
678 cursor: nesw-resize;
679 }
680 .react-flow__resize-control.bottom-right {
681 bottom: -4px;
682 right: -4px;
683 cursor: nwse-resize;
684 }
685 .react-flow__node-resizer {
686 position: absolute;
687 z-index: 4;
688 }
689 .react-flow .nopan {
690 pointer-events: none;
691 }
692 /* --- End React Flow CSS --- */
693 `;
694
716 <script type="module">
717 // --- Client-Side Imports ---
718 import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
719 import React, { useState, useCallback, useEffect, useMemo } from "https://esm.sh/react@18.2.0";
720 import ReactFlow, {
721 Controls,
722 Background,
726 // Types needed for state and props
727 type Node, type Edge, type OnNodesChange, type OnEdgesChange, type DefaultEdgeOptions
728 } from 'https://esm.sh/reactflow@11.11.3?dev'; // Use dev version for better debugging if needed
729
730 // --- Client-Side Type Definitions (Subset needed for React) ---
731 type ClientPolicyNode = Node<{ label: string; type?: string; details?: string }>;
732 type ClientPolicyEdge = Edge<{ label?: string }>;
739 }
740
741 // --- React App Component ---
742 function App() {
743 const [policyText, setPolicyText] = useState("");
748 const [error, setError] = useState<string | null>(null);
749
750 // --- React Flow State Handlers ---
751 const onNodesChange: OnNodesChange = useCallback(
752 (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
821 }
822
823 // --- React Flow requires 'position' ---
824 // If the server didn't add positions, add basic random ones here
825 // A layout library (Dagre, ELK) would be much better for real applications
848
849 // --- JSX Rendering ---
850 return React.createElement(React.Fragment, null,
851 React.createElement("header", null,
852 React.createElement("h1", null, "🔗 Policy Analysis Tool")
853 ),
854 React.createElement("div", { className: "main-content" },
855 React.createElement("div", { className: "control-panel" },
856 React.createElement("h2", null, "Policy Text Input"),
857 React.createElement("textarea", {
858 value: policyText,
859 onChange: (e) => setPolicyText(e.target.value),
863 rows: 10, // Example row count
864 }),
865 React.createElement("button", { onClick: handleAnalyze, disabled: isLoading || !policyText.trim() },
866 isLoading ? "Analyzing..." : "Analyze Connections"
867 ),
868 // Loading Indicator
869 isLoading && React.createElement("div", { className: "loading-indicator"},
870 React.createElement("div", { className: "spinner" }),
871 React.createElement("span", null, "Processing...")
872 ),
873 // Error Display Area
874 error && React.createElement("div", { className: "error-message-container"},
875 React.createElement("div", { className: "error-message", role: "alert"},
876 React.createElement("strong", null, "Error: "), error
877 )
878 ),
879 // Analysis Summary Area
880 analysisSummary && !error && React.createElement("div", { className: "summary-container"},
881 React.createElement("h3", null, "Analysis Summary"),
882 React.createElement("p", null, analysisSummary)
883 )
884 ),
885 React.createElement("div", { className: "graph-container" },
886 React.createElement(ReactFlow, {
887 nodes: nodes,
888 edges: edges,
897 // className: "bg-gradient-to-br from-gray-50 to-gray-100" // Style applied via CSS
898 },
899 React.createElement(Controls, null),
900 React.createElement(Background, { color: "#ccc", variant: "dots", gap: 16, size: 1 })
901 ),
902 // Placeholder messages within the graph container
903 !isLoading && nodes.length === 0 && !error && React.createElement("div", { className: "graph-placeholder" }, "Graph will appear here after analysis."),
904 !isLoading && error && nodes.length === 0 && React.createElement("div", { className: "graph-placeholder error" }, "Could not generate graph due to error.")
905
906 )
907 ),
908 React.createElement("div", { className: "source-link" },
909 "Powered by ",
910 React.createElement("a", { href: "${valTownUrl}", target: "_top" }, "Val Town"),
911 " | View Source"
912 )
914 }
915
916 // --- Mount React App ---
917 const rootEl = document.getElementById("root");
918 if (rootEl) {
919 // Clear the "Loading..." message before rendering React
920 rootEl.innerHTML = '';
921 const reactRoot = createRoot(rootEl);
922 reactRoot.render(React.createElement(React.StrictMode, null, React.createElement(App, null)));
923 } else {
924 console.error("Root element #root not found for React app.");
925 document.body.innerHTML = '<div style="color: red; padding: 20px;"><strong>Error:</strong> Application mount point (#root) not found.</div>';
926 }
979
980 // ** CRITICAL: This prompt needs careful design and testing! **
981 // Adjusted prompt for React Flow format (explicit position: {x:0, y:0})
982 const analyzePolicyPrompt = `
983 You are an expert legal and policy analyst AI. Your task is to read the provided policy text and identify key entities (policies, laws, regulations, specific sections, acts, standards) and the relationships between them (e.g., cites, references, amends, is amended by, supersedes, defines term from, related to).
991 1. **Identify Entities:** Find mentions of specific policies, laws, regulations, acts, standards, or distinct sections (e.g., "Public Law 117-103", "Section 508", "GDPR Article 30", "ADA", "ISO 27001", "OMB A-130"). Treat the main input document itself as an entity if possible.
992 2. **Identify Relationships:** Determine how entities relate based *only* on the text. Look for verbs/phrases like "cites", "references", "pursuant to", "as defined in", "amends", "is amended by", "supersedes", "incorporates", "harmonized with", "see also".
993 3. **Create Nodes:** For each unique entity, create a node object for React Flow.
994 * 'id': Use a concise, unique identifier string (e.g., "PL-117-103", "Sec508", "GDPR-Art30"). NO SPACES or special chars. Use hyphens or camelCase.
995 * 'data': An object containing 'label' (the full name/title for display) and optionally 'type' (e.g., "Law", "Regulation", "Section").
996 * 'position': An object \`{ "x": 0, "y": 0 }\` (Client will handle actual layout).
997 4. **Create Edges:** For each relationship (A -> B), create an edge object for React Flow.
998 * 'id': A unique edge identifier string (e.g., "e1", "edge-Sec508-cites-PL117"). Generate simple IDs like "e1", "e2"...
999 * 'source': The 'id' of the source node (A).
1003 6. **Summary:** Briefly summarize the main topic and the most significant relationships found (1-2 sentences).
1004
1005 **Output Format:** Respond ONLY with a valid JSON object containing 'nodes', 'edges', and 'analysisSummary' keys, strictly adhering to the React Flow structure described above. Example:
1006 {
1007 "nodes": [