Timothy Eastridge commited on
Commit
75c3e99
·
1 Parent(s): 12db5fc
docker-compose.yml CHANGED
@@ -62,6 +62,15 @@ services:
62
  - mcp
63
  - neo4j
64
 
 
 
 
 
 
 
 
 
 
65
  volumes:
66
  neo4j_data:
67
  postgres_data:
 
62
  - mcp
63
  - neo4j
64
 
65
+ frontend:
66
+ build: ./frontend
67
+ ports:
68
+ - "3000:3000"
69
+ environment:
70
+ - NEXT_PUBLIC_MCP_URL=http://mcp:8000
71
+ depends_on:
72
+ - mcp
73
+
74
  volumes:
75
  neo4j_data:
76
  postgres_data:
frontend/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+ RUN npm install
7
+
8
+ COPY . .
9
+ RUN npm run build
10
+
11
+ EXPOSE 3000
12
+ CMD ["npm", "start"]
frontend/app/globals.css ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ body {
6
+ @apply bg-gray-50;
7
+ }
frontend/app/layout.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './globals.css'
2
+
3
+ export default function RootLayout({
4
+ children,
5
+ }: {
6
+ children: React.ReactNode
7
+ }) {
8
+ return (
9
+ <html lang="en">
10
+ <body>{children}</body>
11
+ </html>
12
+ )
13
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import cytoscape from 'cytoscape';
5
+ import fcose from 'cytoscape-fcose';
6
+
7
+ cytoscape.use(fcose);
8
+
9
+ const MCP_URL = process.env.NEXT_PUBLIC_MCP_URL || 'http://localhost:8000';
10
+ const API_KEY = 'dev-key-123';
11
+
12
+ interface Message {
13
+ id: string;
14
+ role: 'user' | 'assistant' | 'system';
15
+ content: string;
16
+ timestamp: Date;
17
+ }
18
+
19
+ interface WorkflowStatus {
20
+ workflow_id: string;
21
+ current_instruction: string;
22
+ status: string;
23
+ pause_remaining?: number;
24
+ }
25
+
26
+ export default function ChatPage() {
27
+ const [messages, setMessages] = useState<Message[]>([]);
28
+ const [input, setInput] = useState('');
29
+ const [loading, setLoading] = useState(false);
30
+ const [workflowStatus, setWorkflowStatus] = useState<WorkflowStatus | null>(null);
31
+ const [lastInstructions, setLastInstructions] = useState<any[]>([]);
32
+ const [graphData, setGraphData] = useState<any>(null);
33
+ const cyContainer = useRef<HTMLDivElement>(null);
34
+ const cyRef = useRef<any>(null);
35
+
36
+ // MCP API call helper
37
+ const callMCP = async (tool: string, params: any = {}) => {
38
+ const response = await fetch(`${MCP_URL}/mcp`, {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'X-API-Key': API_KEY,
43
+ },
44
+ body: JSON.stringify({ tool, params }),
45
+ });
46
+ return response.json();
47
+ };
48
+
49
+ // Poll for workflow status
50
+ useEffect(() => {
51
+ const interval = setInterval(async () => {
52
+ try {
53
+ // Get active workflow
54
+ const result = await callMCP('query_graph', {
55
+ query: `
56
+ MATCH (w:Workflow {status: 'active'})-[:HAS_INSTRUCTION]->(i:Instruction {status: 'executing'})
57
+ RETURN w.id as workflow_id, i.id as current_instruction, i.type as instruction_type,
58
+ i.status as status, i.pause_duration as pause_duration
59
+ ORDER BY i.sequence DESC
60
+ LIMIT 1
61
+ `,
62
+ });
63
+
64
+ if (result.data && result.data.length > 0) {
65
+ const data = result.data[0];
66
+ setWorkflowStatus({
67
+ workflow_id: data.workflow_id,
68
+ current_instruction: data.instruction_type,
69
+ status: data.status,
70
+ });
71
+ } else {
72
+ setWorkflowStatus(null);
73
+ }
74
+
75
+ // Get last 5 instructions
76
+ const instructionsResult = await callMCP('query_graph', {
77
+ query: `
78
+ MATCH (i:Instruction)-[:EXECUTED_AS]->(e:Execution)
79
+ RETURN i.id as id, i.type as type, i.status as status,
80
+ e.completed_at as completed_at
81
+ ORDER BY e.completed_at DESC
82
+ LIMIT 5
83
+ `,
84
+ });
85
+
86
+ if (instructionsResult.data) {
87
+ setLastInstructions(instructionsResult.data);
88
+ }
89
+
90
+ // Update graph visualization
91
+ await updateGraph();
92
+
93
+ } catch (error) {
94
+ console.error('Error polling status:', error);
95
+ }
96
+ }, 5000); // Poll every 5 seconds
97
+
98
+ return () => clearInterval(interval);
99
+ }, []);
100
+
101
+ // Initialize and update graph
102
+ const updateGraph = async () => {
103
+ try {
104
+ const result = await callMCP('query_graph', {
105
+ query: `
106
+ MATCH (w:Workflow {status: 'active'})-[:HAS_INSTRUCTION]->(i:Instruction)
107
+ OPTIONAL MATCH (i)-[:NEXT_INSTRUCTION]->(next)
108
+ RETURN w, i, next
109
+ `,
110
+ });
111
+
112
+ if (!result.data || result.data.length === 0) return;
113
+
114
+ const nodes: any[] = [];
115
+ const edges: any[] = [];
116
+ const nodeIds = new Set();
117
+
118
+ // Add workflow node
119
+ const workflow = result.data[0].w;
120
+ if (workflow && !nodeIds.has(workflow.id)) {
121
+ nodes.push({
122
+ data: {
123
+ id: workflow.id,
124
+ label: workflow.name || 'Workflow',
125
+ type: 'workflow',
126
+ status: workflow.status
127
+ }
128
+ });
129
+ nodeIds.add(workflow.id);
130
+ }
131
+
132
+ // Add instruction nodes and edges
133
+ result.data.forEach((row: any) => {
134
+ const instruction = row.i;
135
+ const next = row.next;
136
+
137
+ if (instruction && !nodeIds.has(instruction.id)) {
138
+ nodes.push({
139
+ data: {
140
+ id: instruction.id,
141
+ label: instruction.type,
142
+ type: 'instruction',
143
+ status: instruction.status
144
+ }
145
+ });
146
+ nodeIds.add(instruction.id);
147
+
148
+ // Add edge from workflow to instruction
149
+ if (workflow) {
150
+ edges.push({
151
+ data: {
152
+ id: `${workflow.id}-${instruction.id}`,
153
+ source: workflow.id,
154
+ target: instruction.id
155
+ }
156
+ });
157
+ }
158
+ }
159
+
160
+ // Add next instruction edge
161
+ if (instruction && next) {
162
+ edges.push({
163
+ data: {
164
+ id: `${instruction.id}-${next.id}`,
165
+ source: instruction.id,
166
+ target: next.id
167
+ }
168
+ });
169
+ }
170
+ });
171
+
172
+ // Initialize or update Cytoscape
173
+ if (!cyRef.current && cyContainer.current) {
174
+ cyRef.current = cytoscape({
175
+ container: cyContainer.current,
176
+ elements: { nodes, edges },
177
+ style: [
178
+ {
179
+ selector: 'node',
180
+ style: {
181
+ 'label': 'data(label)',
182
+ 'text-valign': 'center',
183
+ 'text-halign': 'center',
184
+ 'width': '80px',
185
+ 'height': '80px',
186
+ 'font-size': '12px',
187
+ 'background-color': (ele: any) => {
188
+ const status = ele.data('status');
189
+ if (status === 'complete') return '#10B981';
190
+ if (status === 'executing') return '#FCD34D';
191
+ if (status === 'failed') return '#EF4444';
192
+ return '#9CA3AF';
193
+ }
194
+ }
195
+ },
196
+ {
197
+ selector: 'edge',
198
+ style: {
199
+ 'width': 2,
200
+ 'line-color': '#9CA3AF',
201
+ 'target-arrow-color': '#9CA3AF',
202
+ 'target-arrow-shape': 'triangle',
203
+ 'curve-style': 'bezier'
204
+ }
205
+ }
206
+ ],
207
+ layout: {
208
+ name: 'fcose'
209
+ }
210
+ });
211
+
212
+ // Add click handler for nodes
213
+ cyRef.current.on('tap', 'node', function(evt: any) {
214
+ const node = evt.target;
215
+ alert(`Node: ${node.data('label')}\nStatus: ${node.data('status')}\nID: ${node.data('id')}`);
216
+ });
217
+
218
+ } else if (cyRef.current) {
219
+ // Update existing graph
220
+ cyRef.current.json({ elements: { nodes, edges } });
221
+ cyRef.current.layout({ name: 'fcose' }).run();
222
+ }
223
+
224
+ } catch (error) {
225
+ console.error('Error updating graph:', error);
226
+ }
227
+ };
228
+
229
+ // Handle message submission
230
+ const handleSubmit = async (e: React.FormEvent) => {
231
+ e.preventDefault();
232
+ if (!input.trim() || loading) return;
233
+
234
+ const userMessage: Message = {
235
+ id: Date.now().toString(),
236
+ role: 'user',
237
+ content: input,
238
+ timestamp: new Date(),
239
+ };
240
+
241
+ setMessages(prev => [...prev, userMessage]);
242
+ setInput('');
243
+ setLoading(true);
244
+
245
+ try {
246
+ // Create a new workflow with the user's question
247
+ const workflowResult = await callMCP('write_graph', {
248
+ action: 'create_node',
249
+ label: 'Workflow',
250
+ properties: {
251
+ id: `workflow-${Date.now()}`,
252
+ name: `Query: ${input.substring(0, 50)}`,
253
+ status: 'active',
254
+ created_at: new Date().toISOString(),
255
+ },
256
+ });
257
+
258
+ // Create instructions for the workflow
259
+ const instructions = [
260
+ { type: 'discover_schema', sequence: 1 },
261
+ { type: 'generate_sql', sequence: 2, parameters: JSON.stringify({ question: input }) },
262
+ { type: 'review_results', sequence: 3 },
263
+ ];
264
+
265
+ for (const inst of instructions) {
266
+ const instResult = await callMCP('write_graph', {
267
+ action: 'create_node',
268
+ label: 'Instruction',
269
+ properties: {
270
+ id: `inst-${Date.now()}-${inst.sequence}`,
271
+ type: inst.type,
272
+ sequence: inst.sequence,
273
+ status: 'pending',
274
+ pause_duration: 30, // Shorter pause for demo
275
+ parameters: inst.parameters || '{}',
276
+ },
277
+ });
278
+
279
+ // Link instruction to workflow
280
+ await callMCP('query_graph', {
281
+ query: `
282
+ MATCH (w:Workflow {id: $wid}), (i:Instruction {id: $iid})
283
+ CREATE (w)-[:HAS_INSTRUCTION]->(i)
284
+ `,
285
+ parameters: {
286
+ wid: workflowResult.created.id,
287
+ iid: instResult.created.id,
288
+ },
289
+ });
290
+ }
291
+
292
+ // Create instruction chain
293
+ const instIds = instructions.map((_, i) => `inst-${Date.now()}-${i + 1}`);
294
+ for (let i = 0; i < instIds.length - 1; i++) {
295
+ await callMCP('query_graph', {
296
+ query: `
297
+ MATCH (i1:Instruction {id: $id1}), (i2:Instruction {id: $id2})
298
+ CREATE (i1)-[:NEXT_INSTRUCTION]->(i2)
299
+ `,
300
+ parameters: { id1: instIds[i], id2: instIds[i + 1] },
301
+ });
302
+ }
303
+
304
+ const systemMessage: Message = {
305
+ id: (Date.now() + 1).toString(),
306
+ role: 'system',
307
+ content: 'Workflow created! The agent will now process your request...',
308
+ timestamp: new Date(),
309
+ };
310
+ setMessages(prev => [...prev, systemMessage]);
311
+
312
+ // Poll for results
313
+ const pollForResults = async () => {
314
+ let attempts = 0;
315
+ const maxAttempts = 60; // 5 minutes max
316
+
317
+ while (attempts < maxAttempts) {
318
+ await new Promise(resolve => setTimeout(resolve, 5000));
319
+
320
+ // Check if SQL generation is complete
321
+ const execResult = await callMCP('query_graph', {
322
+ query: `
323
+ MATCH (i:Instruction {type: 'generate_sql'})-[:EXECUTED_AS]->(e:Execution)
324
+ WHERE i.id IN $inst_ids
325
+ RETURN e.result as result
326
+ ORDER BY e.completed_at DESC
327
+ LIMIT 1
328
+ `,
329
+ parameters: { inst_ids: instIds },
330
+ });
331
+
332
+ if (execResult.data && execResult.data.length > 0) {
333
+ const result = JSON.parse(execResult.data[0].result);
334
+
335
+ if (result.status === 'success') {
336
+ const assistantMessage: Message = {
337
+ id: (Date.now() + 2).toString(),
338
+ role: 'assistant',
339
+ content: formatSQLResult(result),
340
+ timestamp: new Date(),
341
+ };
342
+ setMessages(prev => [...prev, assistantMessage]);
343
+ break;
344
+ }
345
+ }
346
+
347
+ attempts++;
348
+ }
349
+ };
350
+
351
+ pollForResults();
352
+
353
+ } catch (error) {
354
+ console.error('Error creating workflow:', error);
355
+ const errorMessage: Message = {
356
+ id: (Date.now() + 2).toString(),
357
+ role: 'system',
358
+ content: 'Error: Failed to process your request',
359
+ timestamp: new Date(),
360
+ };
361
+ setMessages(prev => [...prev, errorMessage]);
362
+ } finally {
363
+ setLoading(false);
364
+ }
365
+ };
366
+
367
+ // Format SQL results for display
368
+ const formatSQLResult = (result: any) => {
369
+ if (!result.data || result.data.length === 0) {
370
+ return `Query executed successfully but returned no results.\n\nSQL: ${result.generated_sql}`;
371
+ }
372
+
373
+ const columns = Object.keys(result.data[0]);
374
+ const rows = result.data;
375
+
376
+ let table = '<table class="min-w-full border border-gray-300">';
377
+ table += '<thead><tr class="bg-gray-100">';
378
+ columns.forEach(col => {
379
+ table += `<th class="px-4 py-2 border">${col}</th>`;
380
+ });
381
+ table += '</tr></thead><tbody>';
382
+
383
+ rows.forEach((row: any) => {
384
+ table += '<tr>';
385
+ columns.forEach(col => {
386
+ table += `<td class="px-4 py-2 border">${row[col] ?? 'null'}</td>`;
387
+ });
388
+ table += '</tr>';
389
+ });
390
+ table += '</tbody></table>';
391
+
392
+ return `
393
+ <div>
394
+ <p class="mb-2"><strong>Generated SQL:</strong></p>
395
+ <code class="block bg-gray-100 p-2 mb-4 rounded">${result.generated_sql}</code>
396
+ <p class="mb-2"><strong>Results (${result.row_count} rows):</strong></p>
397
+ ${table}
398
+ </div>
399
+ `;
400
+ };
401
+
402
+ // Handle stop button
403
+ const handleStop = async () => {
404
+ if (!workflowStatus) return;
405
+
406
+ await callMCP('query_graph', {
407
+ query: `MATCH (w:Workflow {id: $id}) SET w.status = 'stopped'`,
408
+ parameters: { id: workflowStatus.workflow_id },
409
+ });
410
+
411
+ setWorkflowStatus(null);
412
+ };
413
+
414
+ return (
415
+ <div className="flex h-screen">
416
+ {/* Main Chat Area */}
417
+ <div className="flex-1 flex flex-col">
418
+ {/* Header */}
419
+ <div className="bg-white border-b px-6 py-4">
420
+ <h1 className="text-2xl font-bold">Graph-Driven Agent Chat</h1>
421
+ {workflowStatus && (
422
+ <div className="mt-2 flex items-center space-x-4">
423
+ <span className="text-sm text-gray-600">
424
+ Status: <span className="font-semibold">{workflowStatus.status}</span>
425
+ </span>
426
+ <span className="text-sm text-gray-600">
427
+ Current: <span className="font-semibold">{workflowStatus.current_instruction}</span>
428
+ </span>
429
+ {loading && (
430
+ <span className="text-sm text-yellow-600 animate-pulse">
431
+ Agent thinking...
432
+ </span>
433
+ )}
434
+ </div>
435
+ )}
436
+ </div>
437
+
438
+ {/* Messages */}
439
+ <div className="flex-1 overflow-y-auto p-6 space-y-4">
440
+ {messages.map(message => (
441
+ <div
442
+ key={message.id}
443
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
444
+ >
445
+ <div
446
+ className={`max-w-2xl px-4 py-2 rounded-lg ${
447
+ message.role === 'user'
448
+ ? 'bg-blue-500 text-white'
449
+ : message.role === 'assistant'
450
+ ? 'bg-gray-200'
451
+ : 'bg-yellow-100'
452
+ }`}
453
+ >
454
+ {message.content.startsWith('<div>') ? (
455
+ <div dangerouslySetInnerHTML={{ __html: message.content }} />
456
+ ) : (
457
+ <p>{message.content}</p>
458
+ )}
459
+ <p className="text-xs mt-1 opacity-70">
460
+ {message.timestamp.toLocaleTimeString()}
461
+ </p>
462
+ </div>
463
+ </div>
464
+ ))}
465
+ </div>
466
+
467
+ {/* Input Form */}
468
+ <form onSubmit={handleSubmit} className="border-t bg-white p-4">
469
+ <div className="flex space-x-4">
470
+ <input
471
+ type="text"
472
+ value={input}
473
+ onChange={e => setInput(e.target.value)}
474
+ placeholder="Ask a question about your data..."
475
+ className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
476
+ disabled={loading}
477
+ />
478
+ <button
479
+ type="submit"
480
+ disabled={loading || !input.trim()}
481
+ className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-400"
482
+ >
483
+ Send
484
+ </button>
485
+ {workflowStatus && (
486
+ <button
487
+ type="button"
488
+ onClick={handleStop}
489
+ className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
490
+ >
491
+ STOP
492
+ </button>
493
+ )}
494
+ </div>
495
+ </form>
496
+ </div>
497
+
498
+ {/* Right Sidebar */}
499
+ <div className="w-96 bg-gray-50 border-l flex flex-col">
500
+ {/* Graph Visualization */}
501
+ <div className="flex-1 p-4">
502
+ <h2 className="text-lg font-semibold mb-2">Workflow Graph</h2>
503
+ <div
504
+ ref={cyContainer}
505
+ className="w-full h-64 bg-white border rounded-lg"
506
+ />
507
+ </div>
508
+
509
+ {/* Recent Instructions */}
510
+ <div className="p-4 border-t">
511
+ <h2 className="text-lg font-semibold mb-2">Recent Instructions</h2>
512
+ <div className="space-y-2">
513
+ {lastInstructions.map((inst, idx) => (
514
+ <div key={idx} className="text-sm bg-white p-2 rounded border">
515
+ <span className={`font-semibold ${
516
+ inst.status === 'complete' ? 'text-green-600' :
517
+ inst.status === 'failed' ? 'text-red-600' :
518
+ 'text-gray-600'
519
+ }`}>
520
+ {inst.type}
521
+ </span>
522
+ <span className="text-gray-500 ml-2">
523
+ {inst.status}
524
+ </span>
525
+ </div>
526
+ ))}
527
+ </div>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ );
532
+ }
frontend/next.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // App directory is enabled by default in Next.js 13+
4
+ }
5
+
6
+ module.exports = nextConfig
frontend/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "agent-frontend",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "dev": "next dev",
6
+ "build": "next build",
7
+ "start": "next start"
8
+ },
9
+ "dependencies": {
10
+ "next": "14.0.0",
11
+ "react": "18.2.0",
12
+ "react-dom": "18.2.0",
13
+ "typescript": "5.2.2",
14
+ "@types/react": "18.2.0",
15
+ "@types/node": "20.8.0",
16
+ "cytoscape": "3.27.0",
17
+ "cytoscape-fcose": "2.2.0",
18
+ "@types/cytoscape": "3.19.0"
19
+ },
20
+ "devDependencies": {
21
+ "tailwindcss": "3.3.5",
22
+ "autoprefixer": "10.4.16",
23
+ "postcss": "8.4.31"
24
+ }
25
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ content: [
3
+ './pages/**/*.{js,ts,jsx,tsx}',
4
+ './components/**/*.{js,ts,jsx,tsx}',
5
+ './app/**/*.{js,ts,jsx,tsx}',
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "preserve",
16
+ "incremental": true,
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["./*"]
20
+ }
21
+ },
22
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"],
23
+ "exclude": ["node_modules"]
24
+ }
frontend/types/cytoscape-fcose.d.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare module 'cytoscape-fcose' {
2
+ import { Core } from 'cytoscape';
3
+
4
+ interface FcoseLayoutOptions {
5
+ name: string;
6
+ animate?: boolean;
7
+ randomize?: boolean;
8
+ fit?: boolean;
9
+ padding?: number;
10
+ nodeRepulsion?: number;
11
+ idealEdgeLength?: number;
12
+ }
13
+
14
+ function fcose(cytoscape: (options?: any) => Core): void;
15
+ export = fcose;
16
+ }