robobyte-front-builder 1.0.21 → 1.0.23

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robobyte-front-builder",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "RoboByte low-code UI builder, Report builder, and navigation extension system",
5
5
  "main": "src/lib/index.js",
6
6
  "files": [
@@ -8,7 +8,9 @@
8
8
  "public",
9
9
  "styles",
10
10
  "next.config.js",
11
- "jsconfig.json"
11
+ "jsconfig.json",
12
+ "INTEGRATION.md",
13
+ "RoboByteBuilder_User_Manual.docx"
12
14
  ],
13
15
  "exports": {
14
16
  ".": "./src/lib/index.js",
@@ -2,6 +2,9 @@ import { createContext, useCallback, useContext, useEffect, useReducer, useRef,
2
2
  import { executeActionsByEvent } from 'services/builderHelper/actionExecutor'
3
3
  import { findNode } from 'services/builderHelper'
4
4
  import { executeJSCode } from 'services/builderHelper/jsExecutor'
5
+ import UnsavedChangesGuard from 'views/builder/UnsavedChangesGuard'
6
+ import SessionLogDialog from 'views/builder/SessionLogDialog'
7
+ import { logSchema as logSchemaToStore } from 'services/sessionLog'
5
8
 
6
9
  const BuilderContext = createContext(null)
7
10
 
@@ -136,6 +139,16 @@ function historyReducer(state, action) {
136
139
  future: state.future.slice(1)
137
140
  }
138
141
  }
142
+ // LOAD — replace the schema without marking it as a user edit (e.g. fetched
143
+ // from server). Clears history so the freshly loaded state is the new baseline.
144
+ case 'LOAD': {
145
+ return { past: [], present: action.payload, future: [] }
146
+ }
147
+ // MARK_CLEAN — keep the present schema but reset history so isDirty becomes
148
+ // false. Used after a successful save.
149
+ case 'MARK_CLEAN': {
150
+ return { past: [], present: state.present, future: [] }
151
+ }
139
152
  default:
140
153
  return state
141
154
  }
@@ -150,12 +163,17 @@ export function BuilderProvider({ children }) {
150
163
 
151
164
  // Convenience aliases — schema reads the present snapshot;
152
165
  // setSchema dispatches a SET action (automatically tracked in history).
153
- const schema = historyState.present
154
- const setSchema = useCallback((updater) => dispatch({ type: 'SET', payload: updater }), [])
155
- const undo = useCallback(() => dispatch({ type: 'UNDO' }), [])
156
- const redo = useCallback(() => dispatch({ type: 'REDO' }), [])
157
- const canUndo = historyState.past.length > 0
158
- const canRedo = historyState.future.length > 0
166
+ const schema = historyState.present
167
+ const setSchema = useCallback((updater) => dispatch({ type: 'SET', payload: updater }), [])
168
+ const loadSchema = useCallback((next) => dispatch({ type: 'LOAD', payload: next }), [])
169
+ const markClean = useCallback(() => dispatch({ type: 'MARK_CLEAN' }), [])
170
+ const undo = useCallback(() => dispatch({ type: 'UNDO' }), [])
171
+ const redo = useCallback(() => dispatch({ type: 'REDO' }), [])
172
+ const canUndo = historyState.past.length > 0
173
+ const canRedo = historyState.future.length > 0
174
+ // isDirty — present schema differs from the last loaded/saved baseline.
175
+ // Equivalent to canUndo because LOAD and MARK_CLEAN both reset past to [].
176
+ const isDirty = canUndo
159
177
  const [selectedId, setSelectedId] = useState(null)
160
178
  const [viewMetaData, setViewMetaData] = useState({
161
179
  id: null,
@@ -181,6 +199,30 @@ export function BuilderProvider({ children }) {
181
199
  const [draggingNodeId, setDraggingNodeId] = useState(null)
182
200
  const [clipboard, setClipboard] = useState(null) // copy/paste clipboard
183
201
 
202
+ // ── Session log dialog (training-data capture) ───────────────────────────
203
+ const [sessionLogOpen, setSessionLogOpen] = useState(false)
204
+ const openSessionLog = useCallback(() => setSessionLogOpen(true), [])
205
+ const closeSessionLog = useCallback(() => setSessionLogOpen(false), [])
206
+
207
+ /**
208
+ * Append a schema snapshot to the session log. Call this from save handlers
209
+ * so every successful save becomes a labelable training example.
210
+ * `prompt` is intentionally left empty — developers fill it in later via
211
+ * the Session Logs dialog.
212
+ */
213
+ const logSchema = useCallback(({ builder, trigger = 'save', title = '', viewMetaData = {}, schema: schemaArg }) => {
214
+ return logSchemaToStore({
215
+ builder,
216
+ trigger,
217
+ title,
218
+ viewMetaData,
219
+ schema: schemaArg ?? schema,
220
+ prompt: ''
221
+ })
222
+ // schema is captured at call time when omitted; keep deps minimal
223
+ // eslint-disable-next-line react-hooks/exhaustive-deps
224
+ }, [schema])
225
+
184
226
  // ── Unified ref storage ────────────────────────────────────────────────────
185
227
  // dataRef : mutable non-reactive store; read/write from any function
186
228
  // without triggering re-renders. Access via dataRef.current.
@@ -273,6 +315,9 @@ export function BuilderProvider({ children }) {
273
315
  value={{
274
316
  schema,
275
317
  setSchema,
318
+ loadSchema,
319
+ markClean,
320
+ isDirty,
276
321
 
277
322
  // ── Undo / redo ──────────────────────────────────────────────────────
278
323
  undo,
@@ -326,9 +371,20 @@ export function BuilderProvider({ children }) {
326
371
  validationErrors,
327
372
  setValidationErrors,
328
373
  submitForm,
329
- resetForm
374
+ resetForm,
375
+
376
+ // ── Session log (AI training-data capture) ───────────────────────────
377
+ // logSchema({ builder, trigger?, title?, viewMetaData?, schema? })
378
+ // appends an entry to localStorage. Schema defaults to the current
379
+ // builder schema if not provided.
380
+ // openSessionLog / closeSessionLog control the review dialog.
381
+ logSchema,
382
+ openSessionLog,
383
+ closeSessionLog
330
384
  }}
331
385
  >
386
+ <UnsavedChangesGuard isDirty={isDirty} />
387
+ <SessionLogDialog open={sessionLogOpen} onClose={closeSessionLog} />
332
388
  {children}
333
389
  </BuilderContext.Provider>
334
390
  )
@@ -42,6 +42,26 @@ const PROVIDERS = {
42
42
  }),
43
43
  extract: (data) => data?.choices?.[0]?.message?.content ?? '',
44
44
  },
45
+
46
+ // Together AI — OpenAI-compatible API. Set TOGETHER_API_KEY and
47
+ // (optionally) NEXT_PUBLIC_TOGETHER_MODEL — point this at the base
48
+ // model for testing, or at your fine-tuned model id once a fine-tune
49
+ // job has finished (e.g. "username/llama-3.3-70b-rbb-ft-2026-05-10").
50
+ together: {
51
+ url: 'https://api.together.xyz/v1/chat/completions',
52
+ apiKey: () => process.env.TOGETHER_API_KEY ?? '',
53
+ model: () => process.env.NEXT_PUBLIC_TOGETHER_MODEL ?? 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
54
+ headers: (key) => ({
55
+ 'Content-Type': 'application/json',
56
+ Authorization: `Bearer ${key}`,
57
+ }),
58
+ body: (model, messages, system) => ({
59
+ model,
60
+ max_tokens: 8096,
61
+ messages: [{ role: 'system', content: system }, ...messages],
62
+ }),
63
+ extract: (data) => data?.choices?.[0]?.message?.content ?? '',
64
+ },
45
65
  }
46
66
 
47
67
  export default async function handler(req, res) {
@@ -147,7 +147,7 @@ function PrintSidebar({ collapsed }) {
147
147
  function PrintBuilderContent({ isDark, onToggleTheme }) {
148
148
  const router = useRouter()
149
149
  const { id } = router.query
150
- const { setSchema } = useBuilder()
150
+ const { loadSchema } = useBuilder()
151
151
 
152
152
  const [metaData, setMetaData] = useState({ id: null, title: '' })
153
153
  const [leftCollapsed, setLeftCollapsed] = useState(false)
@@ -166,7 +166,7 @@ function PrintBuilderContent({ isDark, onToggleTheme }) {
166
166
  // Ensure root reflects activeZone
167
167
  const activeZone = merged.activeZone ?? INITIAL_PRINT_SCHEMA.activeZone
168
168
  merged.root = merged.zones?.[activeZone] ?? createEmptyNode(activeZone)
169
- setSchema(merged)
169
+ loadSchema(merged)
170
170
  setMetaData({ id: response.data.id, title: response.data.title })
171
171
  }
172
172
  } catch (err) {
@@ -178,7 +178,7 @@ function PrintBuilderContent({ isDark, onToggleTheme }) {
178
178
  if (id) {
179
179
  handleLoad(id)
180
180
  } else {
181
- setSchema(INITIAL_PRINT_SCHEMA)
181
+ loadSchema(INITIAL_PRINT_SCHEMA)
182
182
  }
183
183
  }, [id])
184
184
 
@@ -8,7 +8,7 @@ import CardHeader from "@mui/material/CardHeader";
8
8
  import Grid from "@mui/material/Grid";
9
9
  import {Plus} from "mdi-material-ui";
10
10
  import TAGGrid from "views/genericTable/TAGGrid";
11
- import {SettingsOutlined} from "@mui/icons-material";
11
+ import {SettingsOutlined, AssessmentOutlined} from "@mui/icons-material";
12
12
  import UpdateReportPermissionDialog from "views/rolePermissions/UpdateReportPermissionDialog";
13
13
  import {getReportSession} from 'src/services/helper/reportSessionHelper'
14
14
  import BlankLayout from '../../../../lib/layouts/BlankLayout';
@@ -41,6 +41,13 @@ const ReportViewer = (params) => {
41
41
  globalParams,
42
42
  isRerender = false,
43
43
  updateRef,
44
+ // Presentation — render above the toolbar. If title is omitted, the
45
+ // report's own name from builderMetadata.name is used.
46
+ title,
47
+ caption,
48
+ // Page-level toolbar buttons rendered after Filter / Refresh.
49
+ // Built by ReportViewerRenderer with bound onClick handlers.
50
+ viewerActions,
44
51
  // nodeId / reportRefs are injected by the UI builder's ReportViewerRenderer.
45
52
  // nodeId — the builder schema node ID for this ReportViewer instance
46
53
  // reportRefs — the shared registry { [nodeId]: updateRef } for all reports on the page
@@ -187,14 +194,56 @@ const ReportViewer = (params) => {
187
194
  <CircularProgress/>
188
195
  </Box>
189
196
  ) : !builderModel ? (
190
- <Box textAlign="center" mt={8}>
191
- <Typography variant="subtitle1" color="text.secondary">
197
+ // ── Empty state ─────────────────────────────────────────────────────
198
+ // Shown when no `id` / `pageId` has resolved yet. In the standalone
199
+ // /report/viewer page this is the persistent "pick a report" state;
200
+ // inside the builder it usually flashes for one render before the
201
+ // model loads.
202
+ <Box
203
+ sx={{
204
+ minHeight: minimized ? 'auto' : '60vh',
205
+ display: 'flex',
206
+ flexDirection: 'column',
207
+ alignItems: 'center',
208
+ justifyContent: 'center',
209
+ gap: 1.5,
210
+ p: 4,
211
+ color: 'text.secondary',
212
+ }}
213
+ >
214
+ <Box
215
+ sx={{
216
+ width: 72,
217
+ height: 72,
218
+ borderRadius: '50%',
219
+ display: 'flex',
220
+ alignItems: 'center',
221
+ justifyContent: 'center',
222
+ bgcolor: 'action.hover',
223
+ color: 'text.disabled',
224
+ mb: 0.5,
225
+ }}
226
+ >
227
+ <AssessmentOutlined sx={{ fontSize: 36 }} />
228
+ </Box>
229
+ <Typography variant='h6' sx={{ fontWeight: 600, color: 'text.primary', textAlign: 'center' }}>
192
230
  No report selected
193
231
  </Typography>
232
+ <Typography variant='body2' sx={{ color: 'text.secondary', textAlign: 'center', maxWidth: 380 }}>
233
+ Pick a report from the list, or pass a <code>pageId</code> / <code>id</code> to load one here.
234
+ </Typography>
194
235
  </Box>
195
236
  ) : (
196
237
  <>
197
238
  <Box display="flex" gap={1} flexDirection={'column'} sx={{ width: '100%' }}>
239
+ {/*
240
+ Studio header — only shown on the standalone /report/viewer page
241
+ (noHeader is set by the builder when ReportViewer is embedded as
242
+ a component, which suppresses this row entirely). The title and
243
+ caption are now rendered inside SGrid's toolbar on the same row
244
+ as Filter / Refresh / viewer actions, so this header is just the
245
+ "go to studio" affordance for the standalone page.
246
+ */}
198
247
  {(minimized !== true && noHeader !== true) &&
199
248
  <Box display="flex" justifyContent={'space-between'} alignItems={'center'} gap={1} sx={{p: 2}}>
200
249
  <Typography variant="subtitle1" color="text.secondary">{builderMetadata?.name ?? ""}</Typography>
@@ -209,6 +258,7 @@ const ReportViewer = (params) => {
209
258
  </IconButton>
210
259
  </Tooltip>
211
260
  </Box>}
261
+
212
262
  <Box sx={{width: '100%'}}>
213
263
  <SGrid
214
264
  key={`rv-${payloadVersion}`}
@@ -220,6 +270,12 @@ const ReportViewer = (params) => {
220
270
  setOutGridApi={setOutGridApi}
221
271
  externalTimer={externalTimer}
222
272
  actions={actions}
273
+ viewerActions={viewerActions}
274
+ // Toolbar left side. Fall back to the report's own name from
275
+ // builderMetadata so standalone-page views still get a sensible
276
+ // title without explicit configuration.
277
+ title={title ?? builderMetadata?.name}
278
+ caption={caption}
223
279
  reportTitle={builderMetadata?.name}
224
280
  filter={sessionPayload?.externalFilter ?? filter}
225
281
  columnsConfig={columnsConfig ?? []}
@@ -78,7 +78,7 @@ function CollapseToggle({ collapsed, onClick, side }) {
78
78
  function ViewBuilderContent({ isDark, onToggleTheme }) {
79
79
  const router = useRouter()
80
80
  const { id } = router.query
81
- const { setSchema, setViewMetaData } = useBuilder()
81
+ const { loadSchema, setViewMetaData } = useBuilder()
82
82
  const [leftCollapsed, setLeftCollapsed] = useState(false)
83
83
  const [rightCollapsed, setRightCollapsed] = useState(false)
84
84
 
@@ -90,7 +90,7 @@ function ViewBuilderContent({ isDark, onToggleTheme }) {
90
90
  if (response) {
91
91
  const parsed = JSON.parse(response.data.value)
92
92
  // Backfill dialogs array for schemas saved before the dialogs feature
93
- setSchema({ dialogs: [], ...parsed })
93
+ loadSchema({ dialogs: [], ...parsed })
94
94
  setViewMetaData({
95
95
  id: response.data.id,
96
96
  title: response.data.title,
@@ -4,43 +4,43 @@ export const ReportBuilderEndpoints = {
4
4
  Post: {
5
5
  GetModels: {
6
6
  group: 'ReportBuilder', name: 'GetModels',
7
- URL: 'ReportingModule/ReportBuilder/GetTypes',
7
+ URL: 'ReportBuilder/GetTypes',
8
8
  ContentType: ContentTypes.Json,
9
9
  DataType: DataTypes.Params
10
10
  },
11
11
  GetModelFields: {
12
12
  group: 'ReportBuilder', name: 'GetModelFields',
13
- URL: 'ReportingModule/ReportBuilder/GetModelFields',
13
+ URL: 'ReportBuilder/GetModelFields',
14
14
  ContentType: ContentTypes.Json,
15
15
  DataType: DataTypes.Params
16
16
  },
17
17
  GenericGet: {
18
18
  group: 'ReportBuilder', name: 'GenericGet',
19
- URL: 'ReportingModule/ReportBuilder/GenericGet',
19
+ URL: 'ReportBuilder/GenericGet',
20
20
  ContentType: ContentTypes.Json,
21
21
  DataType: DataTypes.Body
22
22
  },
23
23
  GetRawColumns: {
24
24
  group: 'ReportBuilder', name: 'GetRawColumns',
25
- URL: 'ReportingModule/ReportBuilder/GetRawColumns',
25
+ URL: 'ReportBuilder/GetRawColumns',
26
26
  ContentType: ContentTypes.Json,
27
27
  DataType: DataTypes.Body
28
28
  },
29
29
  GetAllReports: {
30
30
  group: 'ReportBuilder', name: 'GetAllReports',
31
- URL: 'ReportingModule/ReportBuilder/GetAllReports',
31
+ URL: 'ReportBuilder/GetAllReports',
32
32
  ContentType: ContentTypes.Json,
33
33
  DataType: DataTypes.Body
34
34
  },
35
35
  GetReportDetails: {
36
36
  group: 'ReportBuilder', name: 'GetReportDetails',
37
- URL: 'ReportingModule/ReportBuilder/GetReportDetails',
37
+ URL: 'ReportBuilder/GetReportDetails',
38
38
  ContentType: ContentTypes.Json,
39
39
  DataType: DataTypes.Body
40
40
  },
41
41
  AddUpdate: {
42
42
  group: 'ReportBuilder', name: 'AddUpdate',
43
- URL: 'ReportingModule/ReportBuilder/AddUpdate',
43
+ URL: 'ReportBuilder/AddUpdate',
44
44
  ContentType: ContentTypes.Json,
45
45
  DataType: DataTypes.Body
46
46
  },
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Session log — captures every successful save of a UI Builder or Print
3
+ * Builder schema while developers use the tool. The captured entries
4
+ * become future training data for the AI assistant.
5
+ *
6
+ * Storage:
7
+ * localStorage key rbb:sessionLogs:v1
8
+ * value JSON array of entries (newest first)
9
+ *
10
+ * Entry shape:
11
+ * {
12
+ * id: string, // unique
13
+ * createdAt: string, // ISO timestamp
14
+ * builder: 'ui' | 'print',
15
+ * trigger: string, // 'save' | future: 'manual', 'auto'
16
+ * title: string, // saved view title (for display only)
17
+ * viewMetaData: object, // builder-specific id/group/etc.
18
+ * schema: object, // the actual saved schema
19
+ * prompt: string, // empty by default — filled later by the
20
+ * // developer to label the example for
21
+ * // training (e.g. the natural-language
22
+ * // instruction the schema corresponds to).
23
+ * notes: string, // optional free-form developer notes
24
+ * }
25
+ */
26
+
27
+ const STORAGE_KEY = 'rbb:sessionLogs:v1'
28
+
29
+ // ── Internal: pub/sub so the dialog re-renders on changes ────────────────────
30
+ const listeners = new Set()
31
+ function notify() { listeners.forEach(fn => { try { fn() } catch (_) {} }) }
32
+
33
+ export function subscribe(fn) {
34
+ listeners.add(fn)
35
+ return () => listeners.delete(fn)
36
+ }
37
+
38
+ // ── Internal: read/write ─────────────────────────────────────────────────────
39
+ function safeRead() {
40
+ try {
41
+ const raw = typeof window !== 'undefined' ? window.localStorage.getItem(STORAGE_KEY) : null
42
+ if (!raw) return []
43
+ const parsed = JSON.parse(raw)
44
+ return Array.isArray(parsed) ? parsed : []
45
+ } catch (e) {
46
+ console.warn('[sessionLog] failed to read storage:', e)
47
+ return []
48
+ }
49
+ }
50
+
51
+ function safeWrite(entries) {
52
+ try {
53
+ if (typeof window === 'undefined') return
54
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(entries))
55
+ notify()
56
+ } catch (e) {
57
+ console.warn('[sessionLog] failed to write storage (likely quota):', e)
58
+ }
59
+ }
60
+
61
+ function uid() {
62
+ try { return `log_${crypto.randomUUID()}` }
63
+ catch { return `log_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}` }
64
+ }
65
+
66
+ // ── Public API ───────────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Append a new entry. Returns the created entry.
70
+ *
71
+ * @param {Object} params
72
+ * @param {'ui'|'print'} params.builder
73
+ * @param {string} [params.trigger='save']
74
+ * @param {string} [params.title='']
75
+ * @param {Object} [params.viewMetaData]
76
+ * @param {Object} params.schema - the actual schema being logged
77
+ * @param {string} [params.prompt='']
78
+ * @param {string} [params.notes='']
79
+ */
80
+ export function logSchema({
81
+ builder,
82
+ trigger = 'save',
83
+ title = '',
84
+ viewMetaData = {},
85
+ schema,
86
+ prompt = '',
87
+ notes = ''
88
+ }) {
89
+ if (!schema) {
90
+ console.warn('[sessionLog] refusing to log empty schema')
91
+ return null
92
+ }
93
+ const entry = {
94
+ id: uid(),
95
+ createdAt: new Date().toISOString(),
96
+ builder,
97
+ trigger,
98
+ title,
99
+ viewMetaData,
100
+ // deep clone via JSON to detach from any reactive state
101
+ schema: JSON.parse(JSON.stringify(schema)),
102
+ prompt,
103
+ notes
104
+ }
105
+ const next = [entry, ...safeRead()]
106
+ safeWrite(next)
107
+ return entry
108
+ }
109
+
110
+ /** All entries, newest first. */
111
+ export function listEntries() {
112
+ return safeRead()
113
+ }
114
+
115
+ export function getEntry(id) {
116
+ return safeRead().find(e => e.id === id) ?? null
117
+ }
118
+
119
+ /** Patch an entry — typically used to fill in `prompt` / `notes`. */
120
+ export function updateEntry(id, patch) {
121
+ const entries = safeRead()
122
+ const idx = entries.findIndex(e => e.id === id)
123
+ if (idx === -1) return null
124
+ const updated = { ...entries[idx], ...patch, id, createdAt: entries[idx].createdAt }
125
+ entries[idx] = updated
126
+ safeWrite(entries)
127
+ return updated
128
+ }
129
+
130
+ export function deleteEntry(id) {
131
+ const next = safeRead().filter(e => e.id !== id)
132
+ safeWrite(next)
133
+ }
134
+
135
+ export function clearAll() {
136
+ safeWrite([])
137
+ }
138
+
139
+ /**
140
+ * Trigger a JSON file download of all entries.
141
+ * Filename: rbb-session-logs-YYYY-MM-DD.json
142
+ */
143
+ export function exportJSON() {
144
+ if (typeof window === 'undefined') return
145
+ const entries = safeRead()
146
+ const blob = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' })
147
+ const url = URL.createObjectURL(blob)
148
+ const a = document.createElement('a')
149
+ const stamp = new Date().toISOString().slice(0, 10)
150
+ a.href = url
151
+ a.download = `rbb-session-logs-${stamp}.json`
152
+ document.body.appendChild(a)
153
+ a.click()
154
+ document.body.removeChild(a)
155
+ setTimeout(() => URL.revokeObjectURL(url), 0)
156
+ }
157
+
158
+ /**
159
+ * Bulk import (e.g. restoring from a backup). Replaces existing entries.
160
+ */
161
+ export function importJSON(json) {
162
+ let parsed
163
+ try {
164
+ parsed = typeof json === 'string' ? JSON.parse(json) : json
165
+ } catch (e) {
166
+ throw new Error('Invalid JSON')
167
+ }
168
+ if (!Array.isArray(parsed)) throw new Error('Expected an array of entries')
169
+ safeWrite(parsed)
170
+ return parsed.length
171
+ }