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.
- package/README.md +290 -0
- package/dist/builder/ai-prompt-popover.d.ts +26 -0
- package/dist/builder/ai-prompt-popover.d.ts.map +1 -0
- package/dist/builder/ai-prompt-popover.js +180 -0
- package/dist/builder/builder-client.d.ts +48 -0
- package/dist/builder/builder-client.d.ts.map +1 -0
- package/dist/builder/builder-client.js +157 -0
- package/dist/builder/builder.d.ts +41 -0
- package/dist/builder/builder.d.ts.map +1 -0
- package/dist/builder/builder.js +537 -0
- package/dist/builder/bundle-manager.d.ts +60 -0
- package/dist/builder/bundle-manager.d.ts.map +1 -0
- package/dist/builder/bundle-manager.js +357 -0
- package/dist/builder/classifier/classname-analyzer.d.ts +6 -0
- package/dist/builder/classifier/classname-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/classname-analyzer.js +107 -0
- package/dist/builder/classifier/index.d.ts +20 -0
- package/dist/builder/classifier/index.d.ts.map +1 -0
- package/dist/builder/classifier/index.js +181 -0
- package/dist/builder/classifier/semantic-analyzer.d.ts +24 -0
- package/dist/builder/classifier/semantic-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/semantic-analyzer.js +94 -0
- package/dist/builder/classifier/size-analyzer.d.ts +7 -0
- package/dist/builder/classifier/size-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/size-analyzer.js +120 -0
- package/dist/builder/classifier/visual-analyzer.d.ts +6 -0
- package/dist/builder/classifier/visual-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/visual-analyzer.js +158 -0
- package/dist/builder/client.d.ts +22 -0
- package/dist/builder/client.d.ts.map +1 -0
- package/dist/builder/client.js +54 -0
- package/dist/builder/code-client.d.ts +101 -0
- package/dist/builder/code-client.d.ts.map +1 -0
- package/dist/builder/code-client.js +418 -0
- package/dist/builder/diff-state.d.ts +24 -0
- package/dist/builder/diff-state.d.ts.map +1 -0
- package/dist/builder/diff-state.js +134 -0
- package/dist/builder/dom-scanner.d.ts +20 -0
- package/dist/builder/dom-scanner.d.ts.map +1 -0
- package/dist/builder/dom-scanner.js +102 -0
- package/dist/builder/drag-engine.d.ts +41 -0
- package/dist/builder/drag-engine.d.ts.map +1 -0
- package/dist/builder/drag-engine.js +686 -0
- package/dist/builder/editor-overlays.d.ts +31 -0
- package/dist/builder/editor-overlays.d.ts.map +1 -0
- package/dist/builder/editor-overlays.js +202 -0
- package/dist/builder/editor-state.d.ts +50 -0
- package/dist/builder/editor-state.d.ts.map +1 -0
- package/dist/builder/editor-state.js +132 -0
- package/dist/builder/element-utils.d.ts +43 -0
- package/dist/builder/element-utils.d.ts.map +1 -0
- package/dist/builder/element-utils.js +227 -0
- package/dist/builder/fiber-capture.d.ts +28 -0
- package/dist/builder/fiber-capture.d.ts.map +1 -0
- package/dist/builder/fiber-capture.js +264 -0
- package/dist/builder/freeze-overlay.d.ts +26 -0
- package/dist/builder/freeze-overlay.d.ts.map +1 -0
- package/dist/builder/freeze-overlay.js +213 -0
- package/dist/builder/history-state.d.ts +41 -0
- package/dist/builder/history-state.d.ts.map +1 -0
- package/dist/builder/history-state.js +76 -0
- package/dist/builder/index.d.ts +62 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +92 -0
- package/dist/builder/state.d.ts +27 -0
- package/dist/builder/state.d.ts.map +1 -0
- package/dist/builder/state.js +50 -0
- package/dist/builder/style-applier.d.ts +61 -0
- package/dist/builder/style-applier.d.ts.map +1 -0
- package/dist/builder/style-applier.js +311 -0
- package/dist/builder/tree-state.d.ts +71 -0
- package/dist/builder/tree-state.d.ts.map +1 -0
- package/dist/builder/tree-state.js +168 -0
- package/dist/builder/widget.d.ts +29 -0
- package/dist/builder/widget.d.ts.map +1 -0
- package/dist/builder/widget.js +181 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/package.json +25 -0
- package/src/styles/base.css +378 -0
- package/src/styles/builder.css +422 -0
- package/src/styles/editor.css +131 -0
- package/src/styles/themes/dark.css +24 -0
- package/src/styles/themes/light.css +21 -0
- 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
|
+
}
|