dynim-core 1.0.0

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.
Files changed (86) hide show
  1. package/README.md +290 -0
  2. package/dist/builder/ai-prompt-popover.d.ts +26 -0
  3. package/dist/builder/ai-prompt-popover.d.ts.map +1 -0
  4. package/dist/builder/ai-prompt-popover.js +180 -0
  5. package/dist/builder/builder-client.d.ts +48 -0
  6. package/dist/builder/builder-client.d.ts.map +1 -0
  7. package/dist/builder/builder-client.js +157 -0
  8. package/dist/builder/builder.d.ts +41 -0
  9. package/dist/builder/builder.d.ts.map +1 -0
  10. package/dist/builder/builder.js +537 -0
  11. package/dist/builder/bundle-manager.d.ts +60 -0
  12. package/dist/builder/bundle-manager.d.ts.map +1 -0
  13. package/dist/builder/bundle-manager.js +357 -0
  14. package/dist/builder/classifier/classname-analyzer.d.ts +6 -0
  15. package/dist/builder/classifier/classname-analyzer.d.ts.map +1 -0
  16. package/dist/builder/classifier/classname-analyzer.js +107 -0
  17. package/dist/builder/classifier/index.d.ts +20 -0
  18. package/dist/builder/classifier/index.d.ts.map +1 -0
  19. package/dist/builder/classifier/index.js +181 -0
  20. package/dist/builder/classifier/semantic-analyzer.d.ts +24 -0
  21. package/dist/builder/classifier/semantic-analyzer.d.ts.map +1 -0
  22. package/dist/builder/classifier/semantic-analyzer.js +94 -0
  23. package/dist/builder/classifier/size-analyzer.d.ts +7 -0
  24. package/dist/builder/classifier/size-analyzer.d.ts.map +1 -0
  25. package/dist/builder/classifier/size-analyzer.js +120 -0
  26. package/dist/builder/classifier/visual-analyzer.d.ts +6 -0
  27. package/dist/builder/classifier/visual-analyzer.d.ts.map +1 -0
  28. package/dist/builder/classifier/visual-analyzer.js +158 -0
  29. package/dist/builder/client.d.ts +22 -0
  30. package/dist/builder/client.d.ts.map +1 -0
  31. package/dist/builder/client.js +54 -0
  32. package/dist/builder/code-client.d.ts +101 -0
  33. package/dist/builder/code-client.d.ts.map +1 -0
  34. package/dist/builder/code-client.js +418 -0
  35. package/dist/builder/diff-state.d.ts +24 -0
  36. package/dist/builder/diff-state.d.ts.map +1 -0
  37. package/dist/builder/diff-state.js +134 -0
  38. package/dist/builder/dom-scanner.d.ts +20 -0
  39. package/dist/builder/dom-scanner.d.ts.map +1 -0
  40. package/dist/builder/dom-scanner.js +102 -0
  41. package/dist/builder/drag-engine.d.ts +41 -0
  42. package/dist/builder/drag-engine.d.ts.map +1 -0
  43. package/dist/builder/drag-engine.js +686 -0
  44. package/dist/builder/editor-overlays.d.ts +31 -0
  45. package/dist/builder/editor-overlays.d.ts.map +1 -0
  46. package/dist/builder/editor-overlays.js +202 -0
  47. package/dist/builder/editor-state.d.ts +50 -0
  48. package/dist/builder/editor-state.d.ts.map +1 -0
  49. package/dist/builder/editor-state.js +132 -0
  50. package/dist/builder/element-utils.d.ts +43 -0
  51. package/dist/builder/element-utils.d.ts.map +1 -0
  52. package/dist/builder/element-utils.js +227 -0
  53. package/dist/builder/fiber-capture.d.ts +28 -0
  54. package/dist/builder/fiber-capture.d.ts.map +1 -0
  55. package/dist/builder/fiber-capture.js +264 -0
  56. package/dist/builder/freeze-overlay.d.ts +26 -0
  57. package/dist/builder/freeze-overlay.d.ts.map +1 -0
  58. package/dist/builder/freeze-overlay.js +213 -0
  59. package/dist/builder/history-state.d.ts +41 -0
  60. package/dist/builder/history-state.d.ts.map +1 -0
  61. package/dist/builder/history-state.js +76 -0
  62. package/dist/builder/index.d.ts +62 -0
  63. package/dist/builder/index.d.ts.map +1 -0
  64. package/dist/builder/index.js +92 -0
  65. package/dist/builder/state.d.ts +27 -0
  66. package/dist/builder/state.d.ts.map +1 -0
  67. package/dist/builder/state.js +50 -0
  68. package/dist/builder/style-applier.d.ts +61 -0
  69. package/dist/builder/style-applier.d.ts.map +1 -0
  70. package/dist/builder/style-applier.js +311 -0
  71. package/dist/builder/tree-state.d.ts +71 -0
  72. package/dist/builder/tree-state.d.ts.map +1 -0
  73. package/dist/builder/tree-state.js +168 -0
  74. package/dist/builder/widget.d.ts +29 -0
  75. package/dist/builder/widget.d.ts.map +1 -0
  76. package/dist/builder/widget.js +181 -0
  77. package/dist/index.d.ts +11 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +12 -0
  80. package/package.json +25 -0
  81. package/src/styles/base.css +378 -0
  82. package/src/styles/builder.css +422 -0
  83. package/src/styles/editor.css +131 -0
  84. package/src/styles/themes/dark.css +24 -0
  85. package/src/styles/themes/light.css +21 -0
  86. package/src/styles/variables.css +63 -0
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # dynim-core
2
+
3
+ Framework-agnostic TypeScript library for visual page building, AI chat, and code generation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install dynim-core
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Visual Page Builder** - Drag-and-drop editing with undo/redo history
14
+ - **AI Chat Integration** - SSE-based streaming chat with element context awareness
15
+ - **Code Generation** - Flexcode integration for AI-powered code changes
16
+ - **Element Classification** - Semantic, visual, and size analysis of DOM elements
17
+ - **DOM Utilities** - Tree scanning, stable IDs, selector generation
18
+
19
+ ## Quick Start
20
+
21
+ ### Chat Widget
22
+
23
+ ```typescript
24
+ import { createState, createClient, createWidget } from 'dynim-core';
25
+
26
+ const state = createState();
27
+ const client = createClient({
28
+ endpoint: '/api/chat',
29
+ apiKey: 'your-api-key',
30
+ onMessage: (data) => state.updateMessage(data.id, data.text),
31
+ onError: (error) => console.error(error),
32
+ });
33
+
34
+ const widget = createWidget({
35
+ state,
36
+ client,
37
+ position: 'bottom-right', // or 'inline'
38
+ });
39
+
40
+ widget.mount(document.body);
41
+ ```
42
+
43
+ ### Visual Builder
44
+
45
+ ```typescript
46
+ import { createBuilder, createBuilderClient } from 'dynim-core';
47
+
48
+ const builderClient = createBuilderClient({
49
+ apiBase: 'https://api.example.com',
50
+ sessionToken: 'jwt-token',
51
+ onMessage: (msg) => console.log('AI response:', msg),
52
+ onError: (err) => console.error(err),
53
+ });
54
+
55
+ const builder = createBuilder({
56
+ contentRoot: document.getElementById('app'),
57
+ client: builderClient,
58
+ });
59
+
60
+ // Enter edit mode
61
+ builder.enter();
62
+
63
+ // Save changes
64
+ await builder.save();
65
+
66
+ // Exit without saving
67
+ builder.exit();
68
+ ```
69
+
70
+ ### Code Generation (Flexcode)
71
+
72
+ ```typescript
73
+ import { createCodeClient } from 'dynim-core';
74
+
75
+ const codeClient = createCodeClient({
76
+ apiBase: 'https://api.example.com',
77
+ sessionToken: 'jwt-token',
78
+ onMessage: (event) => {
79
+ switch (event.type) {
80
+ case 'text':
81
+ console.log('Response:', event.content);
82
+ break;
83
+ case 'thinking':
84
+ console.log('Thinking:', event.content);
85
+ break;
86
+ case 'edit':
87
+ console.log('Code edit:', event.edit);
88
+ break;
89
+ case 'done':
90
+ console.log('Complete');
91
+ break;
92
+ }
93
+ },
94
+ });
95
+
96
+ // Send code generation request
97
+ await codeClient.sendCode('project-id', 'Add a dark mode toggle');
98
+
99
+ // Get pending edits
100
+ const edits = codeClient.getEdits();
101
+
102
+ // Save or abandon
103
+ await codeClient.saveCode('project-id');
104
+ await codeClient.abandonCode('project-id');
105
+ ```
106
+
107
+ ## API Reference
108
+
109
+ ### State Management
110
+
111
+ ```typescript
112
+ createState(): StateStore
113
+
114
+ interface StateStore {
115
+ getState(): ChatState;
116
+ setState(state: Partial<ChatState>): void;
117
+ subscribe(listener: (state: ChatState) => void): () => void;
118
+ addMessage(message: Message): void;
119
+ updateMessage(id: string, text: string): void;
120
+ clearMessages(): void;
121
+ }
122
+ ```
123
+
124
+ ### Streaming Client
125
+
126
+ ```typescript
127
+ createClient(config: ClientConfig): StreamClient
128
+
129
+ interface ClientConfig {
130
+ endpoint: string;
131
+ apiKey?: string;
132
+ onMessage: (data: MessageData) => void;
133
+ onError?: (error: Error) => void;
134
+ }
135
+
136
+ interface StreamClient {
137
+ send(message: string): Promise<void>;
138
+ }
139
+ ```
140
+
141
+ ### Builder
142
+
143
+ ```typescript
144
+ createBuilder(config: BuilderConfig): Builder
145
+
146
+ interface BuilderConfig {
147
+ contentRoot?: HTMLElement;
148
+ client: BuilderClient;
149
+ }
150
+
151
+ interface Builder {
152
+ enter(): void;
153
+ exit(): void;
154
+ save(): Promise<void>;
155
+ isActive(): boolean;
156
+ getChanges(): DiffEntry[];
157
+ getEditorState(): EditorState;
158
+ getTreeState(): TreeState;
159
+ destroy(): void;
160
+ }
161
+ ```
162
+
163
+ ### Builder Client
164
+
165
+ ```typescript
166
+ createBuilderClient(config: BuilderClientConfig): BuilderClient
167
+
168
+ interface BuilderClientConfig {
169
+ apiBase?: string;
170
+ sessionToken?: string;
171
+ refreshToken?: string;
172
+ getSession?: () => Promise<{ token: string }>;
173
+ onMessage?: (data: BuilderMessage) => void;
174
+ onError?: (error: Error) => void;
175
+ }
176
+
177
+ interface BuilderClient {
178
+ sendChat(message: string): Promise<void>;
179
+ sendElementChat(message: string, element: HTMLElement): Promise<void>;
180
+ saveDiffs(diffs: DiffEntry[]): Promise<void>;
181
+ setSessionToken(token: string): void;
182
+ isAuthenticated(): boolean;
183
+ }
184
+ ```
185
+
186
+ ### Code Client
187
+
188
+ ```typescript
189
+ createCodeClient(config: CodeClientConfig): CodeClient
190
+
191
+ interface CodeClientConfig {
192
+ apiBase?: string;
193
+ sessionToken?: string;
194
+ getSession?: () => Promise<{ token: string }>;
195
+ onMessage?: (event: CodeEvent) => void;
196
+ }
197
+
198
+ interface CodeClient {
199
+ sendCode(projectId: string, prompt: string): Promise<void>;
200
+ saveCode(projectId: string): Promise<void>;
201
+ abandonCode(projectId: string): Promise<void>;
202
+ getEdits(): CodeEdit[];
203
+ }
204
+
205
+ type CodeEvent =
206
+ | { type: 'text'; content: string }
207
+ | { type: 'thinking'; content: string }
208
+ | { type: 'edit'; edit: CodeEdit }
209
+ | { type: 'question'; content: string }
210
+ | { type: 'done' }
211
+ | { type: 'error'; content: string };
212
+ ```
213
+
214
+ ### DOM Utilities
215
+
216
+ ```typescript
217
+ // Scan DOM tree
218
+ scanDOM(root: HTMLElement): DOMNode[]
219
+
220
+ // Create debounced scanner
221
+ createDebouncedScanner(callback: (nodes: DOMNode[]) => void): Scanner
222
+
223
+ // Element queries
224
+ getElementAtPath(root: HTMLElement, path: number[]): HTMLElement | null
225
+ queryElements(root: HTMLElement, selector: string): HTMLElement[]
226
+ generateSelector(element: HTMLElement): string
227
+
228
+ // Stable IDs for tracking elements across changes
229
+ getStableId(element: HTMLElement): string
230
+ findByStableId(root: HTMLElement, id: string): HTMLElement | null
231
+
232
+ // Build element context for AI
233
+ buildElementIdentifier(element: HTMLElement): ElementIdentifier
234
+ ```
235
+
236
+ ### Element Classifier
237
+
238
+ ```typescript
239
+ createClassifier(): Classifier
240
+
241
+ interface Classifier {
242
+ classify(element: HTMLElement): Classification;
243
+ }
244
+
245
+ interface Classification {
246
+ semantic: SemanticInfo; // HTML tag, ARIA roles, attributes
247
+ classes: ClassInfo; // CSS class patterns
248
+ visual: VisualInfo; // Colors, fonts, spacing
249
+ size: SizeInfo; // Dimensions, viewport relation
250
+ }
251
+ ```
252
+
253
+ ## Styling
254
+
255
+ Import base styles for the chat widget and builder UI:
256
+
257
+ ```typescript
258
+ import 'dynim-core/styles/base.css';
259
+ import 'dynim-core/styles/builder.css';
260
+ import 'dynim-core/styles/editor.css';
261
+
262
+ // Themes
263
+ import 'dynim-core/styles/themes/light.css';
264
+ import 'dynim-core/styles/themes/dark.css';
265
+ ```
266
+
267
+ ## Browser Global API
268
+
269
+ For non-module usage, `dynim-core` exposes global helpers:
270
+
271
+ ```html
272
+ <script src="dynim-core/dist/index.js"></script>
273
+ <script>
274
+ // Quick chatbot setup
275
+ window.Chatbot.init({
276
+ endpoint: '/api/chat',
277
+ position: 'bottom-right',
278
+ });
279
+
280
+ // Quick builder setup
281
+ window.Builder.create({
282
+ apiBase: 'https://api.example.com',
283
+ sessionToken: 'jwt-token',
284
+ });
285
+ </script>
286
+ ```
287
+
288
+ ## License
289
+
290
+ MIT
@@ -0,0 +1,26 @@
1
+ /**
2
+ * AI Prompt Popover - appears below selected element
3
+ * Allows user to type commands to modify the selected element
4
+ */
5
+ export interface AIPromptPopoverConfig {
6
+ onSubmit?: (prompt: string) => void;
7
+ onClose?: () => void;
8
+ }
9
+ export interface AIPromptPopover {
10
+ element: HTMLDivElement;
11
+ mount: () => void;
12
+ unmount: () => void;
13
+ show: (rect: DOMRect | {
14
+ x?: number;
15
+ left?: number;
16
+ y?: number;
17
+ top?: number;
18
+ width: number;
19
+ height: number;
20
+ } | null) => void;
21
+ hide: () => void;
22
+ updatePosition: () => void;
23
+ isVisible: () => boolean;
24
+ }
25
+ export declare function createAIPromptPopover(config?: AIPromptPopoverConfig): AIPromptPopover;
26
+ //# sourceMappingURL=ai-prompt-popover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-prompt-popover.d.ts","sourceRoot":"","sources":["../../src/builder/ai-prompt-popover.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9H,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,MAAM,GAAE,qBAA0B,GAAG,eAAe,CAsMzF"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * AI Prompt Popover - appears below selected element
3
+ * Allows user to type commands to modify the selected element
4
+ */
5
+ export function createAIPromptPopover(config = {}) {
6
+ const { onSubmit, onClose } = config;
7
+ const popover = document.createElement('div');
8
+ popover.id = '__ai-prompt-popover__';
9
+ popover.style.cssText = 'display: none;';
10
+ popover.innerHTML = `
11
+ <form class="ai-prompt-form">
12
+ <div class="ai-prompt-icon">
13
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
14
+ <path d="M12 3v18M3 12h18M5.5 5.5l13 13M18.5 5.5l-13 13" />
15
+ </svg>
16
+ </div>
17
+ <input
18
+ type="text"
19
+ class="ai-prompt-input"
20
+ placeholder="Ask AI to modify this element..."
21
+ />
22
+ <button type="submit" class="ai-prompt-submit" disabled>
23
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
24
+ <line x1="22" y1="2" x2="11" y2="13" />
25
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
26
+ </svg>
27
+ </button>
28
+ </form>
29
+ `;
30
+ const style = document.createElement('style');
31
+ style.id = '__ai-prompt-popover-styles__';
32
+ style.textContent = `
33
+ #__ai-prompt-popover__ {
34
+ position: fixed;
35
+ z-index: 10003;
36
+ pointer-events: auto;
37
+ }
38
+
39
+ .ai-prompt-form {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 8px;
43
+ background: #18181b;
44
+ border-radius: 10px;
45
+ padding: 6px 10px;
46
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(255, 255, 255, 0.08);
47
+ width: 320px;
48
+ }
49
+
50
+ .ai-prompt-icon {
51
+ color: #a78bfa;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ .ai-prompt-input {
59
+ flex: 1;
60
+ background: transparent;
61
+ border: none;
62
+ outline: none;
63
+ color: #fafafa;
64
+ font-size: 14px;
65
+ font-family: inherit;
66
+ padding: 6px 0;
67
+ }
68
+
69
+ .ai-prompt-input::placeholder {
70
+ color: #71717a;
71
+ }
72
+
73
+ .ai-prompt-submit {
74
+ background: #7c3aed;
75
+ border: none;
76
+ border-radius: 6px;
77
+ padding: 6px 8px;
78
+ cursor: pointer;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ color: white;
83
+ transition: opacity 0.15s;
84
+ }
85
+
86
+ .ai-prompt-submit:disabled {
87
+ opacity: 0.5;
88
+ cursor: not-allowed;
89
+ }
90
+
91
+ .ai-prompt-submit:not(:disabled):hover {
92
+ background: #6d28d9;
93
+ }
94
+ `;
95
+ const form = popover.querySelector('.ai-prompt-form');
96
+ const input = popover.querySelector('.ai-prompt-input');
97
+ const submitBtn = popover.querySelector('.ai-prompt-submit');
98
+ let currentRect = null;
99
+ input.addEventListener('input', () => {
100
+ submitBtn.disabled = !input.value.trim();
101
+ });
102
+ function handleSubmit() {
103
+ const prompt = input.value.trim();
104
+ if (prompt) {
105
+ onSubmit?.(prompt);
106
+ input.value = '';
107
+ submitBtn.disabled = true;
108
+ }
109
+ }
110
+ form.addEventListener('submit', (e) => {
111
+ e.preventDefault();
112
+ handleSubmit();
113
+ });
114
+ // Backup: handle click directly on submit button
115
+ submitBtn.addEventListener('click', (e) => {
116
+ e.preventDefault();
117
+ handleSubmit();
118
+ });
119
+ input.addEventListener('keydown', (e) => {
120
+ if (e.key === 'Escape') {
121
+ hide();
122
+ onClose?.();
123
+ }
124
+ });
125
+ function mount() {
126
+ document.head.appendChild(style);
127
+ document.body.appendChild(popover);
128
+ }
129
+ function unmount() {
130
+ style.remove();
131
+ popover.remove();
132
+ }
133
+ function show(rect) {
134
+ if (!rect) {
135
+ hide();
136
+ return;
137
+ }
138
+ currentRect = rect;
139
+ let left = Math.max(16, (rect.left ?? rect.x ?? 0) + (rect.width / 2) - 160);
140
+ let top = (rect.top ?? rect.y ?? 0) + rect.height + 8;
141
+ if (top + 60 > window.innerHeight) {
142
+ top = (rect.top ?? rect.y ?? 0) - 52;
143
+ }
144
+ if (left + 320 > window.innerWidth) {
145
+ left = window.innerWidth - 336;
146
+ }
147
+ popover.style.cssText = `
148
+ position: fixed;
149
+ display: block;
150
+ left: ${left}px;
151
+ top: ${top}px;
152
+ z-index: 10003;
153
+ pointer-events: auto;
154
+ `;
155
+ setTimeout(() => input.focus(), 0);
156
+ }
157
+ function hide() {
158
+ popover.style.cssText = 'display: none;';
159
+ input.value = '';
160
+ submitBtn.disabled = true;
161
+ currentRect = null;
162
+ }
163
+ function updatePosition() {
164
+ if (currentRect) {
165
+ show(currentRect);
166
+ }
167
+ }
168
+ function isVisible() {
169
+ return popover.style.display !== 'none';
170
+ }
171
+ return {
172
+ element: popover,
173
+ mount,
174
+ unmount,
175
+ show,
176
+ hide,
177
+ updatePosition,
178
+ isVisible
179
+ };
180
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Builder Client - Handles visual builder operations
3
+ *
4
+ * NOTE: For AI chat/code operations, use code-client.ts instead.
5
+ * This client handles visual builder-specific operations:
6
+ * - saveDiffs: Save visual DOM changes
7
+ * - preview: Generate preview URL
8
+ * - exit: Notify server of builder exit
9
+ *
10
+ * Authentication uses JWT session tokens.
11
+ */
12
+ import type { DiffEntry } from './diff-state';
13
+ export interface BuilderClientConfig {
14
+ apiBase?: string;
15
+ /** JWT session token for authentication */
16
+ sessionToken?: string;
17
+ /** Refresh token for getting new session tokens */
18
+ refreshToken?: string;
19
+ /** Function to fetch a new session when current one expires */
20
+ getSession?: () => Promise<{
21
+ token: string;
22
+ refreshToken?: string;
23
+ }>;
24
+ onError?: (error: Error) => void;
25
+ /** Called when session is refreshed */
26
+ onSessionRefresh?: (token: string) => void;
27
+ /** Called when authentication fails */
28
+ onAuthError?: (error: Error) => void;
29
+ }
30
+ export interface BuilderClient {
31
+ /** Save visual DOM diffs to backend */
32
+ saveDiffs: (pageId: string, diffs: DiffEntry[]) => Promise<unknown>;
33
+ /** Generate preview URL for current changes */
34
+ preview: (pageId: string, diffs?: DiffEntry[]) => Promise<{
35
+ success: boolean;
36
+ previewUrl?: string;
37
+ message?: string;
38
+ }>;
39
+ /** Notify server of builder exit */
40
+ exit: (pageId: string, discard?: boolean) => Promise<{
41
+ success: boolean;
42
+ message?: string;
43
+ }>;
44
+ setSessionToken: (token: string, refreshToken?: string) => void;
45
+ isAuthenticated: () => boolean;
46
+ }
47
+ export declare function createBuilderClient(config?: BuilderClientConfig): BuilderClient;
48
+ //# sourceMappingURL=builder-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder-client.d.ts","sourceRoot":"","sources":["../../src/builder/builder-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,uCAAuC;IACvC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,uCAAuC;IACvC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACtC;AAGD,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpE,+CAA+C;IAC/C,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvH,oCAAoC;IACpC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7F,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,eAAe,EAAE,MAAM,OAAO,CAAC;CAChC;AAED,wBAAgB,mBAAmB,CAAC,MAAM,GAAE,mBAAwB,GAAG,aAAa,CAyKnF"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Builder Client - Handles visual builder operations
3
+ *
4
+ * NOTE: For AI chat/code operations, use code-client.ts instead.
5
+ * This client handles visual builder-specific operations:
6
+ * - saveDiffs: Save visual DOM changes
7
+ * - preview: Generate preview URL
8
+ * - exit: Notify server of builder exit
9
+ *
10
+ * Authentication uses JWT session tokens.
11
+ */
12
+ const DEFAULT_API_BASE = 'http://localhost:8080';
13
+ export function createBuilderClient(config = {}) {
14
+ const { apiBase = DEFAULT_API_BASE, sessionToken: initialSessionToken, refreshToken: initialRefreshToken, getSession, onError, onSessionRefresh, onAuthError } = config;
15
+ let currentSessionToken = initialSessionToken;
16
+ let currentRefreshToken = initialRefreshToken;
17
+ let isRefreshing = false;
18
+ /**
19
+ * Get a valid session token, refreshing if necessary
20
+ */
21
+ async function getValidToken() {
22
+ if (currentSessionToken) {
23
+ // Check if token is expired (JWT decode)
24
+ try {
25
+ const payload = JSON.parse(atob(currentSessionToken.split('.')[1]));
26
+ const exp = payload.exp * 1000; // Convert to ms
27
+ if (Date.now() < exp - 60000) { // 1 minute buffer
28
+ return currentSessionToken;
29
+ }
30
+ }
31
+ catch {
32
+ // Token parse failed, try to refresh
33
+ }
34
+ }
35
+ // Try to refresh
36
+ if (currentRefreshToken && !isRefreshing) {
37
+ isRefreshing = true;
38
+ try {
39
+ const response = await fetch(`${apiBase}/api/sessions/refresh`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ refresh_token: currentRefreshToken })
43
+ });
44
+ if (response.ok) {
45
+ const data = await response.json();
46
+ currentSessionToken = data.token;
47
+ onSessionRefresh?.(data.token);
48
+ isRefreshing = false;
49
+ return currentSessionToken ?? null;
50
+ }
51
+ }
52
+ catch (err) {
53
+ console.error('[BuilderClient] Failed to refresh session:', err);
54
+ }
55
+ isRefreshing = false;
56
+ }
57
+ // Try to get new session from callback
58
+ if (getSession) {
59
+ try {
60
+ const session = await getSession();
61
+ currentSessionToken = session.token;
62
+ if (session.refreshToken) {
63
+ currentRefreshToken = session.refreshToken;
64
+ }
65
+ return currentSessionToken;
66
+ }
67
+ catch (err) {
68
+ console.error('[BuilderClient] Failed to get session:', err);
69
+ onAuthError?.(err);
70
+ }
71
+ }
72
+ onAuthError?.(new Error('No valid session token available'));
73
+ return null;
74
+ }
75
+ /**
76
+ * Make an authenticated request
77
+ */
78
+ async function authenticatedFetch(url, options = {}) {
79
+ const token = await getValidToken();
80
+ if (!token) {
81
+ throw new Error('Not authenticated');
82
+ }
83
+ const headers = new Headers(options.headers);
84
+ headers.set('Authorization', `Bearer ${token}`);
85
+ return fetch(url, { ...options, headers });
86
+ }
87
+ async function saveDiffs(pageId, diffs) {
88
+ try {
89
+ const response = await authenticatedFetch(`${apiBase}/api/builder/save`, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ pageId, diffs })
93
+ });
94
+ if (!response.ok) {
95
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
96
+ }
97
+ return response.json();
98
+ }
99
+ catch (error) {
100
+ console.error('[BuilderClient] Save error:', error);
101
+ onError?.(error);
102
+ throw error;
103
+ }
104
+ }
105
+ async function preview(pageId, diffs) {
106
+ try {
107
+ const response = await authenticatedFetch(`${apiBase}/api/builder/preview`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ pageId, diffs: diffs || [] })
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
114
+ }
115
+ return response.json();
116
+ }
117
+ catch (error) {
118
+ console.error('[BuilderClient] Preview error:', error);
119
+ onError?.(error);
120
+ throw error;
121
+ }
122
+ }
123
+ async function exit(pageId, discard) {
124
+ try {
125
+ const response = await authenticatedFetch(`${apiBase}/api/builder/exit`, {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({ pageId, discard: discard || false })
129
+ });
130
+ if (!response.ok) {
131
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
132
+ }
133
+ return response.json();
134
+ }
135
+ catch (error) {
136
+ console.error('[BuilderClient] Exit error:', error);
137
+ onError?.(error);
138
+ throw error;
139
+ }
140
+ }
141
+ /**
142
+ * Set/update the session token
143
+ */
144
+ function setSessionToken(token, refreshToken) {
145
+ currentSessionToken = token;
146
+ if (refreshToken) {
147
+ currentRefreshToken = refreshToken;
148
+ }
149
+ }
150
+ return {
151
+ saveDiffs,
152
+ preview,
153
+ exit,
154
+ setSessionToken,
155
+ isAuthenticated: () => !!currentSessionToken,
156
+ };
157
+ }