Simford.Dong Claude commited on
Commit
4ee7a5e
·
1 Parent(s): a809d45

Add web UI dashboard for market research

Browse files

Features:
- Interactive dashboard with Chart.js visualizations
- Input form for keyword research
- Real-time market forecast charts
- Regional distribution pie chart
- Competitive landscape table
- Key insights display (opportunities, challenges, drivers)
- Responsive design with gradient theme

- Add static file serving to server.js
- Update Dockerfile to copy public/ directory

Access at: https://sim4imgbed-openclaw.hf.space/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (3) hide show
  1. Dockerfile +3 -0
  2. public/index.html +648 -0
  3. server.js +8 -0
Dockerfile CHANGED
@@ -10,5 +10,8 @@ RUN npm install
10
  COPY src/ ./src/
11
  COPY server.js .
12
 
 
 
 
13
  EXPOSE 7860
14
  CMD ["node", "server.js"]
 
10
  COPY src/ ./src/
11
  COPY server.js .
12
 
13
+ # copy web UI
14
+ COPY public/ ./public/
15
+
16
  EXPOSE 7860
17
  CMD ["node", "server.js"]
public/index.html ADDED
@@ -0,0 +1,648 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OpenClaw Market Research Agent</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .header {
28
+ text-align: center;
29
+ color: white;
30
+ margin-bottom: 30px;
31
+ }
32
+
33
+ .header h1 {
34
+ font-size: 2.5rem;
35
+ margin-bottom: 10px;
36
+ }
37
+
38
+ .header p {
39
+ font-size: 1.1rem;
40
+ opacity: 0.9;
41
+ }
42
+
43
+ .card {
44
+ background: white;
45
+ border-radius: 16px;
46
+ padding: 24px;
47
+ margin-bottom: 20px;
48
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
49
+ }
50
+
51
+ .input-section {
52
+ display: flex;
53
+ gap: 12px;
54
+ align-items: center;
55
+ flex-wrap: wrap;
56
+ }
57
+
58
+ .input-group {
59
+ flex: 1;
60
+ min-width: 200px;
61
+ }
62
+
63
+ .input-group label {
64
+ display: block;
65
+ font-size: 0.9rem;
66
+ color: #666;
67
+ margin-bottom: 6px;
68
+ }
69
+
70
+ .input-group input,
71
+ .input-group select {
72
+ width: 100%;
73
+ padding: 12px 16px;
74
+ border: 2px solid #e0e0e0;
75
+ border-radius: 10px;
76
+ font-size: 1rem;
77
+ transition: border-color 0.2s;
78
+ }
79
+
80
+ .input-group input:focus,
81
+ .input-group select:focus {
82
+ outline: none;
83
+ border-color: #667eea;
84
+ }
85
+
86
+ .btn {
87
+ padding: 12px 24px;
88
+ border: none;
89
+ border-radius: 10px;
90
+ font-size: 1rem;
91
+ font-weight: 600;
92
+ cursor: pointer;
93
+ transition: all 0.2s;
94
+ }
95
+
96
+ .btn-primary {
97
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
98
+ color: white;
99
+ }
100
+
101
+ .btn-primary:hover {
102
+ transform: translateY(-2px);
103
+ box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
104
+ }
105
+
106
+ .btn-primary:disabled {
107
+ opacity: 0.6;
108
+ cursor: not-allowed;
109
+ transform: none;
110
+ }
111
+
112
+ .loading {
113
+ display: none;
114
+ text-align: center;
115
+ padding: 40px;
116
+ }
117
+
118
+ .loading.active {
119
+ display: block;
120
+ }
121
+
122
+ .spinner {
123
+ width: 50px;
124
+ height: 50px;
125
+ border: 4px solid #f3f3f3;
126
+ border-top: 4px solid #667eea;
127
+ border-radius: 50%;
128
+ animation: spin 1s linear infinite;
129
+ margin: 0 auto 20px;
130
+ }
131
+
132
+ @keyframes spin {
133
+ 0% { transform: rotate(0deg); }
134
+ 100% { transform: rotate(360deg); }
135
+ }
136
+
137
+ .results {
138
+ display: none;
139
+ }
140
+
141
+ .results.active {
142
+ display: block;
143
+ }
144
+
145
+ .results-header {
146
+ display: flex;
147
+ justify-content: space-between;
148
+ align-items: center;
149
+ margin-bottom: 20px;
150
+ padding-bottom: 20px;
151
+ border-bottom: 1px solid #e0e0e0;
152
+ }
153
+
154
+ .results-title {
155
+ font-size: 1.5rem;
156
+ color: #333;
157
+ }
158
+
159
+ .meta-info {
160
+ display: flex;
161
+ gap: 20px;
162
+ font-size: 0.9rem;
163
+ color: #666;
164
+ }
165
+
166
+ .meta-item {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 6px;
170
+ }
171
+
172
+ .meta-badge {
173
+ background: #e8f5e9;
174
+ color: #2e7d32;
175
+ padding: 4px 12px;
176
+ border-radius: 20px;
177
+ font-size: 0.85rem;
178
+ font-weight: 600;
179
+ }
180
+
181
+ .summary-grid {
182
+ display: grid;
183
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
184
+ gap: 16px;
185
+ margin-bottom: 24px;
186
+ }
187
+
188
+ .summary-card {
189
+ background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
190
+ border-radius: 12px;
191
+ padding: 20px;
192
+ text-align: center;
193
+ }
194
+
195
+ .summary-label {
196
+ font-size: 0.85rem;
197
+ color: #666;
198
+ margin-bottom: 8px;
199
+ }
200
+
201
+ .summary-value {
202
+ font-size: 1.8rem;
203
+ font-weight: 700;
204
+ color: #333;
205
+ }
206
+
207
+ .summary-value.highlight {
208
+ color: #667eea;
209
+ }
210
+
211
+ .chart-section {
212
+ margin-bottom: 24px;
213
+ }
214
+
215
+ .chart-title {
216
+ font-size: 1.1rem;
217
+ color: #333;
218
+ margin-bottom: 16px;
219
+ }
220
+
221
+ .chart-container {
222
+ height: 300px;
223
+ position: relative;
224
+ }
225
+
226
+ .data-table {
227
+ width: 100%;
228
+ border-collapse: collapse;
229
+ margin-top: 16px;
230
+ }
231
+
232
+ .data-table th,
233
+ .data-table td {
234
+ padding: 12px;
235
+ text-align: left;
236
+ border-bottom: 1px solid #e0e0e0;
237
+ }
238
+
239
+ .data-table th {
240
+ background: #f5f5f5;
241
+ font-weight: 600;
242
+ color: #333;
243
+ }
244
+
245
+ .data-table tr:hover {
246
+ background: #f9f9f9;
247
+ }
248
+
249
+ .section-title {
250
+ font-size: 1.2rem;
251
+ color: #333;
252
+ margin-bottom: 16px;
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 10px;
256
+ }
257
+
258
+ .section-title::before {
259
+ content: '';
260
+ width: 4px;
261
+ height: 24px;
262
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
263
+ border-radius: 2px;
264
+ }
265
+
266
+ .insights-grid {
267
+ display: grid;
268
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
269
+ gap: 16px;
270
+ }
271
+
272
+ .insight-card {
273
+ background: #f9f9f9;
274
+ border-radius: 12px;
275
+ padding: 16px;
276
+ }
277
+
278
+ .insight-card h4 {
279
+ color: #333;
280
+ margin-bottom: 12px;
281
+ font-size: 1rem;
282
+ }
283
+
284
+ .insight-list {
285
+ list-style: none;
286
+ }
287
+
288
+ .insight-list li {
289
+ padding: 6px 0;
290
+ color: #666;
291
+ font-size: 0.95rem;
292
+ }
293
+
294
+ .insight-list li::before {
295
+ content: '•';
296
+ color: #667eea;
297
+ font-weight: bold;
298
+ margin-right: 8px;
299
+ }
300
+
301
+ .error {
302
+ display: none;
303
+ background: #ffebee;
304
+ color: #c62828;
305
+ padding: 16px;
306
+ border-radius: 10px;
307
+ margin-top: 20px;
308
+ }
309
+
310
+ .error.active {
311
+ display: block;
312
+ }
313
+
314
+ @media (max-width: 768px) {
315
+ .header h1 {
316
+ font-size: 1.8rem;
317
+ }
318
+
319
+ .input-section {
320
+ flex-direction: column;
321
+ }
322
+
323
+ .input-group {
324
+ width: 100%;
325
+ }
326
+
327
+ .btn {
328
+ width: 100%;
329
+ }
330
+ }
331
+ </style>
332
+ </head>
333
+ <body>
334
+ <div class="container">
335
+ <div class="header">
336
+ <h1>OpenClaw Market Research</h1>
337
+ <p>AI-powered market analysis and forecasting</p>
338
+ </div>
339
+
340
+ <div class="card">
341
+ <div class="input-section">
342
+ <div class="input-group">
343
+ <label for="keyword">Market Keyword</label>
344
+ <input type="text" id="keyword" placeholder="e.g., AI Healthcare, Electric Vehicles, Cloud Computing" value="AI Healthcare">
345
+ </div>
346
+ <div class="input-group">
347
+ <label for="provider">AI Provider</label>
348
+ <select id="provider">
349
+ <option value="auto">Auto Select</option>
350
+ <option value="gemini">Gemini</option>
351
+ <option value="openai">OpenAI</option>
352
+ <option value="claude">Claude</option>
353
+ <option value="deepseek">DeepSeek</option>
354
+ <option value="openrouter">OpenRouter</option>
355
+ </select>
356
+ </div>
357
+ <button class="btn btn-primary" id="searchBtn" onclick="runResearch()">
358
+ Generate Report
359
+ </button>
360
+ </div>
361
+ </div>
362
+
363
+ <div class="loading" id="loading">
364
+ <div class="spinner"></div>
365
+ <p>Analyzing market data and generating forecast...</p>
366
+ </div>
367
+
368
+ <div class="error" id="error"></div>
369
+
370
+ <div class="results" id="results">
371
+ <div class="card">
372
+ <div class="results-header">
373
+ <h2 class="results-title" id="marketTitle">Market Research Report</h2>
374
+ <div class="meta-info">
375
+ <span class="meta-badge" id="providerBadge">Gemini</span>
376
+ <span class="meta-item" id="timestamp"></span>
377
+ </div>
378
+ </div>
379
+
380
+ <div class="summary-grid">
381
+ <div class="summary-card">
382
+ <div class="summary-label">2023 Market Size</div>
383
+ <div class="summary-value" id="past2023">$0B</div>
384
+ </div>
385
+ <div class="summary-card">
386
+ <div class="summary-label">2025 Market Size</div>
387
+ <div class="summary-value highlight" id="current2025">$0B</div>
388
+ </div>
389
+ <div class="summary-card">
390
+ <div class="summary-label">2033 Forecast</div>
391
+ <div class="summary-value" id="forecast2033">$0B</div>
392
+ </div>
393
+ <div class="summary-card">
394
+ <div class="summary-label">CAGR</div>
395
+ <div class="summary-value highlight" id="cagr">0%</div>
396
+ </div>
397
+ </div>
398
+
399
+ <div class="chart-section">
400
+ <h3 class="chart-title">Market Forecast (2023-2033)</h3>
401
+ <div class="chart-container">
402
+ <canvas id="forecastChart"></canvas>
403
+ </div>
404
+ </div>
405
+
406
+ <div class="chart-section">
407
+ <h3 class="chart-title">Regional Market Distribution</h3>
408
+ <div class="chart-container">
409
+ <canvas id="regionalChart"></canvas>
410
+ </div>
411
+ </div>
412
+
413
+ <h3 class="section-title">Regional Analysis</h3>
414
+ <table class="data-table" id="regionalTable">
415
+ <thead>
416
+ <tr>
417
+ <th>Region</th>
418
+ <th>Market Share</th>
419
+ <th>CAGR</th>
420
+ </tr>
421
+ </thead>
422
+ <tbody id="regionalBody"></tbody>
423
+ </table>
424
+
425
+ <h3 class="section-title" style="margin-top: 24px;">Competitive Landscape</h3>
426
+ <table class="data-table">
427
+ <thead>
428
+ <tr>
429
+ <th>Company</th>
430
+ <th>Market Share</th>
431
+ </tr>
432
+ </thead>
433
+ <tbody id="competitiveBody"></tbody>
434
+ </table>
435
+
436
+ <h3 class="section-title" style="margin-top: 24px;">Key Insights</h3>
437
+ <div class="insights-grid">
438
+ <div class="insight-card">
439
+ <h4>Opportunities</h4>
440
+ <ul class="insight-list" id="opportunitiesList"></ul>
441
+ </div>
442
+ <div class="insight-card">
443
+ <h4>Challenges</h4>
444
+ <ul class="insight-list" id="challengesList"></ul>
445
+ </div>
446
+ <div class="insight-card">
447
+ <h4>Market Drivers</h4>
448
+ <ul class="insight-list" id="driversList"></ul>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </div>
454
+
455
+ <script>
456
+ let forecastChart = null;
457
+ let regionalChart = null;
458
+
459
+ async function runResearch() {
460
+ const keyword = document.getElementById('keyword').value.trim();
461
+ const provider = document.getElementById('provider').value;
462
+
463
+ if (!keyword) {
464
+ showError('Please enter a market keyword');
465
+ return;
466
+ }
467
+
468
+ const loading = document.getElementById('loading');
469
+ const results = document.getElementById('results');
470
+ const error = document.getElementById('error');
471
+ const btn = document.getElementById('searchBtn');
472
+
473
+ loading.classList.add('active');
474
+ results.classList.remove('active');
475
+ error.classList.remove('active');
476
+ btn.disabled = true;
477
+
478
+ try {
479
+ const response = await fetch('/run', {
480
+ method: 'POST',
481
+ headers: {
482
+ 'Content-Type': 'application/json'
483
+ },
484
+ body: JSON.stringify({
485
+ task: 'market_research',
486
+ provider: provider === 'auto' ? undefined : provider,
487
+ keyword: keyword
488
+ })
489
+ });
490
+
491
+ const data = await response.json();
492
+
493
+ if (!response.ok || data.error) {
494
+ throw new Error(data.error || data.details || 'Request failed');
495
+ }
496
+
497
+ displayResults(data);
498
+ results.classList.add('active');
499
+ } catch (err) {
500
+ showError(err.message);
501
+ } finally {
502
+ loading.classList.remove('active');
503
+ btn.disabled = false;
504
+ }
505
+ }
506
+
507
+ function displayResults(data) {
508
+ const dashboard = data.dashboard_view;
509
+
510
+ // Header
511
+ document.getElementById('marketTitle').textContent = dashboard.marketTitle;
512
+ document.getElementById('providerBadge').textContent = data.meta.provider_used || 'Auto';
513
+ document.getElementById('timestamp').textContent = new Date(data.meta.timestamp).toLocaleString();
514
+
515
+ // Summary
516
+ document.getElementById('past2023').textContent = `$${dashboard.marketSummary.past2023}B`;
517
+ document.getElementById('current2025').textContent = `$${dashboard.marketSummary.current2025}B`;
518
+ document.getElementById('forecast2033').textContent = `$${dashboard.marketSummary.forecast2033}B`;
519
+ document.getElementById('cagr').textContent = `${dashboard.marketSummary.cagr}%`;
520
+
521
+ // Forecast Chart
522
+ updateForecastChart(dashboard.forecast);
523
+
524
+ // Regional Chart
525
+ updateRegionalChart(dashboard.regional);
526
+
527
+ // Regional Table
528
+ const regionalBody = document.getElementById('regionalBody');
529
+ regionalBody.innerHTML = dashboard.regional.map(r => `
530
+ <tr>
531
+ <td>${r.region}</td>
532
+ <td>${r.share}%</td>
533
+ <td>${r.cagr}%</td>
534
+ </tr>
535
+ `).join('');
536
+
537
+ // Competitive Table
538
+ const competitiveBody = document.getElementById('competitiveBody');
539
+ competitiveBody.innerHTML = dashboard.competitive.map(c => `
540
+ <tr>
541
+ <td>${c.company}</td>
542
+ <td>${c.share}%</td>
543
+ </tr>
544
+ `).join('');
545
+
546
+ // Insights
547
+ document.getElementById('opportunitiesList').innerHTML = dashboard.insights.keyOpportunities.map(o => `<li>${o}</li>`).join('');
548
+ document.getElementById('challengesList').innerHTML = dashboard.insights.majorChallenges.map(c => `<li>${c}</li>`).join('');
549
+ document.getElementById('driversList').innerHTML = dashboard.drivers.map(d => `<li>${d.driver} (Impact: ${d.impact})</li>`).join('');
550
+ }
551
+
552
+ function updateForecastChart(forecast) {
553
+ const ctx = document.getElementById('forecastChart').getContext('2d');
554
+
555
+ if (forecastChart) {
556
+ forecastChart.destroy();
557
+ }
558
+
559
+ forecastChart = new Chart(ctx, {
560
+ type: 'line',
561
+ data: {
562
+ labels: forecast.map(f => f.year),
563
+ datasets: [{
564
+ label: 'Market Size (Billion USD)',
565
+ data: forecast.map(f => f.value),
566
+ borderColor: '#667eea',
567
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
568
+ fill: true,
569
+ tension: 0.4,
570
+ pointBackgroundColor: '#667eea',
571
+ pointBorderColor: '#fff',
572
+ pointBorderWidth: 2,
573
+ pointRadius: 6
574
+ }]
575
+ },
576
+ options: {
577
+ responsive: true,
578
+ maintainAspectRatio: false,
579
+ plugins: {
580
+ legend: {
581
+ display: false
582
+ }
583
+ },
584
+ scales: {
585
+ y: {
586
+ beginAtZero: true,
587
+ grid: {
588
+ color: 'rgba(0,0,0,0.05)'
589
+ }
590
+ },
591
+ x: {
592
+ grid: {
593
+ display: false
594
+ }
595
+ }
596
+ }
597
+ }
598
+ });
599
+ }
600
+
601
+ function updateRegionalChart(regional) {
602
+ const ctx = document.getElementById('regionalChart').getContext('2d');
603
+
604
+ if (regionalChart) {
605
+ regionalChart.destroy();
606
+ }
607
+
608
+ regionalChart = new Chart(ctx, {
609
+ type: 'doughnut',
610
+ data: {
611
+ labels: regional.map(r => r.region),
612
+ datasets: [{
613
+ data: regional.map(r => r.share),
614
+ backgroundColor: [
615
+ '#667eea',
616
+ '#764ba2',
617
+ '#f093fb',
618
+ '#4facfe',
619
+ '#43e97b'
620
+ ],
621
+ borderWidth: 0
622
+ }]
623
+ },
624
+ options: {
625
+ responsive: true,
626
+ maintainAspectRatio: false,
627
+ plugins: {
628
+ legend: {
629
+ position: 'right'
630
+ }
631
+ }
632
+ }
633
+ });
634
+ }
635
+
636
+ function showError(message) {
637
+ const error = document.getElementById('error');
638
+ error.textContent = `Error: ${message}`;
639
+ error.classList.add('active');
640
+ }
641
+
642
+ // Run on Enter key
643
+ document.getElementById('keyword').addEventListener('keypress', (e) => {
644
+ if (e.key === 'Enter') runResearch();
645
+ });
646
+ </script>
647
+ </body>
648
+ </html>
server.js CHANGED
@@ -1,7 +1,15 @@
1
  import express from "express";
2
  import { spawn } from "child_process";
 
 
 
 
 
3
 
4
  const app = express();
 
 
 
5
  app.use(express.json());
6
 
7
  /* ===============================
 
1
  import express from "express";
2
  import { spawn } from "child_process";
3
+ import { fileURLToPath } from "url";
4
+ import { dirname, join } from "path";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
 
9
  const app = express();
10
+
11
+ // Serve static files from public directory
12
+ app.use(express.static(join(__dirname, "public")));
13
  app.use(express.json());
14
 
15
  /* ===============================