flock-core 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (48) hide show
  1. flock/__init__.py +4 -0
  2. flock/agents/__init__.py +3 -0
  3. flock/agents/batch_agent.py +175 -0
  4. flock/agents/declarative_agent.py +166 -0
  5. flock/agents/loop_agent.py +178 -0
  6. flock/agents/trigger_agent.py +191 -0
  7. flock/agents/user_agent.py +230 -0
  8. flock/app/components/__init__.py +14 -0
  9. flock/app/components/charts/agent_workflow.py +14 -0
  10. flock/app/components/charts/core_architecture.py +14 -0
  11. flock/app/components/charts/tool_system.py +14 -0
  12. flock/app/components/history_grid.py +168 -0
  13. flock/app/components/history_grid_alt.py +189 -0
  14. flock/app/components/sidebar.py +19 -0
  15. flock/app/components/theme.py +9 -0
  16. flock/app/components/util.py +18 -0
  17. flock/app/hive_app.py +118 -0
  18. flock/app/html/d3.html +179 -0
  19. flock/app/modules/__init__.py +12 -0
  20. flock/app/modules/about.py +17 -0
  21. flock/app/modules/agent_detail.py +70 -0
  22. flock/app/modules/agent_list.py +59 -0
  23. flock/app/modules/playground.py +322 -0
  24. flock/app/modules/settings.py +96 -0
  25. flock/core/__init__.py +7 -0
  26. flock/core/agent.py +150 -0
  27. flock/core/agent_registry.py +162 -0
  28. flock/core/config/declarative_agent_config.py +0 -0
  29. flock/core/context.py +279 -0
  30. flock/core/context_vars.py +6 -0
  31. flock/core/flock.py +208 -0
  32. flock/core/handoff/handoff_base.py +12 -0
  33. flock/core/logging/__init__.py +18 -0
  34. flock/core/logging/error_handler.py +84 -0
  35. flock/core/logging/formatters.py +122 -0
  36. flock/core/logging/handlers.py +117 -0
  37. flock/core/logging/logger.py +107 -0
  38. flock/core/serializable.py +206 -0
  39. flock/core/tools/basic_tools.py +98 -0
  40. flock/workflow/activities.py +115 -0
  41. flock/workflow/agent_activities.py +26 -0
  42. flock/workflow/temporal_setup.py +37 -0
  43. flock/workflow/workflow.py +53 -0
  44. flock_core-0.1.1.dist-info/METADATA +449 -0
  45. flock_core-0.1.1.dist-info/RECORD +48 -0
  46. flock_core-0.1.1.dist-info/WHEEL +4 -0
  47. flock_core-0.1.1.dist-info/entry_points.txt +2 -0
  48. flock_core-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,168 @@
1
+ """Agent History Explorer built with MonsterUI"""
2
+
3
+ import json
4
+ from datetime import datetime
5
+
6
+ from fasthtml.common import *
7
+ from monsterui.all import *
8
+
9
+
10
+ def load_history(page=1, per_page=10, search=None):
11
+ with open("data/history.json") as f:
12
+ data = json.load(f)
13
+
14
+ if search:
15
+ search = search.lower()
16
+ data = [
17
+ d
18
+ for d in data
19
+ if search in d["agent_name"].lower() or search in d["location"].lower() or search in d["date"].lower()
20
+ ]
21
+
22
+ total = len(data)
23
+ pages = (total + per_page - 1) // per_page
24
+ start = (page - 1) * per_page
25
+ return {"data": data[start : start + per_page], "total": total, "pages": pages, "page": page}
26
+
27
+
28
+ def format_date(iso_date):
29
+ return datetime.fromisoformat(iso_date).strftime("%b %d, %Y %H:%M")
30
+
31
+
32
+ def json_preview(value):
33
+ return Pre(json.dumps(value, indent=2), cls="text-xs p-2 bg-muted rounded-md max-h-40 overflow-auto")
34
+
35
+
36
+ def header_render(col):
37
+ return Th(col, cls="p-2 text-left")
38
+
39
+
40
+ def cell_render(col, val):
41
+ """Modified cell renderer that only uses column value"""
42
+ match col:
43
+ case "Agent":
44
+ return Td(cls="p-2 font-medium")(Div(val))
45
+ case "Date":
46
+ return Td(cls="p-2 text-sm")(format_date(val))
47
+ case "Location":
48
+ return Td(cls="p-2 capitalize")(val) # val is already location string
49
+ case "Input" | "Output":
50
+ return Td(cls="p-2")(json_preview(val))
51
+ case "Details":
52
+ # val contains the full record here
53
+ return Td(cls="p-2")(Button("View", cls=ButtonT.primary, uk_toggle=f"target: #details-{val['agent_id']}"))
54
+
55
+
56
+ def details_modal(record):
57
+ return Modal(
58
+ Div(cls="p-6 space-y-4")(
59
+ ModalTitle(f"{record['agent_name']} - {format_date(record['date'])}"),
60
+ Grid(
61
+ Div(cls="space-y-2")(H4("Input Details", cls="text-sm font-medium"), json_preview(record["input"])),
62
+ Div(cls="space-y-2")(H4("Output Details", cls="text-sm font-medium"), json_preview(record["output"])),
63
+ ),
64
+ DivRAligned(ModalCloseButton("Close", cls=ButtonT.ghost)),
65
+ ),
66
+ id=f"details-{record['agent_id']}",
67
+ )
68
+
69
+
70
+ def get_history(page: int = 1, per_page: int = 5, search: str = None, reduced=False):
71
+ history = load_history(page, per_page, search)
72
+
73
+ controls = DivFullySpaced(cls="mt-8")(
74
+ Div(cls="flex gap-4 items-center")(
75
+ Input(
76
+ placeholder="Search agents...",
77
+ value=search,
78
+ name="search",
79
+ hx_get="/history",
80
+ hx_trigger="keyup changed delay:500ms",
81
+ hx_target="#history-content",
82
+ hx_include="[name='per_page']",
83
+ cls="w-64",
84
+ ),
85
+ Select(
86
+ Option("5", value="5"),
87
+ Option("10", value="10"),
88
+ Option("20", value="20"),
89
+ name="per_page",
90
+ value=str(per_page),
91
+ hx_get="/history",
92
+ hx_trigger="change",
93
+ hx_target="#history-content",
94
+ cls="w-24",
95
+ ),
96
+ )
97
+ )
98
+
99
+ if reduced:
100
+ header_data = ["Agent", "Date", "Details"]
101
+ bodydata = [
102
+ {
103
+ "Agent": d["agent_name"],
104
+ "Date": d["date"],
105
+ "Details": d, # Store full record here
106
+ }
107
+ for d in history["data"]
108
+ ]
109
+ else:
110
+ header_data = ["Agent", "Date", "Input", "Output", "Details"]
111
+ bodydata = [
112
+ {
113
+ "Agent": d["agent_name"],
114
+ "Date": d["date"],
115
+ # "Location": d["input"]["location"], # Pre-extract location
116
+ "Input": d["input"],
117
+ "Output": d["output"],
118
+ "Details": d, # Store full record here
119
+ }
120
+ for d in history["data"]
121
+ ]
122
+
123
+ table = TableFromDicts(
124
+ header_data=header_data,
125
+ body_data=bodydata,
126
+ body_cell_render=cell_render,
127
+ header_cell_render=header_render,
128
+ cls=TableT.responsive,
129
+ )
130
+
131
+ footer = DivFullySpaced(cls="mt-4")(
132
+ Div(
133
+ f"Showing {min(history['total'], per_page)} of {history['total']} records",
134
+ cls="text-sm text-muted-foreground",
135
+ ),
136
+ Div(cls="flex items-center gap-4")(
137
+ Button(
138
+ "< Prev",
139
+ hx_get=f"/history?page={history['page'] - 1}",
140
+ hx_target="#history-content",
141
+ disabled=history["page"] == 1,
142
+ cls=ButtonT.primary,
143
+ ),
144
+ Span(f"Page {history['page']} of {history['pages']}"),
145
+ Button(
146
+ "Next >",
147
+ hx_get=f"/history?page={history['page'] + 1}",
148
+ hx_target="#history-content",
149
+ disabled=history["page"] == history["pages"],
150
+ cls=ButtonT.primary,
151
+ ),
152
+ ),
153
+ )
154
+
155
+ return Div(id="history-content")(controls, table, footer, *[details_modal(d) for d in history["data"]])
156
+
157
+
158
+ def HistoryGrid(reduced=False):
159
+ if reduced:
160
+ return get_history(reduced)
161
+
162
+ return Div(cls="flex flex-col")(
163
+ Div(cls="px-4 py-2 ")(
164
+ H3("Agent History Explorer"),
165
+ P("Review historical agent executions and their outcomes", cls=TextFont.muted_sm),
166
+ ),
167
+ get_history(reduced),
168
+ )
@@ -0,0 +1,189 @@
1
+ """Agent History Explorer built with MonsterUI"""
2
+
3
+ import json
4
+ from datetime import datetime
5
+
6
+ from fasthtml.common import *
7
+ from monsterui.all import *
8
+
9
+
10
+ def format_date(iso_date):
11
+ return datetime.fromisoformat(iso_date).strftime("%b %d, %Y %H:%M")
12
+
13
+
14
+ def json_preview(value):
15
+ return Pre(json.dumps(value, indent=2), cls="text-xs p-2 bg-muted rounded-md max-h-40 overflow-auto")
16
+
17
+
18
+ def header_render(col):
19
+ return Th(col, cls="p-2 text-left")
20
+
21
+
22
+ def cell_render(col, val):
23
+ """Modified cell renderer that only uses column value"""
24
+ match col:
25
+ case "Agent":
26
+ return Td(cls="p-2 font-medium")(Div(val))
27
+ case "Date":
28
+ return Td(cls="p-2 text-sm")(format_date(val))
29
+ case "Location":
30
+ return Td(cls="p-2 capitalize")(val) # val is already location string
31
+ case "Input" | "Output":
32
+ return Td(cls="p-2")(json_preview(val))
33
+ case "Details":
34
+ # val contains the full record here
35
+ return Td(cls="p-2")(Button("View", cls=ButtonT.primary, uk_toggle=f"target: #details-{val['agent_id']}"))
36
+
37
+
38
+ def load_history(page=1, per_page=10, search=None):
39
+ with open("data/history.json") as f:
40
+ data = json.load(f)
41
+
42
+ # Full-text search across all fields
43
+ if search:
44
+ search = search.lower()
45
+ data = [
46
+ d
47
+ for d in data
48
+ if any(search in str(v).lower() for v in d.values())
49
+ or any(search in str(v).lower() for sub in [d["input"], d["output"]] for v in sub.values())
50
+ ]
51
+
52
+ total = len(data)
53
+ pages = (total + per_page - 1) // per_page
54
+ start = (page - 1) * per_page
55
+ return {"data": data[start : start + per_page], "total": total, "pages": pages, "page": page}
56
+
57
+
58
+ def json_tree(value, depth=0):
59
+ if isinstance(value, dict):
60
+ return Div(cls=f"ml-{depth * 2} space-y-1")(
61
+ *[Details(Summary(k, cls="inline-flex items-center"), json_tree(v, depth + 1)) for k, v in value.items()]
62
+ )
63
+ elif isinstance(value, list):
64
+ return Div(cls=f"ml-{depth * 2} space-y-1")(
65
+ *[
66
+ Div(cls="flex items-center gap-2")(Span("•", cls="text-muted-foreground"), json_tree(item, depth + 1))
67
+ for item in value
68
+ ]
69
+ )
70
+ return Span(str(value), cls="text-muted-foreground")
71
+
72
+
73
+ def render_cell(col, val):
74
+ match col:
75
+ case "date":
76
+ return datetime.fromisoformat(val).strftime("%b %d, %Y %H:%M")
77
+ case "input" | "output":
78
+ return Div(cls="max-w-[300px] overflow-auto p-2 bg-muted rounded-md")(json_tree(val))
79
+ case "_expand":
80
+ return Button("Expand", cls=ButtonT.primary, uk_toggle=f"target: #details-{val['agent_id']}")
81
+ case _:
82
+ return val if isinstance(val, str) else json.dumps(val, default=str)
83
+
84
+
85
+ def details_modal(record):
86
+ return Modal(
87
+ Div(cls="p-6 space-y-4")(
88
+ ModalTitle(f"Agent Execution Details - {record['agent_id']}"),
89
+ Div(cls="space-y-4")(
90
+ Div(cls="grid grid-cols-2 gap-4")(
91
+ Div(cls="space-y-2")(H4("Input", cls="text-sm font-medium"), json_tree(record["input"])),
92
+ Div(cls="space-y-2")(H4("Output", cls="text-sm font-medium"), json_tree(record["output"])),
93
+ ),
94
+ Div(cls="space-y-2")(
95
+ H4("Metadata", cls="text-sm font-medium"),
96
+ json_tree({k: v for k, v in record.items() if k not in ["input", "output"]}),
97
+ ),
98
+ ),
99
+ DivRAligned(ModalCloseButton("Close", cls=ButtonT.ghost)),
100
+ ),
101
+ id=f"details-{record['agent_id']}",
102
+ )
103
+
104
+
105
+ def Pagination(current_page, total_pages, hx_get, hx_target):
106
+ return Div(cls="flex items-center gap-2")(
107
+ Button(
108
+ "Previous",
109
+ disabled=current_page == 1,
110
+ hx_get=f"{hx_get}?page={current_page - 1}",
111
+ hx_target=hx_target,
112
+ cls=ButtonT.primary,
113
+ ),
114
+ Span(f"Page {current_page} of {total_pages}", cls="text-sm"),
115
+ Button(
116
+ "Next",
117
+ disabled=current_page >= total_pages,
118
+ hx_get=f"{hx_get}?page={current_page + 1}",
119
+ hx_target=hx_target,
120
+ cls=ButtonT.primary,
121
+ ),
122
+ )
123
+
124
+
125
+ def get_history(page: int = 1, per_page: int = 10, search: str = None):
126
+ history = load_history(page, per_page, search)
127
+
128
+ # Dynamic columns from first record (if exists)
129
+ columns = []
130
+ if history["data"]:
131
+ sample = history["data"][0]
132
+ columns = [k for k in sample.keys() if k not in ["input", "output", "agent_id"]]
133
+ columns += ["input", "output", "_expand"]
134
+
135
+ controls = DivFullySpaced(cls="mt-8")(
136
+ Input(
137
+ placeholder="Search history...",
138
+ value=search,
139
+ name="search",
140
+ hx_get="/history",
141
+ hx_trigger="keyup changed delay:500ms",
142
+ hx_target="#history-content",
143
+ hx_include="[name='per_page']",
144
+ cls="w-64",
145
+ ),
146
+ Select(
147
+ *[Option(str(n), value=str(n)) for n in [5, 10, 20, 50]],
148
+ name="per_page",
149
+ value=str(per_page),
150
+ hx_get="/history",
151
+ hx_trigger="change",
152
+ hx_target="#history-content",
153
+ cls="w-24",
154
+ ),
155
+ )
156
+
157
+ table = TableFromDicts(
158
+ header_data=columns,
159
+ body_data=[{**d, "_expand": d} for d in history["data"]],
160
+ body_cell_render=lambda col, val: Td(render_cell(col, val)),
161
+ header_cell_render=lambda col: Th(col.replace("_", " ").title(), cls="p-2 text-left"),
162
+ cls=f"{TableT.responsive} {TableT.hover}",
163
+ )
164
+
165
+ footer = DivFullySpaced(cls="mt-4")(
166
+ Div(
167
+ f"Showing {(page - 1) * per_page + 1}-{min(page * per_page, history['total'])} of {history['total']} records",
168
+ cls="text-sm text-muted-foreground",
169
+ ),
170
+ Pagination(current_page=page, total_pages=history["pages"], hx_get="/history", hx_target="#history-content"),
171
+ )
172
+
173
+ return Div(id="history-content")(
174
+ controls,
175
+ table if history["data"] else P("No records found", cls=TextFont.muted),
176
+ footer,
177
+ *[details_modal(d) for d in history["data"]],
178
+ )
179
+
180
+
181
+ def HistoryGrid(reduced=False):
182
+ return Container(
183
+ Div(cls="space-y-4")(
184
+ H1("Agent History Explorer"),
185
+ P("Inspect agent executions with dynamic JSON exploration", cls=TextFont.muted_sm),
186
+ ),
187
+ get_history(),
188
+ cls="p-8",
189
+ )
@@ -0,0 +1,19 @@
1
+ from fasthtml.common import *
2
+ from fasthtml.svg import *
3
+ from monsterui.all import *
4
+
5
+
6
+ def SidebarLi(icon, title=None, href=None):
7
+ if icon == "---":
8
+ return Li(Hr())
9
+ return Li(
10
+ AX(DivLAligned(Span(UkIcon(icon)), Span(title)), hx_get=href, hx_target="#main-grid", hx_swap="outerHTML")
11
+ )
12
+
13
+
14
+ def Sidebar(sidebar):
15
+ return NavContainer(
16
+ NavHeaderLi(H1("Flock UI", cls="text-xxl font-semibold")),
17
+ *[SidebarLi(icon, title, href) for icon, title, href in sidebar],
18
+ cls="space-y-6 mt-3",
19
+ )
@@ -0,0 +1,9 @@
1
+ from fasthtml.common import *
2
+ from fasthtml.svg import *
3
+ from monsterui.all import *
4
+
5
+
6
+ def ThemeDialog():
7
+ from fasthtml.components import Uk_theme_switcher
8
+
9
+ return Uk_theme_switcher()
@@ -0,0 +1,18 @@
1
+ from datetime import datetime
2
+
3
+ from fasthtml.common import *
4
+ from fasthtml.svg import *
5
+ from monsterui.all import *
6
+
7
+
8
+ def format_date(date_str):
9
+ date_obj = datetime.fromisoformat(date_str)
10
+ return date_obj.strftime("%Y-%m-%d %I:%M %p")
11
+
12
+
13
+ def IconNavItem(*d):
14
+ return [Li(A(UkIcon(o[0], uk_tooltip=o[1]))) for o in d]
15
+
16
+
17
+ def IconNav(*c, cls=""):
18
+ return Ul(cls=f"uk-iconnav {cls}")(*c)
flock/app/hive_app.py ADDED
@@ -0,0 +1,118 @@
1
+ from fasthtml.common import *
2
+ from fasthtml.svg import *
3
+ from monsterui.all import *
4
+
5
+ from flock.app.components import HistoryGrid, Sidebar
6
+ from flock.app.modules import AgentContent, AgentDetailView, Playground, Settings, agent_data
7
+ from flock.app.modules.about import AboutPage
8
+
9
+ hdrs = Theme.blue.headers()
10
+
11
+ app, rt = fast_app(hdrs=hdrs, live=True)
12
+
13
+ ##############################
14
+ # Sidebar
15
+ ##############################
16
+
17
+ sidebar = (
18
+ ("layout-dashboard", "Dashboard", "/"),
19
+ ("---", "", ""),
20
+ ("bot", "Agents", "/agents"),
21
+ ("server", "Agent Systems", "/systems"),
22
+ ("scroll", "History", "/history"),
23
+ ("---", "", ""),
24
+ ("wrench", "Tools", "/tools"),
25
+ ("test-tube", "Playground", "/playground"),
26
+ ("---", "", ""),
27
+ ("settings", "Settings", "/settings"),
28
+ ("info", "About", "/about"),
29
+ )
30
+
31
+
32
+ ##############################
33
+ # MAIN
34
+ ##############################
35
+
36
+
37
+ @rt
38
+ def index():
39
+ return (
40
+ Title("Flock UI"),
41
+ Container(
42
+ Grid(
43
+ Div(Sidebar(sidebar), cls="col-span-1 w-48 flex"),
44
+ Grid(
45
+ Div(AgentContent(), cls="col-span-2"),
46
+ Div(AgentDetailView(agent_data[0]), cls="col-span-3"),
47
+ cols=5,
48
+ cls="col-span-5",
49
+ id="main-grid",
50
+ ),
51
+ cols_sm=2,
52
+ cols_md=2,
53
+ cols_lg=6,
54
+ cols_xl=6,
55
+ gap=5,
56
+ cls="flex-1",
57
+ ),
58
+ cls=("flex", ContainerT.xl),
59
+ ),
60
+ )
61
+
62
+
63
+ @rt("/history")
64
+ def get():
65
+ return Grid(
66
+ Div(HistoryGrid(), cls="col-span-5"),
67
+ cols=5,
68
+ cls="col-span-5",
69
+ id="main-grid",
70
+ )
71
+
72
+
73
+ @rt("/playground")
74
+ def get():
75
+ return Grid(
76
+ Div(Playground(), cls="col-span-5"), # noqa: F405
77
+ cols=5,
78
+ cls="col-span-5",
79
+ id="main-grid",
80
+ )
81
+
82
+
83
+ @rt("/settings")
84
+ def get():
85
+ return Grid(
86
+ Div(Settings(), cls="col-span-5"),
87
+ cols=5,
88
+ cls="col-span-5",
89
+ id="main-grid",
90
+ )
91
+
92
+
93
+ @rt("/agents")
94
+ def get():
95
+ return Grid(
96
+ Div(AgentContent(), cls="col-span-2"),
97
+ Div(AgentDetailView(agent=agent_data[0]), cls="col-span-3"),
98
+ cols=5,
99
+ cls="col-span-5",
100
+ id="main-grid",
101
+ )
102
+
103
+
104
+ @rt("/about")
105
+ def get():
106
+ return Grid(
107
+ Div(AboutPage(), cls="col-span-5"),
108
+ cols=5,
109
+ cls="col-span-5",
110
+ id="main-grid",
111
+ )
112
+
113
+
114
+ serve()
115
+
116
+
117
+ def main():
118
+ serve()
flock/app/html/d3.html ADDED
@@ -0,0 +1,179 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>D3.js Node Graph</title>
6
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
7
+ <style>
8
+ .node {
9
+ stroke: #fff;
10
+ stroke-width: 1.5px;
11
+ }
12
+
13
+ .link {
14
+ stroke: #999;
15
+ stroke-opacity: 0.6;
16
+ stroke-width: 1px;
17
+ }
18
+
19
+ .node text {
20
+ pointer-events: none;
21
+ font: 10px sans-serif;
22
+ }
23
+ </style>
24
+ </head>
25
+
26
+ <body>
27
+ <div>Double-click background to add node. Drag nodes to move them.</div>
28
+ <svg width="800" height="600"></svg>
29
+ <script>
30
+ // Initial data
31
+ let data = {
32
+ nodes: [
33
+ { id: 1, name: "Node 1" },
34
+ { id: 2, name: "Node 2" },
35
+ { id: 3, name: "Node 3" }
36
+ ],
37
+ links: [
38
+ { source: 0, target: 1 },
39
+ { source: 1, target: 2 }
40
+ ]
41
+ };
42
+
43
+ // Create SVG container
44
+ const svg = d3.select("svg");
45
+ const width = +svg.attr("width");
46
+ const height = +svg.attr("height");
47
+
48
+ // Create force simulation
49
+ const simulation = d3.forceSimulation(data.nodes)
50
+ .force("link", d3.forceLink(data.links).id(d => d.id))
51
+ .force("charge", d3.forceManyBody().strength(-300))
52
+ .force("center", d3.forceCenter(width / 2, height / 2));
53
+
54
+ // Create the links
55
+ const link = svg.append("g")
56
+ .selectAll("line")
57
+ .data(data.links)
58
+ .join("line")
59
+ .attr("class", "link");
60
+
61
+ // Create the nodes
62
+ const node = svg.append("g")
63
+ .selectAll("g")
64
+ .data(data.nodes)
65
+ .join("g")
66
+ .attr("class", "node");
67
+
68
+ // Add circles to nodes
69
+ node.append("circle")
70
+ .attr("r", 10)
71
+ .style("fill", (d, i) => d3.schemeCategory10[i % 10]);
72
+
73
+ // Add labels to nodes
74
+ node.append("text")
75
+ .attr("dx", 12)
76
+ .attr("dy", ".35em")
77
+ .text(d => d.name);
78
+
79
+ // Add drag behavior
80
+ node.call(d3.drag()
81
+ .on("start", dragstarted)
82
+ .on("drag", dragged)
83
+ .on("end", dragended));
84
+
85
+ // Double click on background to add node
86
+ svg.on("dblclick", function (event) {
87
+ const coords = d3.pointer(event);
88
+ const newNode = {
89
+ id: data.nodes.length + 1,
90
+ name: `Node ${data.nodes.length + 1}`,
91
+ x: coords[0],
92
+ y: coords[1]
93
+ };
94
+ data.nodes.push(newNode);
95
+
96
+ // Add link to nearest node
97
+ if (data.nodes.length > 1) {
98
+ const lastNode = data.nodes[data.nodes.length - 2];
99
+ data.links.push({
100
+ source: lastNode,
101
+ target: newNode
102
+ });
103
+ }
104
+
105
+ updateGraph();
106
+ });
107
+
108
+ // Update function to refresh the graph
109
+ function updateGraph() {
110
+ // Update links
111
+ const link = svg.selectAll(".link")
112
+ .data(data.links)
113
+ .join("line")
114
+ .attr("class", "link");
115
+
116
+ // Update nodes
117
+ const node = svg.selectAll(".node")
118
+ .data(data.nodes)
119
+ .join(
120
+ enter => {
121
+ const nodeEnter = enter.append("g")
122
+ .attr("class", "node")
123
+ .call(d3.drag()
124
+ .on("start", dragstarted)
125
+ .on("drag", dragged)
126
+ .on("end", dragended));
127
+
128
+ nodeEnter.append("circle")
129
+ .attr("r", 10)
130
+ .style("fill", (d, i) => d3.schemeCategory10[i % 10]);
131
+
132
+ nodeEnter.append("text")
133
+ .attr("dx", 12)
134
+ .attr("dy", ".35em")
135
+ .text(d => d.name);
136
+
137
+ return nodeEnter;
138
+ }
139
+ );
140
+
141
+ // Update simulation
142
+ simulation.nodes(data.nodes);
143
+ simulation.force("link").links(data.links);
144
+ simulation.alpha(1).restart();
145
+ }
146
+
147
+ // Simulation tick function
148
+ simulation.on("tick", () => {
149
+ link
150
+ .attr("x1", d => d.source.x)
151
+ .attr("y1", d => d.source.y)
152
+ .attr("x2", d => d.target.x)
153
+ .attr("y2", d => d.target.y);
154
+
155
+ node
156
+ .attr("transform", d => `translate(${d.x},${d.y})`);
157
+ });
158
+
159
+ // Drag functions
160
+ function dragstarted(event, d) {
161
+ if (!event.active) simulation.alphaTarget(0.3).restart();
162
+ d.fx = d.x;
163
+ d.fy = d.y;
164
+ }
165
+
166
+ function dragged(event, d) {
167
+ d.fx = event.x;
168
+ d.fy = event.y;
169
+ }
170
+
171
+ function dragended(event, d) {
172
+ if (!event.active) simulation.alphaTarget(0);
173
+ d.fx = null;
174
+ d.fy = null;
175
+ }
176
+ </script>
177
+ </body>
178
+
179
+ </html>