plugin-build-ui-template 1.0.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.
@@ -0,0 +1,450 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useAPIClient } from '@nocobase/client';
3
+ import {
4
+ Card,
5
+ Space,
6
+ Button,
7
+ Modal,
8
+ Form,
9
+ Input,
10
+ Select,
11
+ Radio,
12
+ Tag,
13
+ Typography,
14
+ List,
15
+ Progress,
16
+ Spin,
17
+ Alert,
18
+ message,
19
+ } from 'antd';
20
+ import {
21
+ PlayCircleOutlined,
22
+ DeleteOutlined,
23
+ PlusOutlined,
24
+ LayoutOutlined,
25
+ CheckCircleOutlined,
26
+ SyncOutlined,
27
+ ExclamationCircleOutlined,
28
+ ArrowRightOutlined,
29
+ } from '@ant-design/icons';
30
+
31
+ const { Title, Paragraph, Text } = Typography;
32
+
33
+ export const BuildUITemplateManager: React.FC = () => {
34
+ const api = useAPIClient();
35
+ const [spaces, setSpaces] = useState<any[]>([]);
36
+ const [collections, setCollections] = useState<any[]>([]);
37
+ const [services, setServices] = useState<any[]>([]);
38
+ const [models, setModels] = useState<any[]>([]);
39
+ const [loading, setLoading] = useState(false);
40
+ const [modalVisible, setModalVisible] = useState(false);
41
+ const [editingSpace, setEditingSpace] = useState<any | null>(null);
42
+
43
+ const [form] = Form.useForm();
44
+ const selectedService = Form.useWatch('llmService', form);
45
+
46
+ // 1. Fetch Spaces
47
+ const fetchSpaces = async () => {
48
+ setLoading(true);
49
+ try {
50
+ const res = await api.resource('aiBuildUiTemplateSpaces').list({
51
+ sort: ['-createdAt'],
52
+ });
53
+ setSpaces(res?.data?.data || []);
54
+ } catch (err) {
55
+ console.error('Failed to load spaces:', err);
56
+ message.error('Failed to load UI generation spaces');
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+
62
+ // 2. Fetch Collections
63
+ const fetchCollections = async () => {
64
+ try {
65
+ const res = await api.resource('collections').list();
66
+ setCollections(res?.data?.data || []);
67
+ } catch (err) {
68
+ console.error('Failed to load collections:', err);
69
+ }
70
+ };
71
+
72
+ // 3. Fetch LLM Services
73
+ const fetchServices = async () => {
74
+ try {
75
+ const res = await api.resource('ai').listLLMServices();
76
+ setServices(res?.data?.data || []);
77
+ } catch (err) {
78
+ console.error('Failed to load LLM services:', err);
79
+ }
80
+ };
81
+
82
+ // 4. Fetch Models
83
+ useEffect(() => {
84
+ if (!selectedService) {
85
+ setModels([]);
86
+ return;
87
+ }
88
+ api
89
+ .resource('ai')
90
+ .listModels({ llmService: selectedService })
91
+ .then((res) => {
92
+ setModels(res?.data?.data || []);
93
+ })
94
+ .catch((err) => {
95
+ console.error('Failed to load models:', err);
96
+ });
97
+ }, [selectedService, api]);
98
+
99
+ useEffect(() => {
100
+ fetchSpaces();
101
+ fetchCollections();
102
+ fetchServices();
103
+
104
+ // Auto refresh active builds every 3 seconds
105
+ const interval = setInterval(() => {
106
+ const hasActiveBuild = spaces.some((s) => s.status === 'building');
107
+ if (hasActiveBuild) {
108
+ api
109
+ .resource('aiBuildUiTemplateSpaces')
110
+ .list({ sort: ['-createdAt'] })
111
+ .then((res) => setSpaces(res?.data?.data || []))
112
+ .catch(() => undefined);
113
+ }
114
+ }, 3000);
115
+
116
+ return () => clearInterval(interval);
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ }, [spaces]);
119
+
120
+ // 5. Open modal for create/edit
121
+ const openModal = (space?: any) => {
122
+ if (space) {
123
+ setEditingSpace(space);
124
+ form.setFieldsValue({
125
+ title: space.title,
126
+ llmService: space.llmService,
127
+ model: space.model,
128
+ systemPrompt: space.systemPrompt,
129
+ promptRequirements: space.promptRequirements,
130
+ type: space.type,
131
+ targetCollection: space.targetCollection,
132
+ });
133
+ } else {
134
+ setEditingSpace(null);
135
+ form.resetFields();
136
+ form.setFieldsValue({
137
+ type: 'block',
138
+ });
139
+ }
140
+ setModalVisible(true);
141
+ };
142
+
143
+ // 6. Save Space
144
+ const handleSave = async () => {
145
+ try {
146
+ const values = await form.validateFields();
147
+ if (editingSpace) {
148
+ await api.resource('aiBuildUiTemplateSpaces').update({
149
+ filterByTk: editingSpace.id,
150
+ values,
151
+ });
152
+ message.success('Space updated successfully');
153
+ } else {
154
+ await api.resource('aiBuildUiTemplateSpaces').create({
155
+ values,
156
+ });
157
+ message.success('Space created successfully');
158
+ }
159
+ setModalVisible(false);
160
+ fetchSpaces();
161
+ } catch (err: any) {
162
+ if (err?.name !== 'ValidateError') {
163
+ message.error(err?.message || 'Failed to save space settings');
164
+ }
165
+ }
166
+ };
167
+
168
+ // 7. Delete Space
169
+ const handleDelete = async (id: string) => {
170
+ Modal.confirm({
171
+ title: 'Are you sure to delete this generation space?',
172
+ icon: <ExclamationCircleOutlined />,
173
+ okType: 'danger',
174
+ onOk: async () => {
175
+ try {
176
+ await api.resource('aiBuildUiTemplateSpaces').destroy({
177
+ filterByTk: id,
178
+ });
179
+ message.success('Space deleted');
180
+ fetchSpaces();
181
+ } catch (err) {
182
+ message.error('Failed to delete space');
183
+ }
184
+ },
185
+ });
186
+ };
187
+
188
+ // 8. Trigger AI build
189
+ const handleBuild = async (id: string) => {
190
+ try {
191
+ message.loading('Triggering AI generation...', 1);
192
+ await api.resource('aiBuildUiTemplateSpaces').build({
193
+ filterByTk: id,
194
+ });
195
+ message.success('UI Template generation task started successfully!');
196
+ fetchSpaces();
197
+ } catch (err: any) {
198
+ message.error(err?.message || 'Failed to trigger build');
199
+ }
200
+ };
201
+
202
+ // Render Helpers
203
+ const renderStatusTag = (status: string, phase: string) => {
204
+ if (status === 'completed') {
205
+ return (
206
+ <Tag color="success" icon={<CheckCircleOutlined />}>
207
+ Completed
208
+ </Tag>
209
+ );
210
+ }
211
+ if (status === 'error') {
212
+ return (
213
+ <Tag color="error" icon={<ExclamationCircleOutlined />}>
214
+ Failed
215
+ </Tag>
216
+ );
217
+ }
218
+ if (status === 'building') {
219
+ return (
220
+ <Tag color="processing" icon={<SyncOutlined spin />}>
221
+ Generating ({phase || 'queued'})
222
+ </Tag>
223
+ );
224
+ }
225
+ return <Tag color="default">Draft</Tag>;
226
+ };
227
+
228
+ const getPhaseProgress = (phase: string) => {
229
+ switch (phase) {
230
+ case 'queued':
231
+ return 10;
232
+ case 'preparing':
233
+ return 25;
234
+ case 'generating':
235
+ return 60;
236
+ case 'saving':
237
+ return 90;
238
+ case 'completed':
239
+ return 100;
240
+ default:
241
+ return 0;
242
+ }
243
+ };
244
+
245
+ return (
246
+ <div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
247
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
248
+ <div>
249
+ <Title level={2}>AI UI Template Builder</Title>
250
+ <Paragraph type="secondary">
251
+ Generate stunning custom UI Blocks and Popups in seconds using state-of-the-art LLMs, then reuse them in
252
+ NocoBase v2 dynamic forms, dashboards and listings.
253
+ </Paragraph>
254
+ </div>
255
+ <Button type="primary" icon={<PlusOutlined />} onClick={() => openModal()}>
256
+ New Generation Space
257
+ </Button>
258
+ </div>
259
+
260
+ <List
261
+ loading={loading && spaces.length === 0}
262
+ dataSource={spaces}
263
+ renderItem={(space: any) => (
264
+ <Card
265
+ key={space.id}
266
+ style={{ marginBottom: '20px', borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.05)' }}
267
+ actions={[
268
+ <Button
269
+ key="generate"
270
+ type="link"
271
+ icon={<PlayCircleOutlined />}
272
+ onClick={() => handleBuild(space.id)}
273
+ disabled={space.status === 'building'}
274
+ >
275
+ Generate
276
+ </Button>,
277
+ <Button key="edit" type="link" onClick={() => openModal(space)}>
278
+ Edit Settings
279
+ </Button>,
280
+ <Button key="delete" type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(space.id)}>
281
+ Delete
282
+ </Button>,
283
+ ]}
284
+ >
285
+ <div
286
+ style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '16px' }}
287
+ >
288
+ <div>
289
+ <Space align="baseline">
290
+ <Title level={4} style={{ margin: 0 }}>
291
+ {space.title}
292
+ </Title>
293
+ {renderStatusTag(space.status, space.buildPhase)}
294
+ </Space>
295
+ <div style={{ marginTop: '8px' }}>
296
+ <Tag color="blue">{space.type === 'popup' ? 'Popup Template' : 'Block Template'}</Tag>
297
+ {space.targetCollection && <Tag color="purple">Collection: {space.targetCollection}</Tag>}
298
+ <Tag color="cyan">
299
+ LLM: {space.llmService} ({space.model})
300
+ </Tag>
301
+ </div>
302
+ </div>
303
+
304
+ {space.templateUid && (
305
+ <Button type="primary" ghost icon={<ArrowRightOutlined />} href="/admin/settings/ui-templates.block">
306
+ View Template Library
307
+ </Button>
308
+ )}
309
+ </div>
310
+
311
+ <Paragraph
312
+ style={{ background: '#f5f5f5', padding: '12px', borderRadius: '8px', borderLeft: '4px solid #1890ff' }}
313
+ >
314
+ <Text strong>User Requirements: </Text>
315
+ {space.promptRequirements || 'No specific requirements typed.'}
316
+ </Paragraph>
317
+
318
+ {space.status === 'building' && (
319
+ <div style={{ marginTop: '16px', background: '#fafafa', padding: '16px', borderRadius: '8px' }}>
320
+ <Text type="secondary">Build progress: </Text>
321
+ <Progress percent={getPhaseProgress(space.buildPhase)} status="active" strokeColor="#1890ff" />
322
+ <div style={{ marginTop: '8px', fontFamily: 'monospace', color: '#666' }}>
323
+ <Spin size="small" style={{ marginRight: '8px' }} />
324
+ {space.buildLog || 'AI is initiating task...'}
325
+ </div>
326
+ </div>
327
+ )}
328
+
329
+ {space.status === 'completed' && space.buildLog && (
330
+ <Alert
331
+ message="Build Complete"
332
+ description={space.buildLog}
333
+ type="success"
334
+ showIcon
335
+ style={{ marginTop: '12px' }}
336
+ />
337
+ )}
338
+
339
+ {space.status === 'error' && space.buildLog && (
340
+ <Alert
341
+ message="Generation Failed"
342
+ description={space.buildLog}
343
+ type="error"
344
+ showIcon
345
+ style={{ marginTop: '12px' }}
346
+ />
347
+ )}
348
+ </Card>
349
+ )}
350
+ />
351
+
352
+ <Modal
353
+ title={editingSpace ? 'Edit Generation Settings' : 'New UI Generation Space'}
354
+ open={modalVisible}
355
+ onOk={handleSave}
356
+ onCancel={() => setModalVisible(false)}
357
+ width={720}
358
+ destroyOnClose
359
+ >
360
+ <Form form={form} layout="vertical" style={{ marginTop: '16px' }}>
361
+ <Form.Item
362
+ name="title"
363
+ label={<Text strong>Space Name</Text>}
364
+ rules={[{ required: true, message: 'Please enter a space name' }]}
365
+ >
366
+ <Input placeholder="e.g. Sales KPI Dashboard, Customer Contact Form" />
367
+ </Form.Item>
368
+
369
+ <Space size="large" style={{ display: 'flex', width: '100%' }}>
370
+ <Form.Item
371
+ name="llmService"
372
+ label={<Text strong>AI Service</Text>}
373
+ rules={[{ required: true, message: 'Please select an LLM Service' }]}
374
+ style={{ flex: 1, minWidth: '300px' }}
375
+ >
376
+ <Select placeholder="Select Service" onChange={() => form.setFieldValue('model', undefined)}>
377
+ {services.map((s) => (
378
+ <Select.Option key={s.name} value={s.name}>
379
+ {s.title || s.name}
380
+ </Select.Option>
381
+ ))}
382
+ </Select>
383
+ </Form.Item>
384
+
385
+ <Form.Item
386
+ name="model"
387
+ label={<Text strong>Model</Text>}
388
+ rules={[{ required: true, message: 'Please select an LLM Model' }]}
389
+ style={{ flex: 1, minWidth: '300px' }}
390
+ >
391
+ <Select placeholder="Select Model" disabled={!selectedService}>
392
+ {models.map((m) => (
393
+ <Select.Option key={m.id || m.name} value={m.id || m.name}>
394
+ {m.id || m.name}
395
+ </Select.Option>
396
+ ))}
397
+ </Select>
398
+ </Form.Item>
399
+ </Space>
400
+
401
+ <Space size="large" style={{ display: 'flex', width: '100%' }}>
402
+ <Form.Item
403
+ name="type"
404
+ label={<Text strong>Template Type</Text>}
405
+ rules={[{ required: true }]}
406
+ style={{ flex: 1 }}
407
+ >
408
+ <Radio.Group>
409
+ <Radio.Button value="block">Block (V2)</Radio.Button>
410
+ <Radio.Button value="popup">Popup (V2)</Radio.Button>
411
+ </Radio.Group>
412
+ </Form.Item>
413
+
414
+ <Form.Item
415
+ name="targetCollection"
416
+ label={<Text strong>Bind Database Collection</Text>}
417
+ style={{ flex: 1, minWidth: '300px' }}
418
+ >
419
+ <Select placeholder="Select target collection (optional)" allowClear showSearch>
420
+ {collections.map((c) => (
421
+ <Select.Option key={c.name} value={c.name}>
422
+ {c.title || c.name}
423
+ </Select.Option>
424
+ ))}
425
+ </Select>
426
+ </Form.Item>
427
+ </Space>
428
+
429
+ <Form.Item
430
+ name="promptRequirements"
431
+ label={<Text strong>UI Requirements & Features Description</Text>}
432
+ rules={[{ required: true, message: 'Please describe the layout you need AI to generate' }]}
433
+ >
434
+ <Input.TextArea
435
+ rows={4}
436
+ placeholder="e.g. Build a comprehensive customer feedback form featuring inputs for name, email, rating slider, multi-line comment text area, and an agreement checkbox. Place them in a nice 2-column grid."
437
+ />
438
+ </Form.Item>
439
+
440
+ <Form.Item name="systemPrompt" label={<Text strong>Advanced System Prompt Override (Optional)</Text>}>
441
+ <Input.TextArea
442
+ rows={3}
443
+ placeholder="Override the default system prompt to customize how the LLM structures the component trees."
444
+ />
445
+ </Form.Item>
446
+ </Form>
447
+ </Modal>
448
+ </div>
449
+ );
450
+ };
@@ -0,0 +1,2 @@
1
+ export * from './plugin';
2
+ export { default } from './plugin';
@@ -0,0 +1,15 @@
1
+ import { Plugin } from '@nocobase/client';
2
+ import { BuildUITemplateManager } from './BuildUITemplateManager';
3
+
4
+ export class PluginBuildUITemplateClient extends Plugin {
5
+ async load() {
6
+ this.app.pluginSettingsManager.add('ai-build-ui-template', {
7
+ icon: 'LayoutOutlined',
8
+ title: 'Build UI Template',
9
+ Component: BuildUITemplateManager,
10
+ aclSnippet: 'pm.ai-build-ui-template',
11
+ });
12
+ }
13
+ }
14
+
15
+ export default PluginBuildUITemplateClient;
@@ -0,0 +1 @@
1
+ export { default } from './plugin';
@@ -0,0 +1,24 @@
1
+ import { Plugin, Application } from '@nocobase/client-v2';
2
+ import React from 'react';
3
+
4
+ export class PluginBuildUiTemplateClient extends Plugin<Record<string, never>, Application> {
5
+ async load() {
6
+ this.pluginSettingsManager.addMenuItem({
7
+ key: 'ai-build-ui-template',
8
+ title: this.t('Build UI Template'),
9
+ icon: 'LayoutOutlined',
10
+ aclSnippet: 'pm.ai-build-ui-template',
11
+ });
12
+
13
+ this.pluginSettingsManager.addPageTabItem({
14
+ menuKey: 'ai-build-ui-template',
15
+ key: 'index',
16
+ title: this.t('Build UI Template'),
17
+
18
+ componentLoader: () => import('../client/BuildUITemplateManager').then(m => ({ default: m.BuildUITemplateManager })),
19
+ });
20
+
21
+ }
22
+ }
23
+
24
+ export default PluginBuildUiTemplateClient;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './server';
2
+ export { default } from './server';