agent-state-machine 1.2.0 → 1.3.1
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.
- package/README.md +8 -3
- package/lib/llm.js +0 -2
- package/lib/runtime/agent.js +2 -1
- package/lib/runtime/prompt.js +2 -1
- package/lib/setup.js +17 -7
- package/lib/ui/index.html +182 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,11 +120,16 @@ export default async function() {
|
|
|
120
120
|
// await agent('yoda-greeter', userInfo);
|
|
121
121
|
|
|
122
122
|
// Example: Parallel execution
|
|
123
|
-
// const [a, b] = await parallel([
|
|
124
|
-
// agent('
|
|
125
|
-
// agent('
|
|
123
|
+
// const [a, b, c] = await parallel([
|
|
124
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
125
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
126
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
126
127
|
// ]);
|
|
127
128
|
|
|
129
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
130
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
131
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
132
|
+
|
|
128
133
|
notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
|
|
129
134
|
|
|
130
135
|
console.log('Workflow completed!');
|
package/lib/llm.js
CHANGED
|
@@ -335,8 +335,6 @@ export async function llm(context, options) {
|
|
|
335
335
|
const models = config.models || {};
|
|
336
336
|
const apiKeys = config.apiKeys || {};
|
|
337
337
|
|
|
338
|
-
// No longer needed to write to prompts/ directory here
|
|
339
|
-
|
|
340
338
|
// Look up the model command/config
|
|
341
339
|
const modelConfig = models[options.model];
|
|
342
340
|
|
package/lib/runtime/agent.js
CHANGED
package/lib/runtime/prompt.js
CHANGED
package/lib/setup.js
CHANGED
|
@@ -99,11 +99,16 @@ export default async function() {
|
|
|
99
99
|
// await agent('yoda-greeter', userInfo);
|
|
100
100
|
|
|
101
101
|
// Example: Parallel execution
|
|
102
|
-
// const [a, b] = await parallel([
|
|
103
|
-
// agent('
|
|
104
|
-
// agent('
|
|
102
|
+
// const [a, b, c] = await parallel([
|
|
103
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
104
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
105
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
105
106
|
// ]);
|
|
106
107
|
|
|
108
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
109
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
110
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
111
|
+
|
|
107
112
|
notify(['${workflowName}', userInfo.name || userInfo + ' has been greeted!']);
|
|
108
113
|
|
|
109
114
|
console.log('Workflow completed!');
|
|
@@ -288,7 +293,7 @@ ${workflowName}/
|
|
|
288
293
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
289
294
|
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
290
295
|
├── interactions/ # Human-in-the-loop inputs (created at runtime)
|
|
291
|
-
├── state/ # Runtime state (current.json, history.jsonl
|
|
296
|
+
├── state/ # Runtime state (current.json, history.jsonl)
|
|
292
297
|
└── steering/ # Steering configuration
|
|
293
298
|
\\\`\\\`\\\`
|
|
294
299
|
|
|
@@ -354,11 +359,16 @@ export default async function() {
|
|
|
354
359
|
// await agent('yoda-greeter', userInfo);
|
|
355
360
|
|
|
356
361
|
// Example: Parallel execution
|
|
357
|
-
// const [a, b] = await parallel([
|
|
358
|
-
// agent('
|
|
359
|
-
// agent('
|
|
362
|
+
// const [a, b, c] = await parallel([
|
|
363
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
364
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
365
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
360
366
|
// ]);
|
|
361
367
|
|
|
368
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
369
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
370
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
371
|
+
|
|
362
372
|
notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
|
|
363
373
|
|
|
364
374
|
console.log('Workflow completed!');
|
package/lib/ui/index.html
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
|
+
|
|
3
4
|
<head>
|
|
4
5
|
<meta charset="UTF-8">
|
|
5
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -14,14 +15,32 @@
|
|
|
14
15
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
|
15
16
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
16
17
|
<style>
|
|
17
|
-
.markdown-body {
|
|
18
|
+
.markdown-body {
|
|
19
|
+
white-space: pre-wrap;
|
|
20
|
+
font-family: monospace;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
/* Scrollbar styles for dark mode */
|
|
19
|
-
.dark ::-webkit-scrollbar {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
.dark ::-webkit-scrollbar {
|
|
25
|
+
width: 10px;
|
|
26
|
+
height: 10px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark ::-webkit-scrollbar-track {
|
|
30
|
+
background: #000000;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.dark ::-webkit-scrollbar-thumb {
|
|
34
|
+
background: #27272a;
|
|
35
|
+
border-radius: 5px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.dark ::-webkit-scrollbar-thumb:hover {
|
|
39
|
+
background: #3f3f46;
|
|
40
|
+
}
|
|
23
41
|
</style>
|
|
24
42
|
</head>
|
|
43
|
+
|
|
25
44
|
<body>
|
|
26
45
|
<div id="root"></div>
|
|
27
46
|
|
|
@@ -64,8 +83,8 @@
|
|
|
64
83
|
};
|
|
65
84
|
|
|
66
85
|
return (
|
|
67
|
-
<button
|
|
68
|
-
onClick={handleCopy}
|
|
86
|
+
<button
|
|
87
|
+
onClick={handleCopy}
|
|
69
88
|
className={`flex items-center space-x-1 text-[9px] uppercase tracking-wider transition-colors hover:text-blue-500 focus:outline-none ${className}`}
|
|
70
89
|
title="Copy to clipboard"
|
|
71
90
|
>
|
|
@@ -75,6 +94,105 @@
|
|
|
75
94
|
);
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
function JsonView({ data, label, onTop = false, timestamp }) {
|
|
98
|
+
const [viewMode, setViewMode] = useState('clean'); // 'clean' | 'raw'
|
|
99
|
+
|
|
100
|
+
const isObject = typeof data === 'object' && data !== null;
|
|
101
|
+
const rawContent = isObject ? JSON.stringify(data, null, 2) : String(data);
|
|
102
|
+
const lineBreak = '';
|
|
103
|
+
|
|
104
|
+
// UPDATED: "clean" view loops through ALL top-level keys.
|
|
105
|
+
// Headers render in CAPS, BOLD, and BLUE.
|
|
106
|
+
let cleanParts = null;
|
|
107
|
+
if (isObject) {
|
|
108
|
+
const keys = Object.keys(data);
|
|
109
|
+
if (keys.length > 0) {
|
|
110
|
+
cleanParts = keys.map((k) => {
|
|
111
|
+
const val = data[k];
|
|
112
|
+
const renderedVal = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
|
|
113
|
+
const prettyVal = String(renderedVal).replace(/\\n/g, lineBreak);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div key={k} className="mb-5 last:mb-0">
|
|
117
|
+
<div className="text-[11px] font-extrabold uppercase tracking-wider text-blue-600 dark:text-blue-400 mb-2">
|
|
118
|
+
{k}
|
|
119
|
+
</div>
|
|
120
|
+
<div className="whitespace-pre-wrap">
|
|
121
|
+
{prettyVal}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// For RAW view we still render as a string
|
|
130
|
+
const rawContentUnescaped = rawContent; // keep raw exact
|
|
131
|
+
|
|
132
|
+
const hasToggle = isObject || rawContent.includes('\\n');
|
|
133
|
+
|
|
134
|
+
if (onTop) {
|
|
135
|
+
return (
|
|
136
|
+
<div className="flex justify-end w-full group">
|
|
137
|
+
<div className="max-w-[85%] bg-blue-50 dark:bg-blue-950/20 border border-blue-100 dark:border-blue-900/40 rounded-2xl rounded-tr-none shadow-sm p-6 transition-all hover:border-blue-200 dark:hover:border-blue-800 relative">
|
|
138
|
+
<div className="flex justify-between items-center mb-3">
|
|
139
|
+
<div className="text-[9px] font-black text-blue-300 dark:text-blue-800/60 uppercase tracking-[0.2em] text-right w-full">
|
|
140
|
+
{label}
|
|
141
|
+
</div>
|
|
142
|
+
<div className="absolute top-4 left-4 opacity-0 group-hover:opacity-100 transition-opacity flex items-center space-x-2">
|
|
143
|
+
{hasToggle && (
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setViewMode(v => v === 'clean' ? 'raw' : 'clean')}
|
|
146
|
+
className="text-[9px] uppercase tracking-wider font-bold text-blue-400 hover:text-blue-600 dark:text-blue-600 dark:hover:text-blue-400 focus:outline-none"
|
|
147
|
+
>
|
|
148
|
+
{viewMode === 'clean' ? 'Raw' : 'Clean'}
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
151
|
+
<CopyButton text={data} className="text-blue-400 hover:text-blue-600 dark:text-blue-600 dark:hover:text-blue-400" />
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="markdown-body text-gray-800 dark:text-zinc-200 text-sm overflow-x-auto leading-relaxed">
|
|
156
|
+
{viewMode === 'clean'
|
|
157
|
+
? (cleanParts ?? String(rawContent).replace(/\\n/g, lineBreak))
|
|
158
|
+
: rawContentUnescaped
|
|
159
|
+
}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="bg-gray-100 dark:bg-zinc-900/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-4 py-3 text-xs w-full max-w-2xl font-mono overflow-x-auto relative group">
|
|
168
|
+
<div className="text-[9px] text-gray-400 dark:text-zinc-600 uppercase tracking-widest mb-1 flex justify-between items-center">
|
|
169
|
+
<div className="flex space-x-4">
|
|
170
|
+
<span>{label}</span>
|
|
171
|
+
{timestamp && <span>{timestamp}</span>}
|
|
172
|
+
</div>
|
|
173
|
+
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center space-x-2">
|
|
174
|
+
{hasToggle && (
|
|
175
|
+
<button
|
|
176
|
+
onClick={() => setViewMode(v => v === 'clean' ? 'raw' : 'clean')}
|
|
177
|
+
className="text-[9px] uppercase tracking-wider font-bold text-zinc-400 hover:text-blue-500 focus:outline-none"
|
|
178
|
+
>
|
|
179
|
+
{viewMode === 'clean' ? 'Raw' : 'Clean'}
|
|
180
|
+
</button>
|
|
181
|
+
)}
|
|
182
|
+
<CopyButton text={data} className="text-gray-400 hover:text-gray-600" />
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="text-gray-600 dark:text-zinc-400 whitespace-pre-wrap">
|
|
187
|
+
{viewMode === 'clean'
|
|
188
|
+
? (cleanParts ?? String(rawContent).replace(/\\n/g, lineBreak))
|
|
189
|
+
: rawContentUnescaped
|
|
190
|
+
}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
78
196
|
function App() {
|
|
79
197
|
const [history, setHistory] = useState([]);
|
|
80
198
|
const [loading, setLoading] = useState(true);
|
|
@@ -129,7 +247,7 @@
|
|
|
129
247
|
</div>
|
|
130
248
|
</div>
|
|
131
249
|
);
|
|
132
|
-
|
|
250
|
+
|
|
133
251
|
if (error) return (
|
|
134
252
|
<div className={theme}>
|
|
135
253
|
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-black text-red-500">
|
|
@@ -138,14 +256,9 @@
|
|
|
138
256
|
</div>
|
|
139
257
|
);
|
|
140
258
|
|
|
141
|
-
// Filter for events we want to display
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
'WORKFLOW_STARTED', 'WORKFLOW_COMPLETED', 'WORKFLOW_FAILED', 'WORKFLOW_RESET',
|
|
145
|
-
'AGENT_STARTED', 'AGENT_COMPLETED', 'AGENT_FAILED',
|
|
146
|
-
'INTERACTION_REQUESTED', 'INTERACTION_RESOLVED'
|
|
147
|
-
].includes(item.event)
|
|
148
|
-
);
|
|
259
|
+
// Filter for events we want to display - NOW INCLUDES EVERYTHING
|
|
260
|
+
// We only filter out nulls or malformed entries if any
|
|
261
|
+
let visibleEvents = history;
|
|
149
262
|
|
|
150
263
|
// Apply Sort
|
|
151
264
|
// History from API is "Newest First" (index 0 is latest)
|
|
@@ -159,26 +272,26 @@
|
|
|
159
272
|
<div className={theme}>
|
|
160
273
|
<div className="min-h-screen bg-gray-50 dark:bg-black transition-colors duration-200">
|
|
161
274
|
<div className="max-w-5xl mx-auto min-h-screen flex flex-col">
|
|
162
|
-
|
|
275
|
+
|
|
163
276
|
{/* Sticky Header */}
|
|
164
277
|
<header className="sticky top-0 z-50 py-4 px-6 bg-gray-50/90 dark:bg-black/90 backdrop-blur-md border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between mb-8 transition-colors">
|
|
165
278
|
<div className="flex-1">
|
|
166
279
|
<h1 className="text-xl font-bold text-gray-800 dark:text-zinc-100 transition-colors uppercase tracking-tight">{workflowName}</h1>
|
|
167
|
-
<p className="text-gray-500 dark:text-zinc-500 text-xs mt-0.5">
|
|
280
|
+
<p className="text-gray-500 dark:text-zinc-500 text-xs mt-0.5">Runtimes History & Prompt Logs</p>
|
|
168
281
|
</div>
|
|
169
282
|
<div className="flex items-center space-x-2">
|
|
170
|
-
<button
|
|
283
|
+
<button
|
|
171
284
|
onClick={toggleSort}
|
|
172
285
|
className="p-2 rounded-full bg-gray-200 dark:bg-zinc-900 text-gray-800 dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-zinc-800 transition-colors"
|
|
173
286
|
title={sortOrder === 'newest' ? "Sort: Newest First" : "Sort: Oldest First"}
|
|
174
287
|
>
|
|
175
|
-
{sortOrder === 'newest' ?
|
|
176
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" /></svg>
|
|
177
|
-
:
|
|
288
|
+
{sortOrder === 'newest' ?
|
|
289
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" /></svg>
|
|
290
|
+
:
|
|
178
291
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" transform="scale(1, -1) translate(0, -24)" /></svg>
|
|
179
292
|
}
|
|
180
293
|
</button>
|
|
181
|
-
<button
|
|
294
|
+
<button
|
|
182
295
|
onClick={toggleTheme}
|
|
183
296
|
className="p-2 rounded-full bg-gray-200 dark:bg-zinc-900 text-gray-800 dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-zinc-800 transition-colors"
|
|
184
297
|
title="Toggle Theme"
|
|
@@ -242,19 +355,40 @@
|
|
|
242
355
|
);
|
|
243
356
|
}
|
|
244
357
|
|
|
245
|
-
// 4. Interaction Requested
|
|
246
|
-
if (item.event === 'INTERACTION_REQUESTED') {
|
|
358
|
+
// 4. Interaction / Prompt Requested
|
|
359
|
+
if (item.event === 'INTERACTION_REQUESTED' || item.event === 'PROMPT_REQUESTED') {
|
|
360
|
+
const isPrompt = item.event === 'PROMPT_REQUESTED';
|
|
247
361
|
return (
|
|
248
362
|
<div key={idx} className="flex justify-center">
|
|
249
363
|
<div className="bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 border-dashed rounded-lg px-6 py-4 text-center max-w-md w-full">
|
|
250
|
-
<div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-bold tracking-widest mb-1">
|
|
251
|
-
|
|
364
|
+
<div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-bold tracking-widest mb-1">
|
|
365
|
+
{isPrompt ? 'User Input Requested' : 'Human Intervention Needed'}
|
|
366
|
+
</div>
|
|
367
|
+
<div className="text-xs text-zinc-600 dark:text-zinc-400 italic">
|
|
368
|
+
{item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"...`}
|
|
369
|
+
</div>
|
|
252
370
|
</div>
|
|
253
371
|
</div>
|
|
254
372
|
);
|
|
255
373
|
}
|
|
256
374
|
|
|
257
|
-
// 5.
|
|
375
|
+
// 5. Prompt Answered
|
|
376
|
+
if (item.event === 'PROMPT_ANSWERED') {
|
|
377
|
+
return (
|
|
378
|
+
<div key={idx} className="flex justify-center">
|
|
379
|
+
<div className="bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900/50 rounded-lg px-4 py-2 text-center max-w-md w-full">
|
|
380
|
+
<div className="text-[10px] text-green-600 dark:text-green-400 uppercase font-bold tracking-widest mb-1">
|
|
381
|
+
User Answered
|
|
382
|
+
</div>
|
|
383
|
+
<div className="text-xs text-green-700 dark:text-green-300 italic font-medium">
|
|
384
|
+
"{item.answer}"
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 6. Agent Completed / Interaction Resolved (The Bubbles)
|
|
258
392
|
if (item.event === 'AGENT_COMPLETED' || item.event === 'INTERACTION_RESOLVED') {
|
|
259
393
|
return (
|
|
260
394
|
<div key={idx} className="flex flex-col space-y-4">
|
|
@@ -269,19 +403,11 @@
|
|
|
269
403
|
|
|
270
404
|
{/* Output (Response) - NOW ON TOP */}
|
|
271
405
|
{(item.output || item.result) && (
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<CopyButton text={item.output || item.result} className="text-blue-400 hover:text-blue-600 dark:text-blue-600 dark:hover:text-blue-400" />
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
<div className="markdown-body text-gray-800 dark:text-zinc-200 text-sm overflow-x-auto leading-relaxed">
|
|
281
|
-
{typeof item.output === 'object' ? JSON.stringify(item.output, null, 2) : (item.output || item.result)}
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
406
|
+
<JsonView
|
|
407
|
+
data={item.output || item.result}
|
|
408
|
+
label="Output / Response"
|
|
409
|
+
onTop={true}
|
|
410
|
+
/>
|
|
285
411
|
)}
|
|
286
412
|
|
|
287
413
|
{/* Prompt (Input) - NOW ON BOTTOM */}
|
|
@@ -304,7 +430,19 @@
|
|
|
304
430
|
);
|
|
305
431
|
}
|
|
306
432
|
|
|
307
|
-
|
|
433
|
+
// 7. CATCH-ALL for Unknown Events
|
|
434
|
+
return (
|
|
435
|
+
<div key={idx} className="flex justify-center px-4">
|
|
436
|
+
<JsonView
|
|
437
|
+
data={JSON.parse(JSON.stringify(item, (key, value) => {
|
|
438
|
+
if (key === 'event' || key === 'timestamp') return undefined; // Keep it clean
|
|
439
|
+
return value;
|
|
440
|
+
}))}
|
|
441
|
+
label={item.event}
|
|
442
|
+
timestamp={formatTime(item.timestamp)}
|
|
443
|
+
/>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
308
446
|
})}
|
|
309
447
|
</div>
|
|
310
448
|
|
|
@@ -321,4 +459,5 @@
|
|
|
321
459
|
root.render(<App />);
|
|
322
460
|
</script>
|
|
323
461
|
</body>
|
|
324
|
-
|
|
462
|
+
|
|
463
|
+
</html>
|