robobyte-front-builder 1.0.21 → 1.0.24
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/INTEGRATION.md +1586 -0
- package/README.md +791 -74
- package/RoboByteBuilder_User_Manual.docx +0 -0
- package/package.json +4 -2
- package/src/context/BuilderContext.jsx +63 -7
- package/src/lib/index.js +8 -0
- package/src/pages/api/ai.js +20 -0
- package/src/pages/printBuilder/index.jsx +3 -3
- package/src/pages/reportModule/reportBuilder/reportViewer/index.js +59 -3
- package/src/pages/viewBuilder/index.jsx +2 -2
- package/src/services/Endpoints/ReportBuilderEndpoints.js +7 -7
- package/src/services/sessionLog.js +171 -0
- package/src/views/builder/SessionLogDialog.jsx +350 -0
- package/src/views/builder/UnsavedChangesGuard.jsx +103 -0
- package/src/views/builder/inspector/definitions/reportViewer/main.js +13 -0
- package/src/views/builder/sidebar/tabs/AiTab/aiProvider.js +7 -3
- package/src/views/builder/sidebar/tabs/AiTab/schemaTransformer.js +65 -0
- package/src/views/builder/sidebar/tabs/AiTab/trainingExport.js +131 -0
- package/src/views/builder/sidebar/tabs/ViewTab.jsx +173 -13
- package/src/views/builder/viewer/ViewerComponentWrapper.jsx +9 -5
- package/src/views/builder/viewer/ViewerToolbar.jsx +18 -3
- package/src/views/builder/viewer/renderers/ReportViewerRenderer.jsx +46 -2
- package/src/views/genericTable/SGrid.js +656 -384
- package/src/views/printBuilder/PrintBuilderViewer.jsx +22 -2
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a Together AI fine-tuning JSONL file from session-log entries.
|
|
3
|
+
*
|
|
4
|
+
* Together accepts the OpenAI conversational fine-tune format:
|
|
5
|
+
* {"messages": [
|
|
6
|
+
* {"role": "system", "content": "<system prompt>"},
|
|
7
|
+
* {"role": "user", "content": "<user instruction>"},
|
|
8
|
+
* {"role": "assistant", "content": "<assistant reply>"}
|
|
9
|
+
* ]}
|
|
10
|
+
* One example per line.
|
|
11
|
+
*
|
|
12
|
+
* For each session-log entry:
|
|
13
|
+
* - system = full system prompt from buildSystemPrompt()
|
|
14
|
+
* - user = entry.prompt (skipped if empty)
|
|
15
|
+
* - assistant = a brief explanation + ```json {...} ``` block carrying the
|
|
16
|
+
* simplified schema. Identical to OUTPUT_FORMAT in the system
|
|
17
|
+
* prompt so the model learns to produce the same shape.
|
|
18
|
+
*
|
|
19
|
+
* End-to-end fine-tune workflow:
|
|
20
|
+
*
|
|
21
|
+
* 1. Save examples in the UI Builder / Print Builder. They are auto-logged
|
|
22
|
+
* to localStorage (rbb:sessionLogs:v1).
|
|
23
|
+
* 2. Open Session Logs dialog → fill in the `prompt` field for each entry
|
|
24
|
+
* that should be used for training. Entries without a prompt are
|
|
25
|
+
* excluded from the export.
|
|
26
|
+
* 3. Click "Export for Together fine-tuning" → downloads .jsonl.
|
|
27
|
+
* 4. CLI: `pip install together && export TOGETHER_API_KEY=…`
|
|
28
|
+
* `together files check rbb-finetune-…jsonl`
|
|
29
|
+
* `together files upload rbb-finetune-…jsonl \
|
|
30
|
+
* --type jsonl --purpose fine-tune`
|
|
31
|
+
* (the --type flag is required — Together rejects the upload
|
|
32
|
+
* with "invalid type, only JsonL, parquet, csv are allowed"
|
|
33
|
+
* if the request omits file_type, even when the extension
|
|
34
|
+
* is .jsonl.)
|
|
35
|
+
* (note the returned file id, then:)
|
|
36
|
+
* `together fine-tuning create \
|
|
37
|
+
* --training-file <file-id> \
|
|
38
|
+
* --model meta-llama/Llama-3.3-70B-Instruct-Reference \
|
|
39
|
+
* --lora`
|
|
40
|
+
* 5. Wait for the job to finish (Together emails you, or `together
|
|
41
|
+
* fine-tuning list`).
|
|
42
|
+
* 6. Deploy as a Dedicated Endpoint, or use serverless if supported for
|
|
43
|
+
* that base model.
|
|
44
|
+
* 7. In .env.local set:
|
|
45
|
+
* NEXT_PUBLIC_AI_PROVIDER=together
|
|
46
|
+
* TOGETHER_API_KEY=...
|
|
47
|
+
* NEXT_PUBLIC_TOGETHER_MODEL=<your-fine-tune-id>
|
|
48
|
+
* The /api/ai proxy already supports the `together` provider.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { buildSystemPrompt } from './systemPrompt'
|
|
52
|
+
import { simplifyFullSchema } from './schemaTransformer'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the assistant content for one example. Mirrors OUTPUT_FORMAT in the
|
|
56
|
+
* system prompt: a brief explanation followed by one fenced JSON block.
|
|
57
|
+
*/
|
|
58
|
+
function buildAssistantContent(entry, simplifiedSchema) {
|
|
59
|
+
const innerMessage = (entry.notes && entry.notes.trim())
|
|
60
|
+
? entry.notes.trim()
|
|
61
|
+
: `Generated ${entry.builder === 'print' ? 'print layout' : 'view'} schema.`
|
|
62
|
+
|
|
63
|
+
const payload = {
|
|
64
|
+
type: 'schema',
|
|
65
|
+
message: innerMessage,
|
|
66
|
+
schema: simplifiedSchema,
|
|
67
|
+
}
|
|
68
|
+
return `${innerMessage}\n\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\``
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert eligible entries into one Together-fine-tune training example each.
|
|
73
|
+
* Returns an array of { messages: [...] } objects. Entries are skipped if:
|
|
74
|
+
* - prompt is empty (no instruction text → can't be a (instruction, output) pair)
|
|
75
|
+
* - the simplified schema has no root (transformer failed)
|
|
76
|
+
*
|
|
77
|
+
* Returns { examples, skipped } so callers can warn the user about losses.
|
|
78
|
+
*/
|
|
79
|
+
export function buildTrainingExamples(entries) {
|
|
80
|
+
const systemPrompt = buildSystemPrompt()
|
|
81
|
+
const examples = []
|
|
82
|
+
const skipped = []
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const userMessage = (entry.prompt ?? '').trim()
|
|
86
|
+
if (!userMessage) {
|
|
87
|
+
skipped.push({ id: entry.id, reason: 'empty prompt' })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const simplified = simplifyFullSchema(entry.schema)
|
|
91
|
+
if (!simplified.root) {
|
|
92
|
+
skipped.push({ id: entry.id, reason: 'unusable schema' })
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
const assistantContent = buildAssistantContent(entry, simplified)
|
|
96
|
+
examples.push({
|
|
97
|
+
messages: [
|
|
98
|
+
{ role: 'system', content: systemPrompt },
|
|
99
|
+
{ role: 'user', content: userMessage },
|
|
100
|
+
{ role: 'assistant', content: assistantContent },
|
|
101
|
+
],
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
return { examples, skipped }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Serialize examples as JSONL — one JSON object per line. */
|
|
108
|
+
export function buildTrainingJSONL(entries) {
|
|
109
|
+
const { examples, skipped } = buildTrainingExamples(entries)
|
|
110
|
+
const jsonl = examples.map(ex => JSON.stringify(ex)).join('\n')
|
|
111
|
+
return { jsonl, count: examples.length, skipped }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Trigger a JSONL file download. Returns { count, skipped }. */
|
|
115
|
+
export function exportTrainingJSONL(entries) {
|
|
116
|
+
if (typeof window === 'undefined') return { count: 0, skipped: [] }
|
|
117
|
+
const { jsonl, count, skipped } = buildTrainingJSONL(entries)
|
|
118
|
+
if (count === 0) return { count: 0, skipped }
|
|
119
|
+
|
|
120
|
+
const blob = new Blob([jsonl + '\n'], { type: 'application/jsonl' })
|
|
121
|
+
const url = URL.createObjectURL(blob)
|
|
122
|
+
const a = document.createElement('a')
|
|
123
|
+
const stamp = new Date().toISOString().slice(0, 10)
|
|
124
|
+
a.href = url
|
|
125
|
+
a.download = `rbb-finetune-${stamp}.jsonl`
|
|
126
|
+
document.body.appendChild(a)
|
|
127
|
+
a.click()
|
|
128
|
+
document.body.removeChild(a)
|
|
129
|
+
setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
130
|
+
return { count, skipped }
|
|
131
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Box,
|
|
3
|
-
|
|
2
|
+
Box,
|
|
3
|
+
Button,
|
|
4
|
+
CircularProgress,
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogActions,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
TextField
|
|
10
|
+
} from '@mui/material'
|
|
11
|
+
import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'
|
|
4
12
|
import { useBuilder } from 'context/BuilderContext'
|
|
5
13
|
import { useRouter } from 'next/router'
|
|
6
14
|
import { useEffect, useState } from 'react'
|
|
@@ -9,14 +17,36 @@ import { Endpoints, Services } from 'services/Endpoints'
|
|
|
9
17
|
import Typography from '@mui/material/Typography'
|
|
10
18
|
import GroupLevelAutocomplete from 'views/builder/sidebar/tabs/Components/GroupLevelAutocomplete'
|
|
11
19
|
|
|
20
|
+
// ── Defaults for the "Save As New" dialog ──────────────────────────────────
|
|
21
|
+
// "Copy of X" is the universal duplicate convention (Excel, Photoshop, …).
|
|
22
|
+
// viewIds must be unique server-side, so we append "_copy" — the user can
|
|
23
|
+
// edit before submitting if the suffix would collide with an existing view.
|
|
24
|
+
function suggestCopyTitle(title) {
|
|
25
|
+
const t = (title ?? '').trim()
|
|
26
|
+
if (!t) return 'Copy'
|
|
27
|
+
if (/^copy of\b/i.test(t)) return t // don't recursively prefix "Copy of Copy of …"
|
|
28
|
+
return `Copy of ${t}`
|
|
29
|
+
}
|
|
12
30
|
|
|
31
|
+
function suggestCopyViewId(viewId) {
|
|
32
|
+
const v = (viewId ?? '').trim()
|
|
33
|
+
if (!v) return ''
|
|
34
|
+
if (/_copy$/i.test(v)) return `${v}_${Date.now().toString(36)}`
|
|
35
|
+
return `${v}_copy`
|
|
36
|
+
}
|
|
13
37
|
|
|
14
38
|
export default function ViewTab() {
|
|
15
39
|
const router = useRouter()
|
|
16
|
-
const {
|
|
17
|
-
const { viewMetaData,setViewMetaData,schema, setSchema } = useBuilder()
|
|
40
|
+
const { viewMetaData, setViewMetaData, schema, markClean, logSchema } = useBuilder()
|
|
18
41
|
const [isLoading, setIsLoading] = useState(false)
|
|
19
|
-
|
|
42
|
+
|
|
43
|
+
// Save As New / Duplicate
|
|
44
|
+
const [duplicateOpen, setDuplicateOpen] = useState(false)
|
|
45
|
+
const [isDuplicating, setIsDuplicating] = useState(false)
|
|
46
|
+
const [duplicateTitle, setDuplicateTitle] = useState('')
|
|
47
|
+
const [duplicateViewId, setDuplicateViewId] = useState('')
|
|
48
|
+
|
|
49
|
+
const { control, handleSubmit, reset, errors } = useForm({
|
|
20
50
|
defaultValues: {
|
|
21
51
|
title: viewMetaData?.title || '',
|
|
22
52
|
viewId: null,
|
|
@@ -28,19 +58,18 @@ export default function ViewTab() {
|
|
|
28
58
|
try {
|
|
29
59
|
setIsLoading(true)
|
|
30
60
|
let data = {
|
|
31
|
-
id
|
|
61
|
+
id: viewMetaData?.id ?? 0,
|
|
32
62
|
title: values.title,
|
|
33
63
|
viewId: values.viewId,
|
|
34
64
|
groupId: values.groupId ?? null,
|
|
35
65
|
value: JSON.stringify(schema)
|
|
36
66
|
}
|
|
37
67
|
|
|
38
|
-
let response = null
|
|
68
|
+
let response = null
|
|
39
69
|
|
|
40
|
-
if(viewMetaData.isNew) {
|
|
70
|
+
if (viewMetaData.isNew) {
|
|
41
71
|
response = await Services.PostService(Endpoints.UiBuilder.Post.AddUpdate, true, data)
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
72
|
+
} else {
|
|
44
73
|
response = await Services.UpdateService(
|
|
45
74
|
Endpoints.UiBuilder.Post.AddUpdate,
|
|
46
75
|
true,
|
|
@@ -58,6 +87,16 @@ export default function ViewTab() {
|
|
|
58
87
|
groupId: values.groupId,
|
|
59
88
|
id: viewMetaData.id ?? response.data
|
|
60
89
|
}))
|
|
90
|
+
markClean?.()
|
|
91
|
+
try {
|
|
92
|
+
logSchema?.({
|
|
93
|
+
builder: 'ui',
|
|
94
|
+
trigger: 'save',
|
|
95
|
+
title: values.title,
|
|
96
|
+
viewMetaData: { id: viewMetaData.id ?? response.data, title: values.title, viewId: values.viewId },
|
|
97
|
+
schema
|
|
98
|
+
})
|
|
99
|
+
} catch (e) { console.warn('[sessionLog] log on save failed:', e) }
|
|
61
100
|
}
|
|
62
101
|
} catch (error) {
|
|
63
102
|
console.error(error)
|
|
@@ -65,6 +104,71 @@ export default function ViewTab() {
|
|
|
65
104
|
setIsLoading(false)
|
|
66
105
|
}
|
|
67
106
|
}
|
|
107
|
+
|
|
108
|
+
// ── Save As New ────────────────────────────────────────────────────────────
|
|
109
|
+
// Always POSTs with id: 0 so the backend creates a new row. Title + viewId
|
|
110
|
+
// come from the dialog form (pre-filled with sensible "Copy of …" defaults
|
|
111
|
+
// but editable). On success, navigate to the new view via URL so the
|
|
112
|
+
// standard load path in viewBuilder/index.jsx picks it up — no manual
|
|
113
|
+
// state sync needed.
|
|
114
|
+
const openDuplicateDialog = () => {
|
|
115
|
+
setDuplicateTitle(suggestCopyTitle(viewMetaData?.title))
|
|
116
|
+
setDuplicateViewId(suggestCopyViewId(viewMetaData?.viewId))
|
|
117
|
+
setDuplicateOpen(true)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const closeDuplicateDialog = () => {
|
|
121
|
+
if (isDuplicating) return
|
|
122
|
+
setDuplicateOpen(false)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const handleSaveAsNew = async () => {
|
|
126
|
+
if (!schema?.root) return
|
|
127
|
+
const title = (duplicateTitle ?? '').trim()
|
|
128
|
+
const viewId = (duplicateViewId ?? '').trim()
|
|
129
|
+
if (!title) return
|
|
130
|
+
if (!viewId) return
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
setIsDuplicating(true)
|
|
134
|
+
const data = {
|
|
135
|
+
id: 0,
|
|
136
|
+
title,
|
|
137
|
+
viewId,
|
|
138
|
+
groupId: viewMetaData?.groupId ?? null,
|
|
139
|
+
value: JSON.stringify(schema)
|
|
140
|
+
}
|
|
141
|
+
const response = await Services.PostService(Endpoints.UiBuilder.Post.AddUpdate, true, data)
|
|
142
|
+
if (!response) return
|
|
143
|
+
|
|
144
|
+
const newId = response.data
|
|
145
|
+
markClean?.()
|
|
146
|
+
// Log the new view as a fresh save (becomes a labelable training example).
|
|
147
|
+
try {
|
|
148
|
+
logSchema?.({
|
|
149
|
+
builder: 'ui',
|
|
150
|
+
trigger: 'duplicate',
|
|
151
|
+
title,
|
|
152
|
+
viewMetaData: { id: newId, title, viewId, groupId: viewMetaData?.groupId ?? null },
|
|
153
|
+
schema
|
|
154
|
+
})
|
|
155
|
+
} catch (e) { console.warn('[sessionLog] log on duplicate failed:', e) }
|
|
156
|
+
|
|
157
|
+
// Hand off to the page's load path — switching `id` in the query triggers
|
|
158
|
+
// viewBuilder/index.jsx's useEffect → handleGetView → loadSchema.
|
|
159
|
+
setDuplicateOpen(false)
|
|
160
|
+
await router.push(
|
|
161
|
+
{ pathname: router.pathname, query: { ...router.query, id: newId } },
|
|
162
|
+
undefined,
|
|
163
|
+
{ shallow: false }
|
|
164
|
+
)
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('Save As New failed:', error)
|
|
167
|
+
} finally {
|
|
168
|
+
setIsDuplicating(false)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
68
172
|
useEffect(() => {
|
|
69
173
|
reset({
|
|
70
174
|
title: viewMetaData?.title,
|
|
@@ -73,6 +177,7 @@ export default function ViewTab() {
|
|
|
73
177
|
})
|
|
74
178
|
}, [viewMetaData])
|
|
75
179
|
|
|
180
|
+
const canDuplicate = Boolean(schema?.root)
|
|
76
181
|
|
|
77
182
|
return (
|
|
78
183
|
<Box p={2}>
|
|
@@ -104,9 +209,7 @@ export default function ViewTab() {
|
|
|
104
209
|
/>
|
|
105
210
|
<GroupLevelAutocomplete selectedViewGroup={viewMetaData?.group} control={control} errors={errors} />
|
|
106
211
|
<Button
|
|
107
|
-
sx={{
|
|
108
|
-
mt: 2
|
|
109
|
-
}}
|
|
212
|
+
sx={{ mt: 2 }}
|
|
110
213
|
type='submit'
|
|
111
214
|
size={'small'}
|
|
112
215
|
fullWidth={true}
|
|
@@ -115,8 +218,65 @@ export default function ViewTab() {
|
|
|
115
218
|
>
|
|
116
219
|
{isLoading ? <CircularProgress size={24} /> : 'Save'}
|
|
117
220
|
</Button>
|
|
221
|
+
<Button
|
|
222
|
+
size='small'
|
|
223
|
+
fullWidth
|
|
224
|
+
variant='outlined'
|
|
225
|
+
startIcon={<ContentCopyOutlinedIcon fontSize='small' />}
|
|
226
|
+
onClick={openDuplicateDialog}
|
|
227
|
+
disabled={!canDuplicate || isLoading || isDuplicating}
|
|
228
|
+
sx={{ mt: -1.5 }}
|
|
229
|
+
>
|
|
230
|
+
Save As New
|
|
231
|
+
</Button>
|
|
118
232
|
</Box>
|
|
119
233
|
</form>
|
|
234
|
+
|
|
235
|
+
{/* ── Save As New / Duplicate dialog ───────────────────────────────────── */}
|
|
236
|
+
<Dialog open={duplicateOpen} onClose={closeDuplicateDialog} maxWidth='xs' fullWidth>
|
|
237
|
+
<DialogTitle>Save As New View</DialogTitle>
|
|
238
|
+
<DialogContent>
|
|
239
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
240
|
+
<Box>
|
|
241
|
+
<Typography variant='body2'>Title</Typography>
|
|
242
|
+
<TextField
|
|
243
|
+
autoFocus
|
|
244
|
+
size='small'
|
|
245
|
+
fullWidth
|
|
246
|
+
value={duplicateTitle}
|
|
247
|
+
onChange={e => setDuplicateTitle(e.target.value)}
|
|
248
|
+
onKeyDown={e => { if (e.key === 'Enter' && duplicateTitle.trim() && duplicateViewId.trim()) handleSaveAsNew() }}
|
|
249
|
+
disabled={isDuplicating}
|
|
250
|
+
/>
|
|
251
|
+
</Box>
|
|
252
|
+
<Box>
|
|
253
|
+
<Typography variant='body2'>View ID</Typography>
|
|
254
|
+
<TextField
|
|
255
|
+
size='small'
|
|
256
|
+
fullWidth
|
|
257
|
+
value={duplicateViewId}
|
|
258
|
+
onChange={e => setDuplicateViewId(e.target.value)}
|
|
259
|
+
helperText='Must be unique. Edit if "<id>_copy" already exists.'
|
|
260
|
+
disabled={isDuplicating}
|
|
261
|
+
/>
|
|
262
|
+
</Box>
|
|
263
|
+
<Typography variant='caption' color='text.secondary'>
|
|
264
|
+
The current schema will be saved as a brand-new view. The original is untouched.
|
|
265
|
+
You will be navigated to the new view after save.
|
|
266
|
+
</Typography>
|
|
267
|
+
</Box>
|
|
268
|
+
</DialogContent>
|
|
269
|
+
<DialogActions>
|
|
270
|
+
<Button onClick={closeDuplicateDialog} disabled={isDuplicating}>Cancel</Button>
|
|
271
|
+
<Button
|
|
272
|
+
variant='contained'
|
|
273
|
+
onClick={handleSaveAsNew}
|
|
274
|
+
disabled={isDuplicating || !duplicateTitle.trim() || !duplicateViewId.trim()}
|
|
275
|
+
>
|
|
276
|
+
{isDuplicating ? <CircularProgress size={20} /> : 'Save As New'}
|
|
277
|
+
</Button>
|
|
278
|
+
</DialogActions>
|
|
279
|
+
</Dialog>
|
|
120
280
|
</Box>
|
|
121
281
|
)
|
|
122
282
|
}
|
|
@@ -69,6 +69,8 @@ export default function ViewerComponentWrapper({ node, children, viewerContext }
|
|
|
69
69
|
// Get selection state from builder context (only available in edit mode)
|
|
70
70
|
const isSelected = builderContext?.selectedId === node.id
|
|
71
71
|
const canEdit = isEditMode && (builderContext != null)
|
|
72
|
+
const isRoot = node.id === 'root'
|
|
73
|
+
const canDrag = canEdit && !isRoot
|
|
72
74
|
const isDraggingThis = builderContext?.draggingNodeId === node.id
|
|
73
75
|
// Handle component selection
|
|
74
76
|
const handleClick = e => {
|
|
@@ -213,7 +215,7 @@ export default function ViewerComponentWrapper({ node, children, viewerContext }
|
|
|
213
215
|
<Box
|
|
214
216
|
className={node.className}
|
|
215
217
|
style={elementCss}
|
|
216
|
-
draggable={
|
|
218
|
+
draggable={canDrag}
|
|
217
219
|
onDragStart={handleDragStart}
|
|
218
220
|
onDragEnd={handleDragEnd}
|
|
219
221
|
onClick={handleClick}
|
|
@@ -226,15 +228,17 @@ export default function ViewerComponentWrapper({ node, children, viewerContext }
|
|
|
226
228
|
...elementStyle,
|
|
227
229
|
// Edit mode styling
|
|
228
230
|
...(canEdit && {
|
|
229
|
-
cursor: 'grab',
|
|
231
|
+
cursor: canDrag ? 'grab' : 'default',
|
|
230
232
|
outline: isSelected ? '2px solid #1976d2' : '1px solid transparent',
|
|
231
233
|
outlineOffset: '2px',
|
|
232
234
|
'&:hover': {
|
|
233
235
|
outline: isSelected ? '2px solid #1976d2' : '1px solid #90caf9'
|
|
234
236
|
},
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
...(canDrag && {
|
|
238
|
+
'&:active': {
|
|
239
|
+
cursor: 'grabbing'
|
|
240
|
+
}
|
|
241
|
+
})
|
|
238
242
|
}),
|
|
239
243
|
// Show hidden components with opacity in edit mode
|
|
240
244
|
...(isEditMode &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, ToggleButton, ToggleButtonGroup, IconButton, Tooltip, Dialog, Tabs, Tab, Chip } from '@mui/material'
|
|
2
2
|
import { useState, useCallback, useEffect } from 'react'
|
|
3
|
-
import { DataObjectOutlined, FullscreenExitOutlined, FullscreenOutlined, CloseOutlined, BugReportOutlined, RefreshOutlined, DarkModeOutlined, LightModeOutlined, UndoOutlined, RedoOutlined } from '@mui/icons-material'
|
|
3
|
+
import { DataObjectOutlined, FullscreenExitOutlined, FullscreenOutlined, CloseOutlined, BugReportOutlined, RefreshOutlined, DarkModeOutlined, LightModeOutlined, UndoOutlined, RedoOutlined, HistoryOutlined } from '@mui/icons-material'
|
|
4
4
|
import Editor from '@monaco-editor/react'
|
|
5
5
|
import SaveViewDialog from './SaveViewDialog'
|
|
6
6
|
import { useBuilder } from 'src/context/BuilderContext'
|
|
@@ -185,7 +185,7 @@ function DebugPanel({ open, onClose, getState }) {
|
|
|
185
185
|
// ── Main toolbar ──────────────────────────────────────────────────────────────
|
|
186
186
|
|
|
187
187
|
export default function ViewerToolbar({ mode, setMode, previewStateRef, isDark, onToggleTheme }) {
|
|
188
|
-
const { schema, data, form, dataRef, reportRefs, undo, redo, canUndo, canRedo } = useBuilder()
|
|
188
|
+
const { schema, data, form, dataRef, reportRefs, undo, redo, canUndo, canRedo, markClean, logSchema, openSessionLog, viewMetaData } = useBuilder()
|
|
189
189
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
|
190
190
|
const [isSaving, setIsSaving] = useState(false)
|
|
191
191
|
const [schemaOpen, setSchemaOpen] = useState(false)
|
|
@@ -197,7 +197,15 @@ export default function ViewerToolbar({ mode, setMode, previewStateRef, isDark,
|
|
|
197
197
|
setIsSaving(true)
|
|
198
198
|
try {
|
|
199
199
|
const response = await PostService(Endpoints.View.Post.Add, true, { title, value: JSON.stringify(schema) })
|
|
200
|
-
if (response)
|
|
200
|
+
if (response) {
|
|
201
|
+
setSaveDialogOpen(false)
|
|
202
|
+
markClean()
|
|
203
|
+
// Capture this save as a future training example. Prompt is left
|
|
204
|
+
// empty — developers fill it in via the Session Logs dialog.
|
|
205
|
+
try {
|
|
206
|
+
logSchema({ builder: 'ui', trigger: 'save', title, viewMetaData, schema })
|
|
207
|
+
} catch (e) { console.warn('[sessionLog] log on save failed:', e) }
|
|
208
|
+
}
|
|
201
209
|
} catch (error) {
|
|
202
210
|
console.error('Error saving view:', error)
|
|
203
211
|
} finally {
|
|
@@ -244,6 +252,13 @@ export default function ViewerToolbar({ mode, setMode, previewStateRef, isDark,
|
|
|
244
252
|
</Tooltip>
|
|
245
253
|
)}
|
|
246
254
|
|
|
255
|
+
{/* Session Logs */}
|
|
256
|
+
<Tooltip title='Session Logs (training data)'>
|
|
257
|
+
<IconButton size='small' onClick={openSessionLog} sx={{ color: 'text.secondary' }}>
|
|
258
|
+
<HistoryOutlined fontSize='small' />
|
|
259
|
+
</IconButton>
|
|
260
|
+
</Tooltip>
|
|
261
|
+
|
|
247
262
|
{/* State Inspector */}
|
|
248
263
|
<Tooltip title='State Inspector'>
|
|
249
264
|
<IconButton
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useCallback, useState } from 'react'
|
|
1
|
+
import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react'
|
|
2
2
|
import { Box, CircularProgress } from '@mui/material'
|
|
3
3
|
import ReportViewer from 'pages/builders/report/viewer'
|
|
4
4
|
import ViewerComponentWrapper from '../ViewerComponentWrapper'
|
|
@@ -6,6 +6,7 @@ import { resolveProps } from 'services/builderHelper/resolveProps'
|
|
|
6
6
|
import RowActionsCell from './RowActionsCell'
|
|
7
7
|
import { PrintTable } from '../PrintDialog'
|
|
8
8
|
import fetchReportDataByPageId from 'services/reportData/fetchReportData'
|
|
9
|
+
import { executeJSCode } from 'services/builderHelper/jsExecutor'
|
|
9
10
|
|
|
10
11
|
// ── Print-mode report table ────────────────────────────────────────────────────
|
|
11
12
|
// Uses fetchReportDataByPageId with isPagination:false to load ALL rows in one
|
|
@@ -104,7 +105,9 @@ export default function ReportViewerRenderer({ node, viewerContext }) {
|
|
|
104
105
|
|
|
105
106
|
// Strip 'key' before spreading — React treats it as a reserved prop and
|
|
106
107
|
// would silently drop it; we pass it explicitly below.
|
|
107
|
-
|
|
108
|
+
// Also strip actionsConfig (row actions — bound separately into actionsRenderer)
|
|
109
|
+
// and viewerActions (page-level actions — bound separately below).
|
|
110
|
+
const { key: _key, actionsConfig: _actionsConfig, viewerActions: _viewerActions, ...mainProps } = main
|
|
108
111
|
|
|
109
112
|
// Each ReportViewer instance owns its updateRef for tracking row mutations.
|
|
110
113
|
// Register under nodeName so column functions can access it via reportRefs.
|
|
@@ -144,6 +147,46 @@ export default function ReportViewerRenderer({ node, viewerContext }) {
|
|
|
144
147
|
[JSON.stringify(actionsConfig)]
|
|
145
148
|
)
|
|
146
149
|
|
|
150
|
+
// ── Build viewer actions with bound onClick handlers ───────────────────────
|
|
151
|
+
// Page-level buttons rendered in the report toolbar after Filter / Refresh.
|
|
152
|
+
// Each item is a copy of the schema config plus a stable `onClick` that
|
|
153
|
+
// runs the Calculation in the same scope as row actions, minus rowData.
|
|
154
|
+
const viewerActionsConfig = Array.isArray(main.viewerActions) && main.viewerActions.length > 0
|
|
155
|
+
? main.viewerActions
|
|
156
|
+
: null
|
|
157
|
+
|
|
158
|
+
const viewerActions = useMemo(() => {
|
|
159
|
+
if (!viewerActionsConfig) return null
|
|
160
|
+
return viewerActionsConfig.map((action, idx) => ({
|
|
161
|
+
key: `${action.label ?? 'action'}-${idx}`,
|
|
162
|
+
label: action.label,
|
|
163
|
+
icon: action.icon,
|
|
164
|
+
color: action.color,
|
|
165
|
+
variant: action.variant,
|
|
166
|
+
disabled: action.disabled,
|
|
167
|
+
confirmation: action.confirmation,
|
|
168
|
+
onClick: async () => {
|
|
169
|
+
if (!action.code) return
|
|
170
|
+
const ctx = ctxRef.current ?? {}
|
|
171
|
+
await executeJSCode(action.code, {
|
|
172
|
+
form: ctx.form ?? {},
|
|
173
|
+
data: ctx.data ?? {},
|
|
174
|
+
setData: ctx.setData ?? (() => {}),
|
|
175
|
+
dataRef: ctx.dataRef?.current ?? {},
|
|
176
|
+
reportRefs: ctx.reportRefs?.current ?? {},
|
|
177
|
+
openDialog: ctx.openDialog ?? (() => {}),
|
|
178
|
+
closeDialog: ctx.closeDialog ?? (() => {}),
|
|
179
|
+
pageData: ctx.pageData,
|
|
180
|
+
pageSetData: ctx.pageSetData,
|
|
181
|
+
pageDataRef: ctx.pageDataRef,
|
|
182
|
+
pageReportRefs: ctx.pageReportRefs,
|
|
183
|
+
})
|
|
184
|
+
},
|
|
185
|
+
}))
|
|
186
|
+
// Stringify the config so we recompute only when its shape changes.
|
|
187
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
188
|
+
}, [JSON.stringify(viewerActionsConfig)])
|
|
189
|
+
|
|
147
190
|
// ── Edit mode: rich placeholder (both UI builder and print builder) ────────
|
|
148
191
|
if (isEditMode) {
|
|
149
192
|
const cols = Array.isArray(mainProps.columnDefs) ? mainProps.columnDefs : []
|
|
@@ -216,6 +259,7 @@ export default function ReportViewerRenderer({ node, viewerContext }) {
|
|
|
216
259
|
noHeader={true}
|
|
217
260
|
{...mainProps}
|
|
218
261
|
actions={actionsRenderer ?? mainProps.actions}
|
|
262
|
+
viewerActions={viewerActions}
|
|
219
263
|
updateRef={updateRef}
|
|
220
264
|
nodeId={nodeName}
|
|
221
265
|
reportRefs={reportRefs}
|