create-mcp-use-app 0.2.2 → 0.3.2
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 +444 -0
- package/dist/index.js +88 -11
- package/dist/templates/ui/README.md +9 -9
- package/dist/templates/ui/index.ts +12 -0
- package/dist/templates/ui/package.json +6 -5
- package/dist/templates/ui/resources/kanban-board.tsx +1 -1
- package/dist/templates/ui/src/server.ts +17 -103
- package/dist/templates/ui/tsconfig.json +1 -1
- package/package.json +3 -3
- package/dist/templates/ui/resources/data-visualization.tsx +0 -475
- package/dist/templates/ui/resources/todo-list.tsx +0 -408
|
@@ -27,126 +27,40 @@ server.tool({
|
|
|
27
27
|
uri: 'ui://widget/kanban-board',
|
|
28
28
|
content: {
|
|
29
29
|
type: 'externalUrl',
|
|
30
|
-
iframeUrl:
|
|
30
|
+
iframeUrl: `http://localhost:${PORT}/mcp-use/widgets/kanban-board`
|
|
31
31
|
},
|
|
32
32
|
encoding: 'text',
|
|
33
33
|
})
|
|
34
|
-
return
|
|
34
|
+
return {
|
|
35
|
+
content: [uiResource]
|
|
36
|
+
}
|
|
35
37
|
},
|
|
36
38
|
})
|
|
37
39
|
|
|
38
|
-
// MCP Resource for server status
|
|
39
|
-
server.resource({
|
|
40
|
-
uri: 'ui://status',
|
|
41
|
-
name: 'UI Server Status',
|
|
42
|
-
description: 'Status of the UI MCP server',
|
|
43
|
-
mimeType: 'application/json',
|
|
44
|
-
fn: async () => {
|
|
45
|
-
return JSON.stringify({
|
|
46
|
-
name: 'ui-mcp-server',
|
|
47
|
-
version: '1.0.0',
|
|
48
|
-
status: 'running',
|
|
49
|
-
transport: process.env.MCP_TRANSPORT || 'http',
|
|
50
|
-
uiEndpoint: `http://localhost:${PORT}/mcp-use/widgets`,
|
|
51
|
-
mcpEndpoint: process.env.MCP_TRANSPORT === 'stdio' ? 'stdio' : `http://localhost:${PORT}/mcp`,
|
|
52
|
-
availableWidgets: ['kanban-board', 'todo-list', 'data-visualization'],
|
|
53
|
-
timestamp: new Date().toISOString(),
|
|
54
|
-
}, null, 2)
|
|
55
|
-
},
|
|
56
|
-
})
|
|
57
40
|
|
|
58
41
|
// MCP Resource for Kanban Board widget
|
|
59
42
|
server.resource({
|
|
60
|
-
uri: 'ui://widget/kanban-board',
|
|
61
43
|
name: 'Kanban Board Widget',
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
fn: async () => {
|
|
65
|
-
const widgetUrl = `http://localhost:${PORT}/mcp-use/widgets/kanban-board`
|
|
66
|
-
return `
|
|
67
|
-
<div id="kanban-root"></div>
|
|
68
|
-
<script type="module" src="${widgetUrl}"></script>
|
|
69
|
-
`.trim()
|
|
70
|
-
},
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// MCP Resource for Todo List widget
|
|
74
|
-
server.resource({
|
|
75
|
-
uri: 'ui://widget/todo-list',
|
|
76
|
-
name: 'Todo List Widget',
|
|
77
|
-
description: 'Interactive todo list widget',
|
|
44
|
+
uri: 'ui://widget/kanban-board',
|
|
45
|
+
title: 'Kanban Board Widget',
|
|
78
46
|
mimeType: 'text/html+skybridge',
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<script type="module" src="${widgetUrl}"></script>
|
|
84
|
-
`.trim()
|
|
47
|
+
description: 'Interactive Kanban board widget',
|
|
48
|
+
annotations: {
|
|
49
|
+
audience: ['user', 'assistant'],
|
|
50
|
+
priority: 0.7
|
|
85
51
|
},
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
// MCP Resource for Data Visualization widget
|
|
89
|
-
server.resource({
|
|
90
|
-
uri: 'ui://widget/data-visualization',
|
|
91
|
-
name: 'Data Visualization Widget',
|
|
92
|
-
description: 'Interactive data visualization widget',
|
|
93
|
-
mimeType: 'text/html+skybridge',
|
|
94
52
|
fn: async () => {
|
|
95
|
-
const widgetUrl = `http://localhost:${PORT}/mcp-use/widgets/
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Tool for showing Kanban Board
|
|
104
|
-
server.tool({
|
|
105
|
-
name: 'show-kanban',
|
|
106
|
-
description: 'Display an interactive Kanban board',
|
|
107
|
-
inputs: [
|
|
108
|
-
{
|
|
109
|
-
name: 'tasks',
|
|
110
|
-
type: 'string',
|
|
111
|
-
description: 'JSON string of tasks to display',
|
|
112
|
-
required: true,
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
fn: async (params: Record<string, any>) => {
|
|
116
|
-
const { tasks } = params
|
|
117
|
-
try {
|
|
118
|
-
const taskData = JSON.parse(tasks)
|
|
119
|
-
return `Displayed Kanban board with ${taskData.length || 0} tasks at http://localhost:${PORT}/mcp-use/widgets/kanban-board`
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
return `Error parsing tasks: ${error instanceof Error ? error.message : 'Invalid JSON'}`
|
|
53
|
+
const widgetUrl = `http://localhost:${PORT}/mcp-use/widgets/kanban-board`
|
|
54
|
+
return {
|
|
55
|
+
contents: [{
|
|
56
|
+
uri: 'ui://widget/kanban-board',
|
|
57
|
+
mimeType: 'text/uri-list',
|
|
58
|
+
text: widgetUrl
|
|
59
|
+
}]
|
|
123
60
|
}
|
|
124
61
|
},
|
|
125
62
|
})
|
|
126
63
|
|
|
127
|
-
// Tool for showing Todo List
|
|
128
|
-
server.tool({
|
|
129
|
-
name: 'show-todo-list',
|
|
130
|
-
description: 'Display an interactive todo list',
|
|
131
|
-
inputs: [
|
|
132
|
-
{
|
|
133
|
-
name: 'todos',
|
|
134
|
-
type: 'string',
|
|
135
|
-
description: 'JSON string of todos to display',
|
|
136
|
-
required: true,
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
fn: async (params: Record<string, any>) => {
|
|
140
|
-
const { todos } = params
|
|
141
|
-
try {
|
|
142
|
-
const todoData = JSON.parse(todos)
|
|
143
|
-
return `Displayed Todo list with ${todoData.length || 0} items at http://localhost:${PORT}/mcp-use/widgets/todo-list`
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
return `Error parsing todos: ${error instanceof Error ? error.message : 'Invalid JSON'}`
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
})
|
|
150
64
|
|
|
151
65
|
|
|
152
66
|
// Start the server (MCP endpoints auto-mounted at /mcp)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-mcp-use-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create MCP-Use apps with one command",
|
|
6
6
|
"author": "mcp-use, Inc.",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "npm run clean && tsup src/index.ts --format esm && tsc --emitDeclarationOnly --declaration && npm run copy-templates",
|
|
55
55
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
56
|
-
"copy-templates": "
|
|
56
|
+
"copy-templates": "node scripts/copy-templates.js",
|
|
57
57
|
"dev": "tsc --build --watch",
|
|
58
|
-
"test": "vitest",
|
|
58
|
+
"test": "vitest --run --passWithNoTests",
|
|
59
59
|
"lint": "eslint ."
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react'
|
|
2
|
-
import { createRoot } from 'react-dom/client'
|
|
3
|
-
|
|
4
|
-
interface DataPoint {
|
|
5
|
-
label: string
|
|
6
|
-
value: number
|
|
7
|
-
color?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface DataVisualizationProps {
|
|
11
|
-
initialData?: DataPoint[]
|
|
12
|
-
chartType?: 'bar' | 'line' | 'pie'
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DataVisualization: React.FC<DataVisualizationProps> = ({
|
|
16
|
-
initialData = [],
|
|
17
|
-
chartType = 'bar',
|
|
18
|
-
}) => {
|
|
19
|
-
const [data, setData] = useState<DataPoint[]>(initialData)
|
|
20
|
-
const [currentChartType, setCurrentChartType] = useState<'bar' | 'line' | 'pie'>(chartType)
|
|
21
|
-
const [newDataPoint, setNewDataPoint] = useState({ label: '', value: 0 })
|
|
22
|
-
|
|
23
|
-
// Load data from URL parameters or use defaults
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
const urlParams = new URLSearchParams(window.location.search)
|
|
26
|
-
const dataParam = urlParams.get('data')
|
|
27
|
-
const typeParam = urlParams.get('chartType')
|
|
28
|
-
|
|
29
|
-
if (dataParam) {
|
|
30
|
-
try {
|
|
31
|
-
const parsedData = JSON.parse(decodeURIComponent(dataParam))
|
|
32
|
-
setData(parsedData)
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
console.error('Error parsing data from URL:', error)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
// Default data for demo
|
|
40
|
-
setData([
|
|
41
|
-
{ label: 'January', value: 65, color: '#3498db' },
|
|
42
|
-
{ label: 'February', value: 59, color: '#e74c3c' },
|
|
43
|
-
{ label: 'March', value: 80, color: '#2ecc71' },
|
|
44
|
-
{ label: 'April', value: 81, color: '#f39c12' },
|
|
45
|
-
{ label: 'May', value: 56, color: '#9b59b6' },
|
|
46
|
-
{ label: 'June', value: 55, color: '#1abc9c' },
|
|
47
|
-
])
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (typeParam) {
|
|
51
|
-
setCurrentChartType(typeParam as 'bar' | 'line' | 'pie')
|
|
52
|
-
}
|
|
53
|
-
}, [])
|
|
54
|
-
|
|
55
|
-
const addDataPoint = () => {
|
|
56
|
-
if (newDataPoint.label.trim() && newDataPoint.value > 0) {
|
|
57
|
-
const colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#34495e', '#e67e22']
|
|
58
|
-
const newPoint: DataPoint = {
|
|
59
|
-
...newDataPoint,
|
|
60
|
-
color: colors[data.length % colors.length],
|
|
61
|
-
}
|
|
62
|
-
setData([...data, newPoint])
|
|
63
|
-
setNewDataPoint({ label: '', value: 0 })
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const removeDataPoint = (index: number) => {
|
|
68
|
-
setData(data.filter((_, i) => i !== index))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const getMaxValue = () => {
|
|
72
|
-
return Math.max(...data.map(d => d.value), 0)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const getTotalValue = () => {
|
|
76
|
-
return data.reduce((sum, d) => sum + d.value, 0)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const renderBarChart = () => {
|
|
80
|
-
const maxValue = getMaxValue()
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div style={{ padding: '20px' }}>
|
|
84
|
-
<h3 style={{ marginBottom: '20px', color: '#2c3e50' }}>Bar Chart</h3>
|
|
85
|
-
<div style={{ display: 'flex', alignItems: 'end', gap: '10px', height: '300px' }}>
|
|
86
|
-
{data.map((point, index) => (
|
|
87
|
-
<div key={index} style={{ flex: '1', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
88
|
-
<div
|
|
89
|
-
style={{
|
|
90
|
-
background: point.color || '#3498db',
|
|
91
|
-
height: `${(point.value / maxValue) * 250}px`,
|
|
92
|
-
width: '100%',
|
|
93
|
-
borderRadius: '4px 4px 0 0',
|
|
94
|
-
transition: 'all 0.3s ease',
|
|
95
|
-
cursor: 'pointer',
|
|
96
|
-
position: 'relative',
|
|
97
|
-
}}
|
|
98
|
-
title={`${point.label}: ${point.value}`}
|
|
99
|
-
>
|
|
100
|
-
<div style={{
|
|
101
|
-
position: 'absolute',
|
|
102
|
-
top: '-25px',
|
|
103
|
-
left: '50%',
|
|
104
|
-
transform: 'translateX(-50%)',
|
|
105
|
-
background: 'rgba(0,0,0,0.8)',
|
|
106
|
-
color: 'white',
|
|
107
|
-
padding: '2px 6px',
|
|
108
|
-
borderRadius: '4px',
|
|
109
|
-
fontSize: '12px',
|
|
110
|
-
whiteSpace: 'nowrap',
|
|
111
|
-
}}
|
|
112
|
-
>
|
|
113
|
-
{point.value}
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
<div style={{
|
|
117
|
-
marginTop: '10px',
|
|
118
|
-
fontSize: '12px',
|
|
119
|
-
textAlign: 'center',
|
|
120
|
-
color: '#7f8c8d',
|
|
121
|
-
wordBreak: 'break-word',
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
{point.label}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
))}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const renderLineChart = () => {
|
|
134
|
-
const maxValue = getMaxValue()
|
|
135
|
-
const width = 600
|
|
136
|
-
const height = 300
|
|
137
|
-
const padding = 40
|
|
138
|
-
|
|
139
|
-
const points = data.map((point, index) => ({
|
|
140
|
-
x: padding + (index * (width - 2 * padding)) / (data.length - 1),
|
|
141
|
-
y: padding + ((maxValue - point.value) / maxValue) * (height - 2 * padding),
|
|
142
|
-
}))
|
|
143
|
-
|
|
144
|
-
const pathData = points.map((point, index) =>
|
|
145
|
-
`${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`,
|
|
146
|
-
).join(' ')
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div style={{ padding: '20px' }}>
|
|
150
|
-
<h3 style={{ marginBottom: '20px', color: '#2c3e50' }}>Line Chart</h3>
|
|
151
|
-
<svg width={width} height={height} style={{ border: '1px solid #ecf0f1', borderRadius: '8px' }}>
|
|
152
|
-
{/* Grid lines */}
|
|
153
|
-
{[0, 0.25, 0.5, 0.75, 1].map(ratio => (
|
|
154
|
-
<line
|
|
155
|
-
key={ratio}
|
|
156
|
-
x1={padding}
|
|
157
|
-
y1={padding + ratio * (height - 2 * padding)}
|
|
158
|
-
x2={width - padding}
|
|
159
|
-
y2={padding + ratio * (height - 2 * padding)}
|
|
160
|
-
stroke="#ecf0f1"
|
|
161
|
-
strokeWidth="1"
|
|
162
|
-
/>
|
|
163
|
-
))}
|
|
164
|
-
|
|
165
|
-
{/* Line */}
|
|
166
|
-
<path
|
|
167
|
-
d={pathData}
|
|
168
|
-
fill="none"
|
|
169
|
-
stroke="#3498db"
|
|
170
|
-
strokeWidth="3"
|
|
171
|
-
/>
|
|
172
|
-
|
|
173
|
-
{/* Data points */}
|
|
174
|
-
{points.map((point, index) => (
|
|
175
|
-
<circle
|
|
176
|
-
key={index}
|
|
177
|
-
cx={point.x}
|
|
178
|
-
cy={point.y}
|
|
179
|
-
r="6"
|
|
180
|
-
fill={data[index].color || '#3498db'}
|
|
181
|
-
stroke="white"
|
|
182
|
-
strokeWidth="2"
|
|
183
|
-
style={{ cursor: 'pointer' }}
|
|
184
|
-
title={`${data[index].label}: ${data[index].value}`}
|
|
185
|
-
/>
|
|
186
|
-
))}
|
|
187
|
-
|
|
188
|
-
{/* Labels */}
|
|
189
|
-
{data.map((point, index) => (
|
|
190
|
-
<text
|
|
191
|
-
key={index}
|
|
192
|
-
x={padding + (index * (width - 2 * padding)) / (data.length - 1)}
|
|
193
|
-
y={height - 10}
|
|
194
|
-
textAnchor="middle"
|
|
195
|
-
fontSize="12"
|
|
196
|
-
fill="#7f8c8d"
|
|
197
|
-
>
|
|
198
|
-
{point.label}
|
|
199
|
-
</text>
|
|
200
|
-
))}
|
|
201
|
-
</svg>
|
|
202
|
-
</div>
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const renderPieChart = () => {
|
|
207
|
-
const total = getTotalValue()
|
|
208
|
-
let currentAngle = 0
|
|
209
|
-
|
|
210
|
-
return (
|
|
211
|
-
<div style={{ padding: '20px' }}>
|
|
212
|
-
<h3 style={{ marginBottom: '20px', color: '#2c3e50' }}>Pie Chart</h3>
|
|
213
|
-
<div style={{ display: 'flex', gap: '40px', alignItems: 'center' }}>
|
|
214
|
-
<svg width="300" height="300" style={{ border: '1px solid #ecf0f1', borderRadius: '8px' }}>
|
|
215
|
-
{data.map((point, index) => {
|
|
216
|
-
const percentage = point.value / total
|
|
217
|
-
const angle = percentage * 360
|
|
218
|
-
const startAngle = currentAngle
|
|
219
|
-
const endAngle = currentAngle + angle
|
|
220
|
-
currentAngle += angle
|
|
221
|
-
|
|
222
|
-
const centerX = 150
|
|
223
|
-
const centerY = 150
|
|
224
|
-
const radius = 120
|
|
225
|
-
|
|
226
|
-
const startAngleRad = (startAngle - 90) * (Math.PI / 180)
|
|
227
|
-
const endAngleRad = (endAngle - 90) * (Math.PI / 180)
|
|
228
|
-
|
|
229
|
-
const x1 = centerX + radius * Math.cos(startAngleRad)
|
|
230
|
-
const y1 = centerY + radius * Math.sin(startAngleRad)
|
|
231
|
-
const x2 = centerX + radius * Math.cos(endAngleRad)
|
|
232
|
-
const y2 = centerY + radius * Math.sin(endAngleRad)
|
|
233
|
-
|
|
234
|
-
const largeArcFlag = angle > 180 ? 1 : 0
|
|
235
|
-
|
|
236
|
-
const pathData = [
|
|
237
|
-
`M ${centerX} ${centerY}`,
|
|
238
|
-
`L ${x1} ${y1}`,
|
|
239
|
-
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
|
|
240
|
-
'Z',
|
|
241
|
-
].join(' ')
|
|
242
|
-
|
|
243
|
-
return (
|
|
244
|
-
<path
|
|
245
|
-
key={index}
|
|
246
|
-
d={pathData}
|
|
247
|
-
fill={point.color || '#3498db'}
|
|
248
|
-
stroke="white"
|
|
249
|
-
strokeWidth="2"
|
|
250
|
-
style={{ cursor: 'pointer' }}
|
|
251
|
-
title={`${point.label}: ${point.value} (${(percentage * 100).toFixed(1)}%)`}
|
|
252
|
-
/>
|
|
253
|
-
)
|
|
254
|
-
})}
|
|
255
|
-
</svg>
|
|
256
|
-
|
|
257
|
-
<div style={{ flex: '1' }}>
|
|
258
|
-
<h4 style={{ marginBottom: '15px', color: '#2c3e50' }}>Legend</h4>
|
|
259
|
-
{data.map((point, index) => (
|
|
260
|
-
<div
|
|
261
|
-
key={index}
|
|
262
|
-
style={{
|
|
263
|
-
display: 'flex',
|
|
264
|
-
alignItems: 'center',
|
|
265
|
-
marginBottom: '8px',
|
|
266
|
-
padding: '8px',
|
|
267
|
-
background: '#f8f9fa',
|
|
268
|
-
borderRadius: '4px',
|
|
269
|
-
}}
|
|
270
|
-
>
|
|
271
|
-
<div style={{
|
|
272
|
-
width: '16px',
|
|
273
|
-
height: '16px',
|
|
274
|
-
background: point.color || '#3498db',
|
|
275
|
-
borderRadius: '2px',
|
|
276
|
-
marginRight: '10px',
|
|
277
|
-
}}
|
|
278
|
-
/>
|
|
279
|
-
<span style={{ flex: '1', fontSize: '14px' }}>{point.label}</span>
|
|
280
|
-
<span style={{
|
|
281
|
-
fontSize: '14px',
|
|
282
|
-
fontWeight: 'bold',
|
|
283
|
-
color: '#2c3e50',
|
|
284
|
-
}}
|
|
285
|
-
>
|
|
286
|
-
{point.value}
|
|
287
|
-
</span>
|
|
288
|
-
</div>
|
|
289
|
-
))}
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return (
|
|
297
|
-
<div style={{ padding: '20px' }}>
|
|
298
|
-
<div style={{ marginBottom: '30px' }}>
|
|
299
|
-
<h1 style={{ margin: '0 0 20px 0', color: '#2c3e50' }}>Data Visualization</h1>
|
|
300
|
-
|
|
301
|
-
{/* Controls */}
|
|
302
|
-
<div style={{
|
|
303
|
-
background: 'white',
|
|
304
|
-
padding: '20px',
|
|
305
|
-
borderRadius: '8px',
|
|
306
|
-
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
307
|
-
marginBottom: '20px',
|
|
308
|
-
}}
|
|
309
|
-
>
|
|
310
|
-
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', alignItems: 'center', marginBottom: '20px' }}>
|
|
311
|
-
<div>
|
|
312
|
-
<label style={{ marginRight: '10px', fontWeight: 'bold', color: '#2c3e50' }}>Chart Type:</label>
|
|
313
|
-
<select
|
|
314
|
-
value={currentChartType}
|
|
315
|
-
onChange={e => setCurrentChartType(e.target.value as typeof currentChartType)}
|
|
316
|
-
style={{
|
|
317
|
-
padding: '8px 12px',
|
|
318
|
-
border: '1px solid #ddd',
|
|
319
|
-
borderRadius: '4px',
|
|
320
|
-
}}
|
|
321
|
-
>
|
|
322
|
-
<option value="bar">Bar Chart</option>
|
|
323
|
-
<option value="line">Line Chart</option>
|
|
324
|
-
<option value="pie">Pie Chart</option>
|
|
325
|
-
</select>
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
|
|
329
|
-
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
330
|
-
<input
|
|
331
|
-
type="text"
|
|
332
|
-
placeholder="Label"
|
|
333
|
-
value={newDataPoint.label}
|
|
334
|
-
onChange={e => setNewDataPoint({ ...newDataPoint, label: e.target.value })}
|
|
335
|
-
style={{
|
|
336
|
-
padding: '8px 12px',
|
|
337
|
-
border: '1px solid #ddd',
|
|
338
|
-
borderRadius: '4px',
|
|
339
|
-
minWidth: '120px',
|
|
340
|
-
}}
|
|
341
|
-
/>
|
|
342
|
-
<input
|
|
343
|
-
type="number"
|
|
344
|
-
placeholder="Value"
|
|
345
|
-
value={newDataPoint.value}
|
|
346
|
-
onChange={e => setNewDataPoint({ ...newDataPoint, value: Number(e.target.value) })}
|
|
347
|
-
style={{
|
|
348
|
-
padding: '8px 12px',
|
|
349
|
-
border: '1px solid #ddd',
|
|
350
|
-
borderRadius: '4px',
|
|
351
|
-
minWidth: '100px',
|
|
352
|
-
}}
|
|
353
|
-
/>
|
|
354
|
-
<button
|
|
355
|
-
onClick={addDataPoint}
|
|
356
|
-
style={{
|
|
357
|
-
padding: '8px 16px',
|
|
358
|
-
background: '#3498db',
|
|
359
|
-
color: 'white',
|
|
360
|
-
border: 'none',
|
|
361
|
-
borderRadius: '4px',
|
|
362
|
-
cursor: 'pointer',
|
|
363
|
-
}}
|
|
364
|
-
>
|
|
365
|
-
Add Data Point
|
|
366
|
-
</button>
|
|
367
|
-
</div>
|
|
368
|
-
</div>
|
|
369
|
-
|
|
370
|
-
{/* Chart */}
|
|
371
|
-
<div style={{
|
|
372
|
-
background: 'white',
|
|
373
|
-
borderRadius: '8px',
|
|
374
|
-
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
375
|
-
overflow: 'hidden',
|
|
376
|
-
}}
|
|
377
|
-
>
|
|
378
|
-
{data.length === 0
|
|
379
|
-
? (
|
|
380
|
-
<div style={{
|
|
381
|
-
textAlign: 'center',
|
|
382
|
-
padding: '40px 20px',
|
|
383
|
-
color: '#7f8c8d',
|
|
384
|
-
fontStyle: 'italic',
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
No data to visualize. Add some data points above!
|
|
388
|
-
</div>
|
|
389
|
-
)
|
|
390
|
-
: (
|
|
391
|
-
<>
|
|
392
|
-
{currentChartType === 'bar' && renderBarChart()}
|
|
393
|
-
{currentChartType === 'line' && renderLineChart()}
|
|
394
|
-
{currentChartType === 'pie' && renderPieChart()}
|
|
395
|
-
</>
|
|
396
|
-
)}
|
|
397
|
-
</div>
|
|
398
|
-
|
|
399
|
-
{/* Data table */}
|
|
400
|
-
{data.length > 0 && (
|
|
401
|
-
<div style={{
|
|
402
|
-
background: 'white',
|
|
403
|
-
borderRadius: '8px',
|
|
404
|
-
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
405
|
-
marginTop: '20px',
|
|
406
|
-
overflow: 'hidden',
|
|
407
|
-
}}
|
|
408
|
-
>
|
|
409
|
-
<div style={{ padding: '20px', borderBottom: '1px solid #ecf0f1' }}>
|
|
410
|
-
<h3 style={{ margin: '0', color: '#2c3e50' }}>Data Table</h3>
|
|
411
|
-
</div>
|
|
412
|
-
<div style={{ overflowX: 'auto' }}>
|
|
413
|
-
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
414
|
-
<thead>
|
|
415
|
-
<tr style={{ background: '#f8f9fa' }}>
|
|
416
|
-
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ecf0f1' }}>Label</th>
|
|
417
|
-
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ecf0f1' }}>Value</th>
|
|
418
|
-
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ecf0f1' }}>Percentage</th>
|
|
419
|
-
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ecf0f1' }}>Color</th>
|
|
420
|
-
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ecf0f1' }}>Actions</th>
|
|
421
|
-
</tr>
|
|
422
|
-
</thead>
|
|
423
|
-
<tbody>
|
|
424
|
-
{data.map((point, index) => (
|
|
425
|
-
<tr key={index} style={{ borderBottom: '1px solid #ecf0f1' }}>
|
|
426
|
-
<td style={{ padding: '12px' }}>{point.label}</td>
|
|
427
|
-
<td style={{ padding: '12px', fontWeight: 'bold' }}>{point.value}</td>
|
|
428
|
-
<td style={{ padding: '12px' }}>
|
|
429
|
-
{((point.value / getTotalValue()) * 100).toFixed(1)}
|
|
430
|
-
%
|
|
431
|
-
</td>
|
|
432
|
-
<td style={{ padding: '12px' }}>
|
|
433
|
-
<div style={{
|
|
434
|
-
width: '20px',
|
|
435
|
-
height: '20px',
|
|
436
|
-
background: point.color || '#3498db',
|
|
437
|
-
borderRadius: '2px',
|
|
438
|
-
display: 'inline-block',
|
|
439
|
-
}}
|
|
440
|
-
/>
|
|
441
|
-
</td>
|
|
442
|
-
<td style={{ padding: '12px' }}>
|
|
443
|
-
<button
|
|
444
|
-
onClick={() => removeDataPoint(index)}
|
|
445
|
-
style={{
|
|
446
|
-
background: '#e74c3c',
|
|
447
|
-
color: 'white',
|
|
448
|
-
border: 'none',
|
|
449
|
-
borderRadius: '4px',
|
|
450
|
-
padding: '4px 8px',
|
|
451
|
-
cursor: 'pointer',
|
|
452
|
-
fontSize: '12px',
|
|
453
|
-
}}
|
|
454
|
-
>
|
|
455
|
-
Remove
|
|
456
|
-
</button>
|
|
457
|
-
</td>
|
|
458
|
-
</tr>
|
|
459
|
-
))}
|
|
460
|
-
</tbody>
|
|
461
|
-
</table>
|
|
462
|
-
</div>
|
|
463
|
-
</div>
|
|
464
|
-
)}
|
|
465
|
-
</div>
|
|
466
|
-
</div>
|
|
467
|
-
)
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Mount the component
|
|
471
|
-
const container = document.getElementById('widget-root')
|
|
472
|
-
if (container) {
|
|
473
|
-
const root = createRoot(container)
|
|
474
|
-
root.render(<DataVisualization />)
|
|
475
|
-
}
|