skema-core 0.1.0 → 0.1.2
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 +98 -49
- package/dist/cli.js +1186 -23
- package/dist/index.d.mts +81 -3
- package/dist/index.d.ts +81 -3
- package/dist/index.js +2388 -991
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2389 -993
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +208 -3
- package/dist/server.d.ts +208 -3
- package/dist/server.js +1012 -143
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +981 -145
- package/dist/server.mjs.map +1 -1
- package/package.json +7 -2
package/dist/server.mjs
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { execSync, spawn } from 'child_process';
|
|
1
|
+
import { execSync, spawn, exec } from 'child_process';
|
|
2
2
|
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
3
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
5
13
|
|
|
6
14
|
// src/lib/utils.ts
|
|
7
15
|
function getGridCellReference(x, y, gridSize = 100) {
|
|
@@ -11,6 +19,204 @@ function getGridCellReference(x, y, gridSize = 100) {
|
|
|
11
19
|
return `${colLabel}${row}`;
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
// src/server/prompts.ts
|
|
23
|
+
var CRITICAL_RULES = `CRITICAL RULES:
|
|
24
|
+
- Do NOT create new files. Only edit existing files.
|
|
25
|
+
- Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line.
|
|
26
|
+
- Ensure all JSX tags are properly closed - every <tag> needs a matching </tag>.
|
|
27
|
+
- Do NOT run any shell commands. No npm, no git, no lint, no build commands. Just edit files.
|
|
28
|
+
- STOP immediately after making the file changes. Do not verify, do not run tests, do not check status.`;
|
|
29
|
+
var JSX_VALIDATION_RULE = `JSX SYNTAX: Every opening tag (<div>, <a>, <span>, <button>) MUST have a matching closing tag. Self-closing tags (<img />, <input />, <br />) must end with />.`;
|
|
30
|
+
function buildFastDomSelectionPrompt(input) {
|
|
31
|
+
const { comment, selector, text, tagName } = input;
|
|
32
|
+
const parts = [];
|
|
33
|
+
if (tagName) {
|
|
34
|
+
parts.push(`<${tagName.toLowerCase()}>`);
|
|
35
|
+
}
|
|
36
|
+
if (selector) {
|
|
37
|
+
parts.push(`selector: ${selector}`);
|
|
38
|
+
}
|
|
39
|
+
if (text) {
|
|
40
|
+
parts.push(`text: "${text.slice(0, 50)}"`);
|
|
41
|
+
}
|
|
42
|
+
const target = parts.length > 0 ? ` (${parts.join(" | ")})` : "";
|
|
43
|
+
return `${comment}${target}. Make the change directly, no explanation needed. ${CRITICAL_RULES} ${JSX_VALIDATION_RULE}`;
|
|
44
|
+
}
|
|
45
|
+
function buildDetailedDomSelectionPrompt(input) {
|
|
46
|
+
const { comment, tagName, selector, text, elementPath, cssClasses, attributes, elements } = input;
|
|
47
|
+
let prompt = `Make this code change: "${comment || "No specific comment provided"}"
|
|
48
|
+
|
|
49
|
+
## Target Element
|
|
50
|
+
- Tag: <${tagName?.toLowerCase() || "unknown"}>`;
|
|
51
|
+
if (selector) {
|
|
52
|
+
prompt += `
|
|
53
|
+
- Selector: ${selector}`;
|
|
54
|
+
}
|
|
55
|
+
if (elementPath) {
|
|
56
|
+
prompt += `
|
|
57
|
+
- DOM Path: ${elementPath}`;
|
|
58
|
+
}
|
|
59
|
+
if (cssClasses) {
|
|
60
|
+
prompt += `
|
|
61
|
+
- CSS Classes: ${cssClasses}`;
|
|
62
|
+
}
|
|
63
|
+
if (attributes && Object.keys(attributes).length > 0) {
|
|
64
|
+
const attrStr = Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(", ");
|
|
65
|
+
prompt += `
|
|
66
|
+
- Attributes: ${attrStr}`;
|
|
67
|
+
}
|
|
68
|
+
if (text) {
|
|
69
|
+
prompt += `
|
|
70
|
+
- Text Content: "${text.slice(0, 150)}"`;
|
|
71
|
+
}
|
|
72
|
+
if (elements && elements.length > 1) {
|
|
73
|
+
prompt += `
|
|
74
|
+
|
|
75
|
+
## Multi-Selection (${elements.length} elements)`;
|
|
76
|
+
elements.slice(0, 5).forEach((el, i) => {
|
|
77
|
+
prompt += `
|
|
78
|
+
${i + 1}. <${el.tagName}> ${el.selector}`;
|
|
79
|
+
if (el.text) prompt += ` - "${el.text.slice(0, 50)}"`;
|
|
80
|
+
});
|
|
81
|
+
if (elements.length > 5) {
|
|
82
|
+
prompt += `
|
|
83
|
+
... and ${elements.length - 5} more`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
prompt += `
|
|
87
|
+
|
|
88
|
+
Make minimal changes. ${CRITICAL_RULES} ${JSX_VALIDATION_RULE}`;
|
|
89
|
+
return prompt;
|
|
90
|
+
}
|
|
91
|
+
function buildGesturePrompt(input) {
|
|
92
|
+
const { comment, gesture, boundingBox } = input;
|
|
93
|
+
return `Make this code change: "${comment || "No specific comment provided"}"
|
|
94
|
+
|
|
95
|
+
Element: gesture: ${gesture || "unknown"} at (${boundingBox?.x || 0}, ${boundingBox?.y || 0})
|
|
96
|
+
|
|
97
|
+
Make minimal changes. ${CRITICAL_RULES} ${JSX_VALIDATION_RULE}`;
|
|
98
|
+
}
|
|
99
|
+
function buildDrawingToCodePrompt(input) {
|
|
100
|
+
const {
|
|
101
|
+
comment = "Create a component based on this drawing",
|
|
102
|
+
boundingBox,
|
|
103
|
+
extractedText,
|
|
104
|
+
gridConfig,
|
|
105
|
+
viewport,
|
|
106
|
+
nearbyElements = [],
|
|
107
|
+
visionDescription
|
|
108
|
+
} = input;
|
|
109
|
+
const gridSize = gridConfig?.size || 100;
|
|
110
|
+
let gridCellRef = "";
|
|
111
|
+
if (boundingBox) {
|
|
112
|
+
gridCellRef = getGridCellReference(boundingBox.x, boundingBox.y, gridSize);
|
|
113
|
+
}
|
|
114
|
+
const positionContext = buildPositionContext(boundingBox, viewport, gridCellRef);
|
|
115
|
+
const nearbyContext = buildNearbyElementsContext(nearbyElements);
|
|
116
|
+
const textContext = extractedText?.trim() ? `
|
|
117
|
+
**Text found in drawing (use as reference if hard to read):**
|
|
118
|
+
${extractedText}` : "";
|
|
119
|
+
const imageNote = buildImageNote(!!input.drawingImage, visionDescription);
|
|
120
|
+
return `Your task is to interpret a user's sketch/wireframe and turn it into code that is integrated in the codebase.
|
|
121
|
+
|
|
122
|
+
## User's Request
|
|
123
|
+
"${comment}"
|
|
124
|
+
|
|
125
|
+
## Drawing Context
|
|
126
|
+
${positionContext}${textContext}${nearbyContext}${imageNote}
|
|
127
|
+
|
|
128
|
+
## Your Process
|
|
129
|
+
1. **Analyze the Sketch:** Understand the visual intent\u2014what UI component does the user want?
|
|
130
|
+
2. **Interpret, Don't Transcribe:** Elevate the low-fidelity drawing into a high-fidelity component. Choose appropriate spacing, colors, and typography that match modern design standards.
|
|
131
|
+
3. **Infer Missing Details:** If something is underspecified, use your expertise to make the best choice. An informed decision is better than an incomplete component.
|
|
132
|
+
|
|
133
|
+
## Implementation Guidelines
|
|
134
|
+
${DRAWING_IMPLEMENTATION_GUIDELINES}
|
|
135
|
+
|
|
136
|
+
Make the changes directly. Insert the UI elements inline at the appropriate location in the page. No explanation needed.
|
|
137
|
+
|
|
138
|
+
## Error Prevention Rules
|
|
139
|
+
${DRAWING_ERROR_PREVENTION_RULES}`;
|
|
140
|
+
}
|
|
141
|
+
var DRAWING_IMPLEMENTATION_GUIDELINES = `- **CRITICAL: DO NOT CREATE ANY NEW FILES. NEVER CREATE NEW FILES. You must ONLY edit existing files.**
|
|
142
|
+
- Add your code directly inline within the existing JSX of the page file - do NOT create separate component files.
|
|
143
|
+
- DO NOT run any shell commands (no npm, git, lint, build, or verification commands). Just edit files and STOP.
|
|
144
|
+
- After making your file edits, you are DONE. Do not run any follow-up commands or checks.
|
|
145
|
+
- Write the UI as inline JSX elements (divs, sections, etc.) directly in the return statement - NOT as a separate component definition
|
|
146
|
+
- Use Tailwind CSS classes for styling (the project uses Tailwind)
|
|
147
|
+
- Do NOT use hardcoded pixel positions or absolute coordinates - integrate naturally with existing page flow
|
|
148
|
+
- Use flexbox, grid, or relative positioning to place the component appropriately
|
|
149
|
+
- If the sketch shows:
|
|
150
|
+
- **Rectangle/box:** Card, container, button, or input field depending on context
|
|
151
|
+
- **Text elements:** Headings, paragraphs, or labels with appropriate hierarchy
|
|
152
|
+
- **Form layout:** Input fields with labels, proper spacing
|
|
153
|
+
- **Icons/shapes:** Use appropriate icons from lucide-react or inline SVGs (but DO NOT add new imports mid-file)
|
|
154
|
+
- **Navigation:** Nav links, menus, or breadcrumbs
|
|
155
|
+
- **Lists:** Ordered/unordered lists or grid layouts
|
|
156
|
+
- Make the UI fit naturally with the existing page design
|
|
157
|
+
- Style it nicely and according to the existing codebase
|
|
158
|
+
- Use semantic HTML and ARIA attributes where appropriate
|
|
159
|
+
- **NEVER add import statements inside JSX or in the middle of a file - all imports must be at the very top of the file**
|
|
160
|
+
- If you need a new import, add it at the TOP of the file with the other imports, then use it in the JSX below`;
|
|
161
|
+
var DRAWING_ERROR_PREVENTION_RULES = `1. **NEVER CREATE NEW FILES** - Do NOT create new component files, utility files, or any other files. Write everything inline in the existing file
|
|
162
|
+
2. You do not need to update package.json or anything, just add / edit the react component.
|
|
163
|
+
3. Do NOT add import statements in the middle of the file or inside JSX - imports go ONLY at the top
|
|
164
|
+
4. Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line or the SkemaWrapper component itself
|
|
165
|
+
5. If you need something that requires an import and it's not already imported, either use an alternative that doesn't need an import, or add the import at the very TOP of the file with the other imports
|
|
166
|
+
6. DONT MAKE ANY CHANGES THAT WOULD RESULT IN A Build Error
|
|
167
|
+
7. **JSX SYNTAX VALIDATION** - ALWAYS ensure every JSX tag is properly closed. Every opening tag like <div>, <a>, <span>, <button> MUST have a matching closing tag </div>, </a>, </span>, </button>. Self-closing tags like <img />, <input />, <br /> must end with />. Before finishing, mentally verify all tag pairs are balanced.`;
|
|
168
|
+
var IMAGE_ANALYSIS_PROMPT = `
|
|
169
|
+
Analyze this UI wireframe sketch in some detail (not too long) for a front-end developer.
|
|
170
|
+
|
|
171
|
+
Describe every element, layout, spacing, icons, and text you see.
|
|
172
|
+
Focus on whats apparent, don't overthink it.
|
|
173
|
+
Mention relative positions and hierarchy.
|
|
174
|
+
Be distinct about what is drawn vs what might be background.
|
|
175
|
+
|
|
176
|
+
Do NOT focus on exact pixel coordinates or absolute positions - describe layouts
|
|
177
|
+
in terms of relative positioning (left/right/top/bottom, centered, evenly spaced, etc.).
|
|
178
|
+
|
|
179
|
+
It is expected that they will be rough draft's / hand-drawn things. Interpret the drawing and its goals as best as you can.
|
|
180
|
+
DO NOT MENTION THAT THINGS HAVE "Hand-sketched" or "Hand-drawn" vibes. Make assumptions of what they were trying to do.
|
|
181
|
+
Just FYI, this gets passed onto a generator to generate the actual code of modern UI componenents.
|
|
182
|
+
`.trim();
|
|
183
|
+
function buildPositionContext(bbox, viewport, gridCellRef) {
|
|
184
|
+
if (!bbox) return "";
|
|
185
|
+
if (viewport) {
|
|
186
|
+
const relX = (bbox.x / viewport.width * 100).toFixed(1);
|
|
187
|
+
const relY = (bbox.y / viewport.height * 100).toFixed(1);
|
|
188
|
+
let context = `**Drawing Location:** Approximately ${relX}% from left, ${relY}% from top of viewport`;
|
|
189
|
+
if (gridCellRef) {
|
|
190
|
+
context += ` (grid cell ${gridCellRef})`;
|
|
191
|
+
}
|
|
192
|
+
return context;
|
|
193
|
+
}
|
|
194
|
+
return `**Drawing Area:** ${Math.round(bbox.width)}\xD7${Math.round(bbox.height)}px`;
|
|
195
|
+
}
|
|
196
|
+
function buildNearbyElementsContext(nearbyElements) {
|
|
197
|
+
if (nearbyElements.length === 0) return "";
|
|
198
|
+
const elementList = nearbyElements.slice(0, 5).map((el) => {
|
|
199
|
+
let desc = `- <${el.tagName.toLowerCase()}>`;
|
|
200
|
+
if (el.text) desc += `: "${el.text.slice(0, 50)}"`;
|
|
201
|
+
if (el.className) desc += ` (class: ${el.className.slice(0, 50)})`;
|
|
202
|
+
desc += ` (${el.selector})`;
|
|
203
|
+
return desc;
|
|
204
|
+
}).join("\n");
|
|
205
|
+
return `
|
|
206
|
+
**Nearby DOM Elements (for placement reference):**
|
|
207
|
+
${elementList}`;
|
|
208
|
+
}
|
|
209
|
+
function buildImageNote(hasImage, visionDescription) {
|
|
210
|
+
let note = hasImage ? "\n**[Drawing image provided as base64 PNG with labeled grid overlay]**" : "";
|
|
211
|
+
if (visionDescription) {
|
|
212
|
+
note += `
|
|
213
|
+
|
|
214
|
+
## Visual Analysis of Drawing
|
|
215
|
+
${visionDescription}`;
|
|
216
|
+
}
|
|
217
|
+
return note;
|
|
218
|
+
}
|
|
219
|
+
|
|
14
220
|
// src/server/gemini-cli.ts
|
|
15
221
|
var annotationSnapshots = /* @__PURE__ */ new Map();
|
|
16
222
|
function createSnapshot(annotationId, cwd) {
|
|
@@ -64,135 +270,50 @@ function revertAnnotation(annotationId, cwd = process.cwd()) {
|
|
|
64
270
|
function getTrackedAnnotations() {
|
|
65
271
|
return Array.from(annotationSnapshots.keys());
|
|
66
272
|
}
|
|
67
|
-
function buildPromptFromAnnotation(annotation,
|
|
273
|
+
function buildPromptFromAnnotation(annotation, _projectContext, options) {
|
|
68
274
|
const fastMode = options?.fastMode ?? true;
|
|
69
275
|
if (annotation.type === "drawing") {
|
|
70
|
-
|
|
276
|
+
const drawingAnnotation = annotation;
|
|
277
|
+
return buildDrawingToCodePrompt({
|
|
278
|
+
comment: drawingAnnotation.comment || "Create a component based on this drawing",
|
|
279
|
+
boundingBox: drawingAnnotation.boundingBox,
|
|
280
|
+
drawingSvg: drawingAnnotation.drawingSvg,
|
|
281
|
+
drawingImage: drawingAnnotation.drawingImage,
|
|
282
|
+
extractedText: drawingAnnotation.extractedText,
|
|
283
|
+
gridConfig: drawingAnnotation.gridConfig,
|
|
284
|
+
viewport: drawingAnnotation.viewport,
|
|
285
|
+
projectStyles: drawingAnnotation.projectStyles,
|
|
286
|
+
nearbyElements: drawingAnnotation.nearbyElements,
|
|
287
|
+
visionDescription: options?.visionDescription
|
|
288
|
+
});
|
|
71
289
|
}
|
|
72
|
-
if (
|
|
73
|
-
const selector = annotation.selector || "";
|
|
74
|
-
const text = annotation.text || "";
|
|
75
|
-
const tag = annotation.tagName?.toLowerCase() || "";
|
|
76
|
-
let target = "";
|
|
77
|
-
if (text) {
|
|
78
|
-
target = `"${text.slice(0, 50)}"`;
|
|
79
|
-
} else if (selector) {
|
|
80
|
-
target = `\`${selector}\``;
|
|
81
|
-
} else if (tag) {
|
|
82
|
-
target = `<${tag}>`;
|
|
83
|
-
}
|
|
84
|
-
return `${annotation.comment}${target ? ` (target: ${target})` : ""}. Make the change directly, no explanation needed. IMPORTANT: Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line.`;
|
|
85
|
-
}
|
|
86
|
-
let prompt = `Make this code change: "${annotation.comment || "No specific comment provided"}"
|
|
87
|
-
|
|
88
|
-
Element: `;
|
|
89
|
-
if (annotation.type === "dom_selection") {
|
|
90
|
-
const domAnnotation = annotation;
|
|
91
|
-
prompt += `<${domAnnotation.tagName?.toLowerCase() || "unknown"}>`;
|
|
92
|
-
if (domAnnotation.selector) prompt += ` | selector: ${domAnnotation.selector}`;
|
|
93
|
-
if (domAnnotation.text) prompt += ` | text: "${domAnnotation.text.slice(0, 100)}"`;
|
|
94
|
-
if (domAnnotation.elements && domAnnotation.elements.length > 1) {
|
|
95
|
-
prompt += `
|
|
96
|
-
${domAnnotation.elements.length} elements selected`;
|
|
97
|
-
}
|
|
98
|
-
} else if (annotation.type === "gesture") {
|
|
290
|
+
if (annotation.type === "gesture") {
|
|
99
291
|
const gestureAnnotation = annotation;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
Make minimal changes. IMPORTANT: Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line.`;
|
|
107
|
-
return prompt;
|
|
108
|
-
}
|
|
109
|
-
function buildDrawingPrompt(annotation, projectContext, visionDescription) {
|
|
110
|
-
const drawingAnnotation = annotation;
|
|
111
|
-
const bbox = drawingAnnotation.boundingBox;
|
|
112
|
-
const nearbyElements = drawingAnnotation.nearbyElements || [];
|
|
113
|
-
const extractedText = drawingAnnotation.extractedText;
|
|
114
|
-
const comment = drawingAnnotation.comment || "Create a component based on this drawing";
|
|
115
|
-
const hasImage = !!drawingAnnotation.drawingImage;
|
|
116
|
-
const gridSize = drawingAnnotation.gridConfig?.size || 100;
|
|
117
|
-
let gridCellRef = "";
|
|
118
|
-
if (bbox) {
|
|
119
|
-
gridCellRef = getGridCellReference(bbox.x, bbox.y, gridSize);
|
|
120
|
-
}
|
|
121
|
-
let positionContext = "";
|
|
122
|
-
if (bbox && drawingAnnotation.viewport) {
|
|
123
|
-
const vp = drawingAnnotation.viewport;
|
|
124
|
-
const relX = (bbox.x / vp.width * 100).toFixed(1);
|
|
125
|
-
const relY = (bbox.y / vp.height * 100).toFixed(1);
|
|
126
|
-
positionContext = `**Drawing Location:** Approximately ${relX}% from left, ${relY}% from top of viewport`;
|
|
127
|
-
if (gridCellRef) {
|
|
128
|
-
positionContext += ` (grid cell ${gridCellRef})`;
|
|
129
|
-
}
|
|
130
|
-
} else if (bbox) {
|
|
131
|
-
positionContext = `**Drawing Area:** ${Math.round(bbox.width)}\xD7${Math.round(bbox.height)}px`;
|
|
132
|
-
}
|
|
133
|
-
let nearbyContext = "";
|
|
134
|
-
if (nearbyElements.length > 0) {
|
|
135
|
-
const elementList = nearbyElements.slice(0, 5).map((el) => {
|
|
136
|
-
let desc = `- <${el.tagName.toLowerCase()}>`;
|
|
137
|
-
if (el.text) desc += `: "${el.text.slice(0, 50)}"`;
|
|
138
|
-
if (el.className) desc += ` (class: ${el.className.slice(0, 50)})`;
|
|
139
|
-
desc += ` (${el.selector})`;
|
|
140
|
-
return desc;
|
|
141
|
-
}).join("\n");
|
|
142
|
-
nearbyContext = `
|
|
143
|
-
**Nearby DOM Elements (for placement reference):**
|
|
144
|
-
${elementList}`;
|
|
145
|
-
}
|
|
146
|
-
let textContext = "";
|
|
147
|
-
if (extractedText && extractedText.trim()) {
|
|
148
|
-
textContext = `
|
|
149
|
-
**Text found in drawing (use as reference if hard to read):**
|
|
150
|
-
${extractedText}`;
|
|
292
|
+
return buildGesturePrompt({
|
|
293
|
+
comment: annotation.comment || "No specific comment provided",
|
|
294
|
+
gesture: gestureAnnotation.gesture,
|
|
295
|
+
boundingBox: gestureAnnotation.boundingBox
|
|
296
|
+
});
|
|
151
297
|
}
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
298
|
+
const domAnnotation = annotation;
|
|
299
|
+
if (fastMode) {
|
|
300
|
+
return buildFastDomSelectionPrompt({
|
|
301
|
+
comment: annotation.comment || "No specific comment provided",
|
|
302
|
+
selector: domAnnotation.selector,
|
|
303
|
+
text: domAnnotation.text,
|
|
304
|
+
tagName: domAnnotation.tagName
|
|
305
|
+
});
|
|
158
306
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
2. **Interpret, Don't Transcribe:** Elevate the low-fidelity drawing into a high-fidelity component. Choose appropriate spacing, colors, and typography that match modern design standards.
|
|
170
|
-
3. **Infer Missing Details:** If something is underspecified, use your expertise to make the best choice. An informed decision is better than an incomplete component.
|
|
171
|
-
|
|
172
|
-
## Implementation Guidelines
|
|
173
|
-
- Create a React component with inline styles or Tailwind CSS classes
|
|
174
|
-
- Do NOT use hardcoded pixel positions or absolute coordinates - integrate naturally with existing page flow
|
|
175
|
-
- Use flexbox, grid, or relative positioning to place the component appropriately
|
|
176
|
-
- If the sketch shows:
|
|
177
|
-
- **Rectangle/box:** Card, container, button, or input field depending on context
|
|
178
|
-
- **Text elements:** Headings, paragraphs, or labels with appropriate hierarchy
|
|
179
|
-
- **Form layout:** Input fields with labels, proper spacing
|
|
180
|
-
- **Icons/shapes:** Use appropriate icons from lucide-react or inline SVGs
|
|
181
|
-
- **Navigation:** Nav links, menus, or breadcrumbs
|
|
182
|
-
- **Lists:** Ordered/unordered lists or grid layouts
|
|
183
|
-
- Make the component fit naturally with the existing page design
|
|
184
|
-
- Style it nicely and according to the existing codebase
|
|
185
|
-
- Use semantic HTML and ARIA attributes where appropriate
|
|
186
|
-
- The component should integrate well with the existing codebase structure
|
|
187
|
-
|
|
188
|
-
## Annotations
|
|
189
|
-
- Any **red marks** in the drawing are instructions\u2014follow them but don't render them
|
|
190
|
-
- Text annotations describe intent or constraints
|
|
191
|
-
|
|
192
|
-
Make the changes directly. Insert the component at the appropriate location in the page. No explanation needed.
|
|
193
|
-
|
|
194
|
-
IMPORTANT: Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line or the SkemaWrapper component itself.`;
|
|
195
|
-
return prompt;
|
|
307
|
+
return buildDetailedDomSelectionPrompt({
|
|
308
|
+
comment: annotation.comment || "No specific comment provided",
|
|
309
|
+
tagName: domAnnotation.tagName,
|
|
310
|
+
selector: domAnnotation.selector,
|
|
311
|
+
text: domAnnotation.text,
|
|
312
|
+
elementPath: domAnnotation.elementPath,
|
|
313
|
+
cssClasses: domAnnotation.cssClasses,
|
|
314
|
+
attributes: domAnnotation.attributes,
|
|
315
|
+
elements: domAnnotation.elements
|
|
316
|
+
});
|
|
196
317
|
}
|
|
197
318
|
function spawnGeminiCLI(prompt, options = {}) {
|
|
198
319
|
const {
|
|
@@ -298,22 +419,8 @@ async function analyzeImageWithGemini(apiKey, base64Image, modelName = "gemini-2
|
|
|
298
419
|
}
|
|
299
420
|
}
|
|
300
421
|
];
|
|
301
|
-
const imageAnalysisPrompt = `
|
|
302
|
-
Analyze this UI wireframe sketch in extreme detail for a front-end developer.
|
|
303
|
-
|
|
304
|
-
Describe every element, layout, spacing, icons, and text you see.
|
|
305
|
-
Mention relative positions and hierarchy.
|
|
306
|
-
Be distinct about what is drawn vs what might be background.
|
|
307
|
-
|
|
308
|
-
Do NOT focus on exact pixel coordinates or absolute positions - describe layouts
|
|
309
|
-
in terms of relative positioning (left/right/top/bottom, centered, evenly spaced, etc.).
|
|
310
|
-
|
|
311
|
-
It is expected that they will be rough draft's / hand-drawn things. Interpret the drawing and its goals as best as you can.
|
|
312
|
-
DO NOT MENTION THAT THINGS HAVE "Hand-sketched" or "Hand-drawn" vibes. Make assumptions of what they were trying to do.
|
|
313
|
-
Just FYI, this gets passed onto a generator to generate the actual code of modern UI componenents.
|
|
314
|
-
`.trim();
|
|
315
422
|
const result = await model.generateContent([
|
|
316
|
-
|
|
423
|
+
IMAGE_ANALYSIS_PROMPT,
|
|
317
424
|
...imageParts
|
|
318
425
|
]);
|
|
319
426
|
const response = await result.response;
|
|
@@ -326,15 +433,32 @@ Just FYI, this gets passed onto a generator to generate the actual code of moder
|
|
|
326
433
|
}
|
|
327
434
|
function createGeminiCLIStream(annotation, projectContext, options) {
|
|
328
435
|
const encoder = new TextEncoder();
|
|
436
|
+
let geminiProcess = null;
|
|
437
|
+
let isCancelled = false;
|
|
329
438
|
return new ReadableStream({
|
|
330
439
|
async start(controller) {
|
|
331
440
|
const apiKey = options?.apiKey || process.env.GEMINI_API_KEY;
|
|
332
441
|
let visionDescription = "";
|
|
442
|
+
if (options?.signal) {
|
|
443
|
+
options.signal.addEventListener("abort", () => {
|
|
444
|
+
isCancelled = true;
|
|
445
|
+
if (geminiProcess && !geminiProcess.killed) {
|
|
446
|
+
console.log("[Skema Server] Killing Gemini CLI process due to client disconnect");
|
|
447
|
+
geminiProcess.kill("SIGTERM");
|
|
448
|
+
}
|
|
449
|
+
options.onCancel?.();
|
|
450
|
+
try {
|
|
451
|
+
controller.close();
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
333
456
|
if (annotation.type === "drawing" && annotation.drawingImage && apiKey) {
|
|
457
|
+
if (isCancelled) return;
|
|
334
458
|
const progressEvent = {
|
|
335
459
|
type: "message",
|
|
336
460
|
role: "assistant",
|
|
337
|
-
content: "
|
|
461
|
+
content: "[Analyzing] Drawing image with Gemini Vision...",
|
|
338
462
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
339
463
|
};
|
|
340
464
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(progressEvent)}
|
|
@@ -345,10 +469,11 @@ function createGeminiCLIStream(annotation, projectContext, options) {
|
|
|
345
469
|
annotation.drawingImage,
|
|
346
470
|
options?.model || "gemini-2.5-flash"
|
|
347
471
|
);
|
|
472
|
+
if (isCancelled) return;
|
|
348
473
|
const analysisEvent = {
|
|
349
474
|
type: "message",
|
|
350
475
|
role: "assistant",
|
|
351
|
-
content:
|
|
476
|
+
content: `[Vision] Visual Analysis:
|
|
352
477
|
${visionDescription}`,
|
|
353
478
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
354
479
|
};
|
|
@@ -356,6 +481,7 @@ ${visionDescription}`,
|
|
|
356
481
|
|
|
357
482
|
`));
|
|
358
483
|
}
|
|
484
|
+
if (isCancelled) return;
|
|
359
485
|
const prompt = buildPromptFromAnnotation(
|
|
360
486
|
annotation,
|
|
361
487
|
projectContext,
|
|
@@ -373,8 +499,10 @@ ${visionDescription}`,
|
|
|
373
499
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(promptEvent)}
|
|
374
500
|
|
|
375
501
|
`));
|
|
376
|
-
const { events } = spawnGeminiCLI(prompt, options);
|
|
502
|
+
const { process: proc, events } = spawnGeminiCLI(prompt, options);
|
|
503
|
+
geminiProcess = proc;
|
|
377
504
|
for await (const event of events) {
|
|
505
|
+
if (isCancelled) break;
|
|
378
506
|
const sseData = `data: ${JSON.stringify(event)}
|
|
379
507
|
|
|
380
508
|
`;
|
|
@@ -384,6 +512,13 @@ ${visionDescription}`,
|
|
|
384
512
|
break;
|
|
385
513
|
}
|
|
386
514
|
}
|
|
515
|
+
},
|
|
516
|
+
cancel() {
|
|
517
|
+
isCancelled = true;
|
|
518
|
+
if (geminiProcess && !geminiProcess.killed) {
|
|
519
|
+
geminiProcess.kill("SIGTERM");
|
|
520
|
+
}
|
|
521
|
+
options?.onCancel?.();
|
|
387
522
|
}
|
|
388
523
|
});
|
|
389
524
|
}
|
|
@@ -428,7 +563,19 @@ function createGeminiRouteHandler(defaultOptions) {
|
|
|
428
563
|
createSnapshot(annotationId, cwd);
|
|
429
564
|
const stream = createGeminiCLIStream(annotation, projectContext, {
|
|
430
565
|
cwd,
|
|
431
|
-
...defaultOptions
|
|
566
|
+
...defaultOptions,
|
|
567
|
+
// Pass abort signal from request for client disconnect detection
|
|
568
|
+
signal: request.signal,
|
|
569
|
+
// Revert snapshot if client cancels/disconnects
|
|
570
|
+
onCancel: () => {
|
|
571
|
+
console.log(`[Skema Server] Client disconnected, reverting snapshot for: ${annotationId}`);
|
|
572
|
+
const result = revertAnnotation(annotationId, cwd);
|
|
573
|
+
if (result.success) {
|
|
574
|
+
console.log(`[Skema Server] Successfully reverted changes for: ${annotationId}`);
|
|
575
|
+
} else {
|
|
576
|
+
console.error(`[Skema Server] Failed to revert changes: ${result.message}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
432
579
|
});
|
|
433
580
|
return new Response(stream, {
|
|
434
581
|
headers: {
|
|
@@ -459,7 +606,696 @@ function createRevertRouteHandler(defaultOptions) {
|
|
|
459
606
|
}
|
|
460
607
|
var POST = createGeminiRouteHandler();
|
|
461
608
|
var DELETE = createRevertRouteHandler();
|
|
609
|
+
var PROVIDERS = {
|
|
610
|
+
gemini: {
|
|
611
|
+
command: "gemini",
|
|
612
|
+
buildArgs: (prompt, options) => {
|
|
613
|
+
const args = ["-p", prompt];
|
|
614
|
+
if (options?.yolo !== false) args.push("--yolo");
|
|
615
|
+
args.push("--output-format", "stream-json");
|
|
616
|
+
if (options?.model) args.push("-m", options.model);
|
|
617
|
+
else args.push("-m", "gemini-2.5-flash");
|
|
618
|
+
return args;
|
|
619
|
+
},
|
|
620
|
+
parseOutput: (line) => {
|
|
621
|
+
try {
|
|
622
|
+
const parsed = JSON.parse(line);
|
|
623
|
+
return {
|
|
624
|
+
type: mapGeminiEventType(parsed.type),
|
|
625
|
+
content: parsed.content || parsed.message,
|
|
626
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
627
|
+
provider: "gemini",
|
|
628
|
+
raw: parsed
|
|
629
|
+
};
|
|
630
|
+
} catch {
|
|
631
|
+
return {
|
|
632
|
+
type: "text",
|
|
633
|
+
content: line,
|
|
634
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
635
|
+
provider: "gemini"
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
claude: {
|
|
641
|
+
command: "claude",
|
|
642
|
+
buildArgs: (prompt, options) => {
|
|
643
|
+
const args = ["-p", prompt, "--output-format", "stream-json"];
|
|
644
|
+
if (options?.yolo !== false) args.push("--dangerously-skip-permissions");
|
|
645
|
+
if (options?.model) args.push("--model", options.model);
|
|
646
|
+
return args;
|
|
647
|
+
},
|
|
648
|
+
parseOutput: (line) => {
|
|
649
|
+
try {
|
|
650
|
+
const parsed = JSON.parse(line);
|
|
651
|
+
return {
|
|
652
|
+
type: mapClaudeEventType(parsed.type),
|
|
653
|
+
content: parsed.content || extractClaudeContent(parsed),
|
|
654
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
655
|
+
provider: "claude",
|
|
656
|
+
raw: parsed
|
|
657
|
+
};
|
|
658
|
+
} catch {
|
|
659
|
+
return {
|
|
660
|
+
type: "text",
|
|
661
|
+
content: line,
|
|
662
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
663
|
+
provider: "claude"
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
function mapGeminiEventType(type) {
|
|
670
|
+
switch (type) {
|
|
671
|
+
case "init":
|
|
672
|
+
return "init";
|
|
673
|
+
case "message":
|
|
674
|
+
return "text";
|
|
675
|
+
case "tool_use":
|
|
676
|
+
return "tool_use";
|
|
677
|
+
case "tool_result":
|
|
678
|
+
return "tool_result";
|
|
679
|
+
case "error":
|
|
680
|
+
return "error";
|
|
681
|
+
case "done":
|
|
682
|
+
case "result":
|
|
683
|
+
return "done";
|
|
684
|
+
default:
|
|
685
|
+
return "text";
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function mapClaudeEventType(type) {
|
|
689
|
+
switch (type) {
|
|
690
|
+
case "system":
|
|
691
|
+
return "init";
|
|
692
|
+
case "assistant":
|
|
693
|
+
case "text":
|
|
694
|
+
return "text";
|
|
695
|
+
case "tool_use":
|
|
696
|
+
return "tool_use";
|
|
697
|
+
case "tool_result":
|
|
698
|
+
return "tool_result";
|
|
699
|
+
case "error":
|
|
700
|
+
return "error";
|
|
701
|
+
case "result":
|
|
702
|
+
return "done";
|
|
703
|
+
default:
|
|
704
|
+
return "text";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function extractClaudeContent(parsed) {
|
|
708
|
+
if (typeof parsed.content === "string") return parsed.content;
|
|
709
|
+
if (parsed.message && typeof parsed.message === "object") {
|
|
710
|
+
const msg = parsed.message;
|
|
711
|
+
if (typeof msg.content === "string") return msg.content;
|
|
712
|
+
}
|
|
713
|
+
return void 0;
|
|
714
|
+
}
|
|
715
|
+
function spawnAICLI(prompt, config) {
|
|
716
|
+
const spec = PROVIDERS[config.provider];
|
|
717
|
+
const args = spec.buildArgs(prompt, { model: config.model });
|
|
718
|
+
const cwd = config.cwd || process.cwd();
|
|
719
|
+
console.log(`[Skema] Spawning ${config.provider}: ${spec.command} ${args.join(" ")}`);
|
|
720
|
+
const child = spawn(spec.command, args, {
|
|
721
|
+
cwd,
|
|
722
|
+
env: process.env
|
|
723
|
+
});
|
|
724
|
+
const events = {
|
|
725
|
+
[Symbol.asyncIterator]() {
|
|
726
|
+
let buffer = "";
|
|
727
|
+
let done = false;
|
|
728
|
+
const queue = [];
|
|
729
|
+
let resolveNext = null;
|
|
730
|
+
const pushEvent = (event) => {
|
|
731
|
+
if (resolveNext) {
|
|
732
|
+
resolveNext({ value: event, done: false });
|
|
733
|
+
resolveNext = null;
|
|
734
|
+
} else {
|
|
735
|
+
queue.push(event);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
child.stdout?.on("data", (data) => {
|
|
739
|
+
buffer += data.toString();
|
|
740
|
+
const lines = buffer.split("\n");
|
|
741
|
+
buffer = lines.pop() || "";
|
|
742
|
+
for (const line of lines) {
|
|
743
|
+
if (line.trim()) {
|
|
744
|
+
const event = spec.parseOutput(line.trim());
|
|
745
|
+
if (event) pushEvent(event);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
child.stderr?.on("data", (data) => {
|
|
750
|
+
const content = data.toString().trim();
|
|
751
|
+
if (content) {
|
|
752
|
+
pushEvent({
|
|
753
|
+
type: "error",
|
|
754
|
+
content,
|
|
755
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
756
|
+
provider: config.provider
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
child.on("close", (code) => {
|
|
761
|
+
if (buffer.trim()) {
|
|
762
|
+
const event = spec.parseOutput(buffer.trim());
|
|
763
|
+
if (event) pushEvent(event);
|
|
764
|
+
}
|
|
765
|
+
pushEvent({
|
|
766
|
+
type: "done",
|
|
767
|
+
content: `Process exited with code ${code}`,
|
|
768
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
769
|
+
provider: config.provider
|
|
770
|
+
});
|
|
771
|
+
done = true;
|
|
772
|
+
if (resolveNext) {
|
|
773
|
+
resolveNext({ value: void 0, done: true });
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
child.on("error", (err) => {
|
|
777
|
+
pushEvent({
|
|
778
|
+
type: "error",
|
|
779
|
+
content: `Failed to spawn ${config.provider}: ${err.message}`,
|
|
780
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
781
|
+
provider: config.provider
|
|
782
|
+
});
|
|
783
|
+
done = true;
|
|
784
|
+
if (resolveNext) {
|
|
785
|
+
resolveNext({ value: void 0, done: true });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
next() {
|
|
790
|
+
if (queue.length > 0) {
|
|
791
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
792
|
+
}
|
|
793
|
+
if (done) {
|
|
794
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
795
|
+
}
|
|
796
|
+
return new Promise((resolve) => {
|
|
797
|
+
resolveNext = resolve;
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
return { process: child, events };
|
|
804
|
+
}
|
|
805
|
+
async function runAICLI(prompt, config) {
|
|
806
|
+
const { events } = spawnAICLI(prompt, config);
|
|
807
|
+
const collectedEvents = [];
|
|
808
|
+
let output = "";
|
|
809
|
+
let success = true;
|
|
810
|
+
for await (const event of events) {
|
|
811
|
+
collectedEvents.push(event);
|
|
812
|
+
if (event.type === "text" && event.content) {
|
|
813
|
+
output += event.content + "\n";
|
|
814
|
+
}
|
|
815
|
+
if (event.type === "error") {
|
|
816
|
+
success = false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
success,
|
|
821
|
+
output: output.trim(),
|
|
822
|
+
events: collectedEvents,
|
|
823
|
+
provider: config.provider
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function isProviderAvailable(provider) {
|
|
827
|
+
const spec = PROVIDERS[provider];
|
|
828
|
+
try {
|
|
829
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
830
|
+
execSync3(`which ${spec.command}`, { stdio: "ignore" });
|
|
831
|
+
return true;
|
|
832
|
+
} catch {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function getAvailableProviders() {
|
|
837
|
+
return ["gemini", "claude"].filter(isProviderAvailable);
|
|
838
|
+
}
|
|
839
|
+
async function analyzeWithGemini(base64Image, apiKey, model = "gemini-2.5-flash") {
|
|
840
|
+
try {
|
|
841
|
+
const genAI = new GoogleGenerativeAI(apiKey);
|
|
842
|
+
const visionModel = genAI.getGenerativeModel({ model });
|
|
843
|
+
const cleanBase64 = base64Image.replace(/^data:image\/\w+;base64,/, "");
|
|
844
|
+
const result = await visionModel.generateContent([
|
|
845
|
+
IMAGE_ANALYSIS_PROMPT,
|
|
846
|
+
{
|
|
847
|
+
inlineData: {
|
|
848
|
+
data: cleanBase64,
|
|
849
|
+
mimeType: "image/png"
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
]);
|
|
853
|
+
const response = await result.response;
|
|
854
|
+
const text = response.text();
|
|
855
|
+
return {
|
|
856
|
+
success: true,
|
|
857
|
+
description: text,
|
|
858
|
+
provider: "gemini"
|
|
859
|
+
};
|
|
860
|
+
} catch (error) {
|
|
861
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
862
|
+
console.error("[Vision] Gemini analysis failed:", message);
|
|
863
|
+
return {
|
|
864
|
+
success: false,
|
|
865
|
+
description: "",
|
|
866
|
+
provider: "gemini",
|
|
867
|
+
error: message
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async function analyzeWithClaude(base64Image, apiKey, model = "claude-sonnet-4-20250514") {
|
|
872
|
+
try {
|
|
873
|
+
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
874
|
+
const client = new Anthropic({ apiKey });
|
|
875
|
+
const cleanBase64 = base64Image.replace(/^data:image\/\w+;base64,/, "");
|
|
876
|
+
const response = await client.messages.create({
|
|
877
|
+
model,
|
|
878
|
+
max_tokens: 1024,
|
|
879
|
+
messages: [
|
|
880
|
+
{
|
|
881
|
+
role: "user",
|
|
882
|
+
content: [
|
|
883
|
+
{
|
|
884
|
+
type: "image",
|
|
885
|
+
source: {
|
|
886
|
+
type: "base64",
|
|
887
|
+
media_type: "image/png",
|
|
888
|
+
data: cleanBase64
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
type: "text",
|
|
893
|
+
text: IMAGE_ANALYSIS_PROMPT
|
|
894
|
+
}
|
|
895
|
+
]
|
|
896
|
+
}
|
|
897
|
+
]
|
|
898
|
+
});
|
|
899
|
+
const textContent = response.content.find((c) => c.type === "text");
|
|
900
|
+
const description = textContent && "text" in textContent ? textContent.text : "";
|
|
901
|
+
return {
|
|
902
|
+
success: true,
|
|
903
|
+
description,
|
|
904
|
+
provider: "claude"
|
|
905
|
+
};
|
|
906
|
+
} catch (error) {
|
|
907
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
908
|
+
console.error("[Vision] Claude analysis failed:", message);
|
|
909
|
+
return {
|
|
910
|
+
success: false,
|
|
911
|
+
description: "",
|
|
912
|
+
provider: "claude",
|
|
913
|
+
error: message
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function analyzeImage(base64Image, config) {
|
|
918
|
+
const { provider } = config;
|
|
919
|
+
let apiKey = config.apiKey;
|
|
920
|
+
if (!apiKey) {
|
|
921
|
+
apiKey = provider === "gemini" ? process.env.GEMINI_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
922
|
+
}
|
|
923
|
+
if (!apiKey) {
|
|
924
|
+
return {
|
|
925
|
+
success: false,
|
|
926
|
+
description: "",
|
|
927
|
+
provider,
|
|
928
|
+
error: `No API key found for ${provider} vision. Set ${provider === "gemini" ? "GEMINI_API_KEY" : "ANTHROPIC_API_KEY"} environment variable.`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
console.log(`[Vision] Analyzing image with ${provider}...`);
|
|
932
|
+
if (provider === "gemini") {
|
|
933
|
+
return analyzeWithGemini(base64Image, apiKey, config.model);
|
|
934
|
+
} else {
|
|
935
|
+
return analyzeWithClaude(base64Image, apiKey, config.model);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function isVisionAvailable(provider) {
|
|
939
|
+
if (provider === "gemini") {
|
|
940
|
+
return !!process.env.GEMINI_API_KEY;
|
|
941
|
+
} else {
|
|
942
|
+
return !!process.env.ANTHROPIC_API_KEY;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/server/daemon.ts
|
|
947
|
+
var currentProvider = "gemini";
|
|
948
|
+
var workingDirectory = process.cwd();
|
|
949
|
+
var annotationSnapshots2 = /* @__PURE__ */ new Map();
|
|
950
|
+
function createSnapshot2(annotationId) {
|
|
951
|
+
try {
|
|
952
|
+
const stashRef = execSync("git stash create", { cwd: workingDirectory, encoding: "utf-8" }).trim();
|
|
953
|
+
if (stashRef) {
|
|
954
|
+
annotationSnapshots2.set(annotationId, stashRef);
|
|
955
|
+
console.log(`[Daemon] Created snapshot ${stashRef.slice(0, 7)} for ${annotationId}`);
|
|
956
|
+
return stashRef;
|
|
957
|
+
}
|
|
958
|
+
const headRef = execSync("git rev-parse HEAD", { cwd: workingDirectory, encoding: "utf-8" }).trim();
|
|
959
|
+
annotationSnapshots2.set(annotationId, headRef);
|
|
960
|
+
return headRef;
|
|
961
|
+
} catch (error) {
|
|
962
|
+
console.error("[Daemon] Failed to create snapshot:", error);
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function revertSnapshot(annotationId) {
|
|
967
|
+
const snapshotRef = annotationSnapshots2.get(annotationId);
|
|
968
|
+
if (!snapshotRef) {
|
|
969
|
+
return { success: false, message: `No snapshot found for ${annotationId}` };
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const changedFiles = execSync(`git diff --name-only ${snapshotRef}`, {
|
|
973
|
+
cwd: workingDirectory,
|
|
974
|
+
encoding: "utf-8"
|
|
975
|
+
}).trim().split("\n").filter(Boolean);
|
|
976
|
+
if (changedFiles.length === 0) {
|
|
977
|
+
annotationSnapshots2.delete(annotationId);
|
|
978
|
+
return { success: true, message: "No changes to revert" };
|
|
979
|
+
}
|
|
980
|
+
for (const file of changedFiles) {
|
|
981
|
+
try {
|
|
982
|
+
execSync(`git checkout ${snapshotRef} -- "${file}"`, { cwd: workingDirectory });
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
annotationSnapshots2.delete(annotationId);
|
|
987
|
+
return { success: true, message: `Reverted ${changedFiles.length} file(s)` };
|
|
988
|
+
} catch (error) {
|
|
989
|
+
return { success: false, message: String(error) };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
var handlers = {
|
|
993
|
+
// -------------------------------------------------------------------------
|
|
994
|
+
// Provider Management
|
|
995
|
+
// -------------------------------------------------------------------------
|
|
996
|
+
"get-provider": async (msg) => {
|
|
997
|
+
return {
|
|
998
|
+
id: msg.id,
|
|
999
|
+
type: "provider",
|
|
1000
|
+
provider: currentProvider,
|
|
1001
|
+
available: getAvailableProviders()
|
|
1002
|
+
};
|
|
1003
|
+
},
|
|
1004
|
+
"set-provider": async (msg) => {
|
|
1005
|
+
const newProvider = msg.provider;
|
|
1006
|
+
if (!["gemini", "claude"].includes(newProvider)) {
|
|
1007
|
+
return { id: msg.id, type: "error", error: `Invalid provider: ${newProvider}` };
|
|
1008
|
+
}
|
|
1009
|
+
if (!isProviderAvailable(newProvider)) {
|
|
1010
|
+
return {
|
|
1011
|
+
id: msg.id,
|
|
1012
|
+
type: "error",
|
|
1013
|
+
error: `Provider "${newProvider}" is not installed. Run: ${newProvider === "gemini" ? "npm install -g @anthropic-ai/gemini-cli" : "npm install -g @anthropic-ai/claude-code"}`
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
currentProvider = newProvider;
|
|
1017
|
+
console.log(`[Daemon] Switched to provider: ${currentProvider}`);
|
|
1018
|
+
return { id: msg.id, type: "provider-changed", provider: currentProvider };
|
|
1019
|
+
},
|
|
1020
|
+
// -------------------------------------------------------------------------
|
|
1021
|
+
// AI Generation (streaming)
|
|
1022
|
+
// -------------------------------------------------------------------------
|
|
1023
|
+
generate: async (msg, ws) => {
|
|
1024
|
+
const annotation = msg.annotation;
|
|
1025
|
+
const projectContext = msg.projectContext;
|
|
1026
|
+
const annotationId = annotation.id || `temp-${Date.now()}`;
|
|
1027
|
+
createSnapshot2(annotationId);
|
|
1028
|
+
let visionDescription = "";
|
|
1029
|
+
const drawingAnnotation = annotation;
|
|
1030
|
+
if (annotation.type === "drawing" && drawingAnnotation.drawingImage) {
|
|
1031
|
+
if (isVisionAvailable("gemini")) {
|
|
1032
|
+
sendMessage(ws, {
|
|
1033
|
+
id: msg.id,
|
|
1034
|
+
type: "ai-event",
|
|
1035
|
+
event: {
|
|
1036
|
+
type: "text",
|
|
1037
|
+
content: `[Analyzing drawing with Gemini vision...]`,
|
|
1038
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1039
|
+
provider: currentProvider
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
const visionResult = await analyzeImage(drawingAnnotation.drawingImage, {
|
|
1043
|
+
provider: "gemini"
|
|
1044
|
+
});
|
|
1045
|
+
if (visionResult.success) {
|
|
1046
|
+
visionDescription = visionResult.description;
|
|
1047
|
+
sendMessage(ws, {
|
|
1048
|
+
id: msg.id,
|
|
1049
|
+
type: "ai-event",
|
|
1050
|
+
event: {
|
|
1051
|
+
type: "text",
|
|
1052
|
+
content: `[Vision analysis complete]
|
|
1053
|
+
${visionDescription}`,
|
|
1054
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1055
|
+
provider: currentProvider
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
} else {
|
|
1059
|
+
sendMessage(ws, {
|
|
1060
|
+
id: msg.id,
|
|
1061
|
+
type: "ai-event",
|
|
1062
|
+
event: {
|
|
1063
|
+
type: "error",
|
|
1064
|
+
content: `Vision analysis failed: ${visionResult.error}`,
|
|
1065
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1066
|
+
provider: currentProvider
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
sendMessage(ws, {
|
|
1072
|
+
id: msg.id,
|
|
1073
|
+
type: "ai-event",
|
|
1074
|
+
event: {
|
|
1075
|
+
type: "text",
|
|
1076
|
+
content: `[Vision not available - set GEMINI_API_KEY for image analysis]`,
|
|
1077
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1078
|
+
provider: currentProvider
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const prompt = buildPromptFromAnnotation(annotation, projectContext, {
|
|
1084
|
+
fastMode: msg.fastMode === true,
|
|
1085
|
+
// Default to detailed mode (false) unless explicitly set to true
|
|
1086
|
+
visionDescription
|
|
1087
|
+
});
|
|
1088
|
+
sendMessage(ws, {
|
|
1089
|
+
id: msg.id,
|
|
1090
|
+
type: "ai-event",
|
|
1091
|
+
event: {
|
|
1092
|
+
type: "debug",
|
|
1093
|
+
content: prompt,
|
|
1094
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1095
|
+
provider: currentProvider
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
const config = {
|
|
1099
|
+
provider: currentProvider,
|
|
1100
|
+
cwd: workingDirectory,
|
|
1101
|
+
model: msg.model
|
|
1102
|
+
};
|
|
1103
|
+
const { process: aiProcess, events } = spawnAICLI(prompt, config);
|
|
1104
|
+
for await (const event of events) {
|
|
1105
|
+
sendMessage(ws, {
|
|
1106
|
+
id: msg.id,
|
|
1107
|
+
type: "ai-event",
|
|
1108
|
+
event,
|
|
1109
|
+
annotationId
|
|
1110
|
+
});
|
|
1111
|
+
if (event.type === "done") {
|
|
1112
|
+
sendMessage(ws, {
|
|
1113
|
+
id: msg.id,
|
|
1114
|
+
type: "generate-complete",
|
|
1115
|
+
success: true,
|
|
1116
|
+
annotationId,
|
|
1117
|
+
provider: currentProvider
|
|
1118
|
+
});
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
// -------------------------------------------------------------------------
|
|
1124
|
+
// Undo/Revert
|
|
1125
|
+
// -------------------------------------------------------------------------
|
|
1126
|
+
revert: async (msg) => {
|
|
1127
|
+
const annotationId = msg.annotationId;
|
|
1128
|
+
if (!annotationId) {
|
|
1129
|
+
return { id: msg.id, type: "error", error: "Missing annotationId" };
|
|
1130
|
+
}
|
|
1131
|
+
const result = revertSnapshot(annotationId);
|
|
1132
|
+
return { id: msg.id, type: "revert-result", ...result };
|
|
1133
|
+
},
|
|
1134
|
+
// -------------------------------------------------------------------------
|
|
1135
|
+
// File Operations
|
|
1136
|
+
// -------------------------------------------------------------------------
|
|
1137
|
+
"read-file": async (msg) => {
|
|
1138
|
+
const filePath = msg.path;
|
|
1139
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workingDirectory, filePath);
|
|
1140
|
+
try {
|
|
1141
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
1142
|
+
return { id: msg.id, type: "file-content", path: filePath, content };
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
return { id: msg.id, type: "error", error: `Failed to read file: ${error}` };
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
"write-file": async (msg) => {
|
|
1148
|
+
const filePath = msg.path;
|
|
1149
|
+
const content = msg.content;
|
|
1150
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workingDirectory, filePath);
|
|
1151
|
+
try {
|
|
1152
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
1153
|
+
fs.writeFileSync(absolutePath, content, "utf-8");
|
|
1154
|
+
console.log(`[Daemon] Wrote file: ${filePath}`);
|
|
1155
|
+
return { id: msg.id, type: "write-success", path: filePath };
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
return { id: msg.id, type: "error", error: `Failed to write file: ${error}` };
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
"list-files": async (msg) => {
|
|
1161
|
+
const dirPath = msg.path || ".";
|
|
1162
|
+
const absolutePath = path.isAbsolute(dirPath) ? dirPath : path.join(workingDirectory, dirPath);
|
|
1163
|
+
try {
|
|
1164
|
+
const entries = fs.readdirSync(absolutePath, { withFileTypes: true });
|
|
1165
|
+
const files = entries.map((entry) => ({
|
|
1166
|
+
name: entry.name,
|
|
1167
|
+
isDirectory: entry.isDirectory()
|
|
1168
|
+
}));
|
|
1169
|
+
return { id: msg.id, type: "file-list", path: dirPath, files };
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
return { id: msg.id, type: "error", error: `Failed to list files: ${error}` };
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
// -------------------------------------------------------------------------
|
|
1175
|
+
// Command Execution
|
|
1176
|
+
// -------------------------------------------------------------------------
|
|
1177
|
+
"run-command": async (msg, ws) => {
|
|
1178
|
+
const command = msg.command;
|
|
1179
|
+
if (!command) {
|
|
1180
|
+
return { id: msg.id, type: "error", error: "Missing command" };
|
|
1181
|
+
}
|
|
1182
|
+
console.log(`[Daemon] Running command: ${command}`);
|
|
1183
|
+
return new Promise((resolve) => {
|
|
1184
|
+
exec(command, { cwd: workingDirectory }, (error, stdout, stderr) => {
|
|
1185
|
+
resolve({
|
|
1186
|
+
id: msg.id,
|
|
1187
|
+
type: "command-result",
|
|
1188
|
+
stdout,
|
|
1189
|
+
stderr,
|
|
1190
|
+
exitCode: error ? error.code : 0
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
},
|
|
1195
|
+
// -------------------------------------------------------------------------
|
|
1196
|
+
// Status
|
|
1197
|
+
// -------------------------------------------------------------------------
|
|
1198
|
+
ping: async (msg) => {
|
|
1199
|
+
return {
|
|
1200
|
+
id: msg.id,
|
|
1201
|
+
type: "pong",
|
|
1202
|
+
provider: currentProvider,
|
|
1203
|
+
cwd: workingDirectory,
|
|
1204
|
+
availableProviders: getAvailableProviders()
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
function sendMessage(ws, message) {
|
|
1209
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1210
|
+
ws.send(JSON.stringify(message));
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function handleConnection(ws) {
|
|
1214
|
+
console.log("[Daemon] Client connected");
|
|
1215
|
+
sendMessage(ws, {
|
|
1216
|
+
type: "connected",
|
|
1217
|
+
provider: currentProvider,
|
|
1218
|
+
cwd: workingDirectory,
|
|
1219
|
+
availableProviders: getAvailableProviders()
|
|
1220
|
+
});
|
|
1221
|
+
ws.on("message", async (data) => {
|
|
1222
|
+
let msg;
|
|
1223
|
+
try {
|
|
1224
|
+
msg = JSON.parse(data.toString());
|
|
1225
|
+
} catch {
|
|
1226
|
+
sendMessage(ws, { type: "error", error: "Invalid JSON" });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const handler = handlers[msg.type];
|
|
1230
|
+
if (!handler) {
|
|
1231
|
+
sendMessage(ws, { id: msg.id, type: "error", error: `Unknown message type: ${msg.type}` });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
try {
|
|
1235
|
+
const response = await handler(msg, ws);
|
|
1236
|
+
if (response) {
|
|
1237
|
+
sendMessage(ws, response);
|
|
1238
|
+
}
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
sendMessage(ws, {
|
|
1241
|
+
id: msg.id,
|
|
1242
|
+
type: "error",
|
|
1243
|
+
error: `Handler error: ${error}`
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
ws.on("close", () => {
|
|
1248
|
+
console.log("[Daemon] Client disconnected");
|
|
1249
|
+
});
|
|
1250
|
+
ws.on("error", (error) => {
|
|
1251
|
+
console.error("[Daemon] WebSocket error:", error);
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
function startDaemon(config = {}) {
|
|
1255
|
+
const port = config.port ?? 9999;
|
|
1256
|
+
workingDirectory = config.cwd ?? process.cwd();
|
|
1257
|
+
currentProvider = config.defaultProvider ?? "gemini";
|
|
1258
|
+
if (!isProviderAvailable(currentProvider)) {
|
|
1259
|
+
const available = getAvailableProviders();
|
|
1260
|
+
if (available.length > 0) {
|
|
1261
|
+
console.log(`[Daemon] ${currentProvider} not found, falling back to ${available[0]}`);
|
|
1262
|
+
currentProvider = available[0];
|
|
1263
|
+
} else {
|
|
1264
|
+
console.warn("[Daemon] Warning: No AI providers found. Install gemini or claude CLI.");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const wss = new WebSocketServer({ port });
|
|
1268
|
+
wss.on("connection", handleConnection);
|
|
1269
|
+
wss.on("listening", () => {
|
|
1270
|
+
console.log("");
|
|
1271
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1272
|
+
console.log(" \u2502 \u2502");
|
|
1273
|
+
console.log(` \u2502 \u{1F3A8} Skema Daemon running on ws://localhost:${port} \u2502`);
|
|
1274
|
+
console.log(" \u2502 \u2502");
|
|
1275
|
+
console.log(` \u2502 Provider: ${currentProvider.padEnd(35)}\u2502`);
|
|
1276
|
+
console.log(` \u2502 Directory: ${workingDirectory.slice(-33).padEnd(34)}\u2502`);
|
|
1277
|
+
console.log(" \u2502 \u2502");
|
|
1278
|
+
console.log(" \u2502 Waiting for browser connections... \u2502");
|
|
1279
|
+
console.log(" \u2502 \u2502");
|
|
1280
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1281
|
+
console.log("");
|
|
1282
|
+
});
|
|
1283
|
+
wss.on("error", (error) => {
|
|
1284
|
+
if (error.code === "EADDRINUSE") {
|
|
1285
|
+
console.error(`[Daemon] Port ${port} is already in use. Is another Skema daemon running?`);
|
|
1286
|
+
} else {
|
|
1287
|
+
console.error("[Daemon] Server error:", error);
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
return {
|
|
1291
|
+
port,
|
|
1292
|
+
close: () => {
|
|
1293
|
+
wss.close();
|
|
1294
|
+
console.log("[Daemon] Server stopped");
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
462
1298
|
|
|
463
|
-
export { DELETE, POST, buildPromptFromAnnotation, createGeminiCLIStream, createGeminiRouteHandler, createRevertRouteHandler, getTrackedAnnotations, revertAnnotation, runGeminiCLI, spawnGeminiCLI };
|
|
1299
|
+
export { DELETE, IMAGE_ANALYSIS_PROMPT, POST, analyzeImage, buildDetailedDomSelectionPrompt, buildDrawingToCodePrompt, buildFastDomSelectionPrompt, buildGesturePrompt, buildPromptFromAnnotation, createGeminiCLIStream, createGeminiRouteHandler, createRevertRouteHandler, getAvailableProviders, getTrackedAnnotations, isProviderAvailable, isVisionAvailable, revertAnnotation, runAICLI, runGeminiCLI, spawnAICLI, spawnGeminiCLI, startDaemon };
|
|
464
1300
|
//# sourceMappingURL=server.mjs.map
|
|
465
1301
|
//# sourceMappingURL=server.mjs.map
|