orizu 0.3.3 → 0.3.5
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 +19 -0
- package/dist/index.js +359 -20
- package/dist/preview-runtime.js +857 -0
- package/package.json +10 -1
- package/src/index.ts +448 -21
- package/src/preview-runtime.ts +976 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +2 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +22 -56
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +31 -6
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
|
+
export const PREVIEW_RUNTIME_REGISTRY_VERSION = 1;
|
|
7
|
+
// BEGIN_ORIZU_PREVIEW_IMPORT_REGISTRY
|
|
8
|
+
export const PREVIEW_ALLOWED_IMPORTS = {
|
|
9
|
+
'@/components/ui/button': ['Button'],
|
|
10
|
+
'@/components/ui/card': ['Card'],
|
|
11
|
+
'@/components/ui/carousel': ['Carousel'],
|
|
12
|
+
'@/components/ui/checkbox': ['Checkbox'],
|
|
13
|
+
'@/components/ui/form': ['Form'],
|
|
14
|
+
'@/components/ui/input': ['Input'],
|
|
15
|
+
'@/components/ui/label': ['Label'],
|
|
16
|
+
'@/components/ui/progress': ['Progress'],
|
|
17
|
+
'@/components/ui/radio-group': ['RadioGroup', 'RadioGroupItem'],
|
|
18
|
+
'@/components/ui/scroll-area': ['ScrollArea'],
|
|
19
|
+
'@/components/ui/select': ['Select', 'SelectContent', 'SelectItem', 'SelectTrigger', 'SelectValue'],
|
|
20
|
+
'@/components/ui/separator': ['Separator'],
|
|
21
|
+
'@/components/ui/switch': ['Switch'],
|
|
22
|
+
'@/components/ui/table': ['Table', 'TableBody', 'TableCell', 'TableHead', 'TableHeader', 'TableRow', 'TableCaption', 'TableFooter'],
|
|
23
|
+
'@/components/ui/tabs': ['Tabs', 'TabsContent', 'TabsList', 'TabsTrigger'],
|
|
24
|
+
'@/components/ui/textarea': ['Textarea'],
|
|
25
|
+
'@/components/ui/toggle': ['Toggle'],
|
|
26
|
+
'@/components/ui/toggle-group': ['ToggleGroup', 'ToggleGroupItem'],
|
|
27
|
+
'@/components/ui/tooltip': ['Tooltip', 'TooltipContent', 'TooltipProvider', 'TooltipTrigger'],
|
|
28
|
+
'@/components/base/content/TextContent': ['TextContent', 'default'],
|
|
29
|
+
'@/components/base/content/CodeBlock': ['CodeBlock', 'default'],
|
|
30
|
+
'@/components/base/content/ConversationView': ['AssistantMessageBlock', 'ContextMessageBlock', 'ConversationMessageBlock', 'ConversationView', 'ReasoningMessageBlock', 'SystemMessageBlock', 'ToolCallBlock', 'ToolResultBlock', 'UserMessageBlock', 'default'],
|
|
31
|
+
'@/components/base/content/ContentRenderer': ['ContentRenderer', 'default'],
|
|
32
|
+
'@/components/base/content/Prose': ['Prose', 'default'],
|
|
33
|
+
'@/components/base/behaviors/Annotatable': ['Annotatable', 'default'],
|
|
34
|
+
'@/components/base/behaviors/Reactable': ['Reactable', 'default'],
|
|
35
|
+
'@/components/base/input/CommentBox': ['CommentBox'],
|
|
36
|
+
'@/components/base/input/CriterionRating': ['CriterionRating'],
|
|
37
|
+
'@/components/base/input/LikertScale': ['LikertScale'],
|
|
38
|
+
'@/components/base/input/NumericRating': ['NumericRating'],
|
|
39
|
+
'@/components/base/input/RatingSelector': ['RatingSelector'],
|
|
40
|
+
'@/components/base/input/StarRating': ['StarRating'],
|
|
41
|
+
'@/components/base/input/TagPicker': ['TagPicker'],
|
|
42
|
+
'@/components/base/input/ThumbsRating': ['ThumbsRating'],
|
|
43
|
+
'@/components/base/ui/ComparisonPanel': ['ComparisonPanel'],
|
|
44
|
+
'@/components/base/ui/DraggableItem': ['DraggableItem'],
|
|
45
|
+
'@/components/templates/classification/TagSelector': ['TagSelector'],
|
|
46
|
+
'@/components/templates/comparison/SideBySideComparison': ['SideBySideComparison'],
|
|
47
|
+
'@/components/templates/correction/CorrectionTask': ['CorrectionTask'],
|
|
48
|
+
'@/components/templates/code/CodeComparison': ['CodeComparison'],
|
|
49
|
+
'@/components/templates/qa/ContextualQA': ['ContextualQA'],
|
|
50
|
+
'@/components/templates/ranking/RankingList': ['RankingList'],
|
|
51
|
+
'@/components/templates/rating/SingleItemRater': ['SingleItemRater'],
|
|
52
|
+
};
|
|
53
|
+
const SUPPORTED_SCHEMA_KEYS = new Set(['type', 'required', 'properties', 'items', 'enum']);
|
|
54
|
+
export function validatePreviewInputs(options) {
|
|
55
|
+
const issues = [];
|
|
56
|
+
const source = readFileSync(options.filePath, 'utf8');
|
|
57
|
+
validateImports(source, issues);
|
|
58
|
+
validateDefaultExport(source, options.componentName, issues);
|
|
59
|
+
validateSchema(options.inputSchema, 'input schema', issues);
|
|
60
|
+
validateSchema(options.outputSchema, 'output schema', issues);
|
|
61
|
+
validatePayload(options.inputSchema, options.sampleRow, 'sample row', issues);
|
|
62
|
+
return issues;
|
|
63
|
+
}
|
|
64
|
+
export async function runLocalAppPreview(options) {
|
|
65
|
+
const issues = validatePreviewInputs(options);
|
|
66
|
+
const errors = issues.filter(issue => issue.level === 'error');
|
|
67
|
+
if (errors.length > 0) {
|
|
68
|
+
throw new Error(errors.map(issue => issue.message).join('\n'));
|
|
69
|
+
}
|
|
70
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'orizu-app-preview-'));
|
|
71
|
+
const appPath = realpathSync(options.filePath);
|
|
72
|
+
const repoRoot = options.forceSnapshot
|
|
73
|
+
? null
|
|
74
|
+
: findPreviewRepoRoot(options.cwd || process.cwd(), appPath);
|
|
75
|
+
const cliPackageRoot = findCliPackageRoot();
|
|
76
|
+
const entryPath = join(tempDir, 'entry.tsx');
|
|
77
|
+
const cssPath = join(tempDir, 'preview.css');
|
|
78
|
+
const htmlPath = join(tempDir, 'index.html');
|
|
79
|
+
const outDir = join(tempDir, 'dist');
|
|
80
|
+
writeFileSync(cssPath, await buildPreviewCss(repoRoot, appPath), 'utf8');
|
|
81
|
+
writeFileSync(entryPath, buildEntrySource(appPath, options.sampleRow), 'utf8');
|
|
82
|
+
writeFileSync(htmlPath, buildPreviewHtml(), 'utf8');
|
|
83
|
+
let server = null;
|
|
84
|
+
try {
|
|
85
|
+
const esbuild = await import('esbuild').catch(() => null);
|
|
86
|
+
if (!esbuild) {
|
|
87
|
+
throw new Error('Local preview requires esbuild. Install project dependencies with `bun install` or run from an Orizu checkout.');
|
|
88
|
+
}
|
|
89
|
+
await esbuild.build({
|
|
90
|
+
entryPoints: [entryPath],
|
|
91
|
+
bundle: true,
|
|
92
|
+
outdir: outDir,
|
|
93
|
+
format: 'esm',
|
|
94
|
+
platform: 'browser',
|
|
95
|
+
target: 'es2020',
|
|
96
|
+
jsx: 'automatic',
|
|
97
|
+
jsxImportSource: 'react',
|
|
98
|
+
loader: {
|
|
99
|
+
'.css': 'css',
|
|
100
|
+
'.tsx': 'tsx',
|
|
101
|
+
'.ts': 'ts',
|
|
102
|
+
},
|
|
103
|
+
absWorkingDir: repoRoot || cliPackageRoot || process.cwd(),
|
|
104
|
+
nodePaths: [
|
|
105
|
+
...(repoRoot ? [join(repoRoot, 'node_modules')] : []),
|
|
106
|
+
...(cliPackageRoot ? findNodeModuleDirs(cliPackageRoot) : []),
|
|
107
|
+
],
|
|
108
|
+
alias: repoRoot ? { '@': repoRoot } : undefined,
|
|
109
|
+
plugins: repoRoot ? undefined : [createPreviewSnapshotPlugin()],
|
|
110
|
+
logLevel: 'silent',
|
|
111
|
+
});
|
|
112
|
+
let url = pathToFileURL(htmlPath).href;
|
|
113
|
+
try {
|
|
114
|
+
server = await serveDirectory(tempDir);
|
|
115
|
+
url = `http://127.0.0.1:${server.port}/index.html`;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
server = null;
|
|
119
|
+
}
|
|
120
|
+
const playwright = options.playwright || await loadPlaywright();
|
|
121
|
+
const chromiumExecutablePath = 'chromiumExecutablePath' in options
|
|
122
|
+
? options.chromiumExecutablePath
|
|
123
|
+
: resolveChromiumExecutablePath();
|
|
124
|
+
const browser = await playwright.chromium.launch({
|
|
125
|
+
headless: !options.headed,
|
|
126
|
+
...(chromiumExecutablePath ? { executablePath: chromiumExecutablePath } : {}),
|
|
127
|
+
});
|
|
128
|
+
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
|
129
|
+
try {
|
|
130
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
131
|
+
await page.waitForSelector('[data-orizu-preview-ready="true"]', { timeout: 15_000 });
|
|
132
|
+
if (options.screenshotPath) {
|
|
133
|
+
await page.screenshot({ path: options.screenshotPath, fullPage: true });
|
|
134
|
+
}
|
|
135
|
+
if (options.keepOpen) {
|
|
136
|
+
await waitForPreviewClose(page, browser);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (!options.keepOpen) {
|
|
141
|
+
await browser.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
url,
|
|
146
|
+
screenshotPath: options.screenshotPath,
|
|
147
|
+
warnings: issues.filter(issue => issue.level === 'warn').map(issue => issue.message),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await server?.close();
|
|
152
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function waitForPreviewClose(page, browser) {
|
|
156
|
+
return new Promise(resolveClose => {
|
|
157
|
+
const cleanups = [];
|
|
158
|
+
let settled = false;
|
|
159
|
+
const done = () => {
|
|
160
|
+
if (settled)
|
|
161
|
+
return;
|
|
162
|
+
settled = true;
|
|
163
|
+
for (const cleanup of cleanups)
|
|
164
|
+
cleanup();
|
|
165
|
+
resolveClose();
|
|
166
|
+
};
|
|
167
|
+
const listen = (target, event) => {
|
|
168
|
+
if (typeof target.once === 'function') {
|
|
169
|
+
target.once(event, done);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (typeof target.on === 'function') {
|
|
173
|
+
target.on(event, done);
|
|
174
|
+
if (typeof target.off === 'function') {
|
|
175
|
+
cleanups.push(() => target.off?.(event, done));
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
const listening = [
|
|
182
|
+
listen(page, 'close'),
|
|
183
|
+
listen(browser, 'disconnected'),
|
|
184
|
+
].some(Boolean);
|
|
185
|
+
if (!listening) {
|
|
186
|
+
page.waitForTimeout(24 * 60 * 60 * 1000).then(done, done);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function validateImports(source, issues) {
|
|
191
|
+
const importRegex = /import\s+(?:(\w+)(?:\s*,\s*)?)?(?:\{([^}]+)\})?\s+from\s+['"`]([^'"`]+)['"`]/g;
|
|
192
|
+
for (const match of source.matchAll(importRegex)) {
|
|
193
|
+
const defaultImport = match[1];
|
|
194
|
+
const namedImports = match[2]
|
|
195
|
+
? match[2].split(',').map(name => name.trim().replace(/\s+as\s+\w+/, '')).filter(Boolean)
|
|
196
|
+
: [];
|
|
197
|
+
const importPath = match[3];
|
|
198
|
+
if (importPath === 'react' || importPath === 'react-dom' || importPath.startsWith('react/')) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const allowed = PREVIEW_ALLOWED_IMPORTS[importPath];
|
|
202
|
+
if (!allowed) {
|
|
203
|
+
issues.push({ level: 'error', message: `Import not available in Orizu apps: ${importPath}` });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (defaultImport && !allowed.includes('default') && !allowed.includes(defaultImport)) {
|
|
207
|
+
issues.push({
|
|
208
|
+
level: 'error',
|
|
209
|
+
message: `Default import '${defaultImport}' is not available from '${importPath}'.`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
for (const name of namedImports) {
|
|
213
|
+
if (!allowed.includes(name)) {
|
|
214
|
+
issues.push({
|
|
215
|
+
level: 'error',
|
|
216
|
+
message: `Named import '${name}' is not available from '${importPath}'.`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function validateDefaultExport(source, componentName, issues) {
|
|
223
|
+
const resolved = resolveDefaultExport(source);
|
|
224
|
+
if (!resolved) {
|
|
225
|
+
issues.push({
|
|
226
|
+
level: 'error',
|
|
227
|
+
message: 'App must default export a named React component.',
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (componentName && resolved.name !== componentName) {
|
|
232
|
+
issues.push({
|
|
233
|
+
level: 'error',
|
|
234
|
+
message: `Component name mismatch: expected '${componentName}', found '${resolved.name}'.`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (resolved.params === null) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const params = resolved.params.trim();
|
|
241
|
+
if (!params.startsWith('{') || !params.includes('inputData') || !params.includes('onComplete')) {
|
|
242
|
+
issues.push({
|
|
243
|
+
level: 'error',
|
|
244
|
+
message: "Default component must destructure contract props 'inputData' and 'onComplete'.",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (/\bdata\b|\bonSubmit\b/.test(params)) {
|
|
248
|
+
issues.push({
|
|
249
|
+
level: 'error',
|
|
250
|
+
message: "Use Orizu contract props instead of deprecated root props 'data' or 'onSubmit'.",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function resolveDefaultExport(source) {
|
|
255
|
+
const directFunction = source.match(/export\s+default\s+function\s+([A-Za-z_$][\w$]*)\s*\(([\s\S]*?)\)/);
|
|
256
|
+
if (directFunction?.[1]) {
|
|
257
|
+
return { name: directFunction[1], params: directFunction[2] ?? '' };
|
|
258
|
+
}
|
|
259
|
+
const directClass = source.match(/export\s+default\s+class\s+([A-Za-z_$][\w$]*)/);
|
|
260
|
+
if (directClass?.[1]) {
|
|
261
|
+
return { name: directClass[1], params: null };
|
|
262
|
+
}
|
|
263
|
+
const exportExpression = source.match(/export\s+default\s+([^\n;]+)/);
|
|
264
|
+
const identifier = exportExpression?.[1]
|
|
265
|
+
? unwrapDefaultExportExpression(exportExpression[1])
|
|
266
|
+
: null;
|
|
267
|
+
if (!identifier) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const escapedIdentifier = escapeRegExp(identifier);
|
|
271
|
+
const functionDeclaration = source.match(new RegExp(`function\\s+${escapedIdentifier}(?![A-Za-z0-9_$])\\s*\\(([\\s\\S]*?)\\)`));
|
|
272
|
+
if (functionDeclaration) {
|
|
273
|
+
return { name: identifier, params: functionDeclaration[1] ?? '' };
|
|
274
|
+
}
|
|
275
|
+
const arrowDeclaration = source.match(new RegExp(`(?:const|let|var)\\s+${escapedIdentifier}(?![A-Za-z0-9_$])\\s*=\\s*(?:\\(([^)]*)\\)|([^=()\\n]+))\\s*(?::\\s*[^=\\n]+)?\\s*=>`));
|
|
276
|
+
if (arrowDeclaration) {
|
|
277
|
+
return { name: identifier, params: arrowDeclaration[1] ?? arrowDeclaration[2] ?? '' };
|
|
278
|
+
}
|
|
279
|
+
const classDeclaration = source.match(new RegExp(`class\\s+${escapedIdentifier}(?![A-Za-z0-9_$])`));
|
|
280
|
+
if (classDeclaration) {
|
|
281
|
+
return { name: identifier, params: null };
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
function unwrapDefaultExportExpression(expression) {
|
|
286
|
+
const trimmed = expression
|
|
287
|
+
.replace(/\/\*[\s\S]*?\*\/\s*$/, '')
|
|
288
|
+
.replace(/\/\/.*$/, '')
|
|
289
|
+
.trim();
|
|
290
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(trimmed)) {
|
|
291
|
+
return trimmed;
|
|
292
|
+
}
|
|
293
|
+
const wrapperMatch = trimmed.match(/^(?:[A-Za-z_$][A-Za-z0-9_$]*\.)?[A-Za-z_$][A-Za-z0-9_$]*\(\s*(.+)\s*\)$/);
|
|
294
|
+
if (!wrapperMatch?.[1]) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return unwrapDefaultExportExpression(wrapperMatch[1]);
|
|
298
|
+
}
|
|
299
|
+
function escapeRegExp(value) {
|
|
300
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
301
|
+
}
|
|
302
|
+
function validateSchema(schema, label, issues, path = label) {
|
|
303
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
304
|
+
issues.push({ level: 'error', message: `${path} must be a JSON object.` });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const obj = schema;
|
|
308
|
+
for (const key of Object.keys(obj)) {
|
|
309
|
+
if (!SUPPORTED_SCHEMA_KEYS.has(key)) {
|
|
310
|
+
issues.push({ level: 'warn', message: `${path}.${key} is outside the supported schema subset and is ignored.` });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if ('properties' in obj && (!obj.properties || typeof obj.properties !== 'object' || Array.isArray(obj.properties))) {
|
|
314
|
+
issues.push({ level: 'error', message: `${path}.properties must be an object.` });
|
|
315
|
+
}
|
|
316
|
+
if ('required' in obj && !Array.isArray(obj.required)) {
|
|
317
|
+
issues.push({ level: 'error', message: `${path}.required must be an array.` });
|
|
318
|
+
}
|
|
319
|
+
if (obj.properties && typeof obj.properties === 'object' && !Array.isArray(obj.properties)) {
|
|
320
|
+
for (const [name, child] of Object.entries(obj.properties)) {
|
|
321
|
+
validateSchema(child, label, issues, `${path}.properties.${name}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (obj.items) {
|
|
325
|
+
validateSchema(obj.items, label, issues, `${path}.items`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function validatePayload(schema, payload, path, issues) {
|
|
329
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
330
|
+
return;
|
|
331
|
+
const obj = schema;
|
|
332
|
+
if (Array.isArray(obj.enum) && !obj.enum.includes(payload)) {
|
|
333
|
+
issues.push({ level: 'error', message: `${path} must be one of: ${obj.enum.join(', ')}` });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (obj.type === 'object') {
|
|
337
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
338
|
+
issues.push({ level: 'error', message: `${path} must be an object.` });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const record = payload;
|
|
342
|
+
if (Array.isArray(obj.required)) {
|
|
343
|
+
for (const key of obj.required) {
|
|
344
|
+
if (typeof key === 'string' && !(key in record)) {
|
|
345
|
+
issues.push({ level: 'error', message: `${path}.${key} is required.` });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (obj.properties && typeof obj.properties === 'object' && !Array.isArray(obj.properties)) {
|
|
350
|
+
for (const [key, child] of Object.entries(obj.properties)) {
|
|
351
|
+
if (key in record)
|
|
352
|
+
validatePayload(child, record[key], `${path}.${key}`, issues);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (obj.type === 'array') {
|
|
357
|
+
if (!Array.isArray(payload)) {
|
|
358
|
+
issues.push({ level: 'error', message: `${path} must be an array.` });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (obj.items) {
|
|
362
|
+
payload.forEach((item, index) => validatePayload(obj.items, item, `${path}[${index}]`, issues));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (obj.type === 'string' && typeof payload !== 'string') {
|
|
366
|
+
issues.push({ level: 'error', message: `${path} must be a string.` });
|
|
367
|
+
}
|
|
368
|
+
if (obj.type === 'number' && typeof payload !== 'number') {
|
|
369
|
+
issues.push({ level: 'error', message: `${path} must be a number.` });
|
|
370
|
+
}
|
|
371
|
+
if (obj.type === 'integer' && (!Number.isInteger(payload))) {
|
|
372
|
+
issues.push({ level: 'error', message: `${path} must be an integer.` });
|
|
373
|
+
}
|
|
374
|
+
if (obj.type === 'boolean' && typeof payload !== 'boolean') {
|
|
375
|
+
issues.push({ level: 'error', message: `${path} must be a boolean.` });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function findPreviewRepoRoot(cwd, appPath) {
|
|
379
|
+
for (const start of [cwd, dirname(appPath), dirname(fileURLPath())]) {
|
|
380
|
+
let current = resolve(start);
|
|
381
|
+
while (true) {
|
|
382
|
+
if (existsSync(join(current, 'components')) &&
|
|
383
|
+
existsSync(join(current, 'app', 'globals.css')) &&
|
|
384
|
+
existsSync(join(current, 'lib', 'available-components.ts'))) {
|
|
385
|
+
return current;
|
|
386
|
+
}
|
|
387
|
+
const parent = dirname(current);
|
|
388
|
+
if (parent === current)
|
|
389
|
+
break;
|
|
390
|
+
current = parent;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
function fileURLPath() {
|
|
396
|
+
return fileURLToPath(new URL('.', import.meta.url));
|
|
397
|
+
}
|
|
398
|
+
function findCliPackageRoot() {
|
|
399
|
+
let current = resolve(dirname(fileURLPath()));
|
|
400
|
+
while (true) {
|
|
401
|
+
if (existsSync(join(current, 'package.json')) &&
|
|
402
|
+
(existsSync(join(current, 'dist', 'index.js')) || existsSync(join(current, 'src', 'index.ts')))) {
|
|
403
|
+
return current;
|
|
404
|
+
}
|
|
405
|
+
const parent = dirname(current);
|
|
406
|
+
if (parent === current)
|
|
407
|
+
break;
|
|
408
|
+
current = parent;
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
function findNodeModuleDirs(start) {
|
|
413
|
+
const dirs = [];
|
|
414
|
+
let current = resolve(start);
|
|
415
|
+
while (true) {
|
|
416
|
+
const candidate = join(current, 'node_modules');
|
|
417
|
+
if (existsSync(candidate)) {
|
|
418
|
+
dirs.push(candidate);
|
|
419
|
+
}
|
|
420
|
+
const parent = dirname(current);
|
|
421
|
+
if (parent === current)
|
|
422
|
+
break;
|
|
423
|
+
current = parent;
|
|
424
|
+
}
|
|
425
|
+
return dirs;
|
|
426
|
+
}
|
|
427
|
+
function buildEntrySource(appPath, sampleRow) {
|
|
428
|
+
return `
|
|
429
|
+
import React from 'react'
|
|
430
|
+
import { createRoot } from 'react-dom/client'
|
|
431
|
+
import App from ${JSON.stringify(appPath)}
|
|
432
|
+
import ${JSON.stringify('./preview.css')}
|
|
433
|
+
|
|
434
|
+
const inputData = ${JSON.stringify(sampleRow)}
|
|
435
|
+
const root = createRoot(document.getElementById('root'))
|
|
436
|
+
|
|
437
|
+
function PreviewShell() {
|
|
438
|
+
const [submissions, setSubmissions] = React.useState([])
|
|
439
|
+
const onComplete = React.useCallback((payload) => {
|
|
440
|
+
window.__orizuSubmissions = [...(window.__orizuSubmissions || []), payload]
|
|
441
|
+
setSubmissions(window.__orizuSubmissions)
|
|
442
|
+
}, [])
|
|
443
|
+
return React.createElement(
|
|
444
|
+
'main',
|
|
445
|
+
{ className: 'orizu-preview-shell', 'data-orizu-preview-ready': 'true' },
|
|
446
|
+
React.createElement(
|
|
447
|
+
'div',
|
|
448
|
+
{ className: 'orizu-preview-app' },
|
|
449
|
+
React.createElement(App, { inputData, initialValues: {}, onComplete })
|
|
450
|
+
),
|
|
451
|
+
React.createElement(
|
|
452
|
+
'aside',
|
|
453
|
+
{ className: 'orizu-preview-submissions', 'aria-label': 'Preview submissions' },
|
|
454
|
+
React.createElement('h2', null, 'Submissions'),
|
|
455
|
+
React.createElement('pre', null, JSON.stringify(submissions, null, 2))
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
root.render(React.createElement(PreviewShell))
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
function buildPreviewHtml() {
|
|
464
|
+
return `<!doctype html>
|
|
465
|
+
<html lang="en">
|
|
466
|
+
<head>
|
|
467
|
+
<meta charset="utf-8" />
|
|
468
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
469
|
+
<title>Orizu app preview</title>
|
|
470
|
+
<link rel="stylesheet" href="./dist/entry.css" />
|
|
471
|
+
</head>
|
|
472
|
+
<body>
|
|
473
|
+
<div id="root"></div>
|
|
474
|
+
<script type="module" src="./dist/entry.js"></script>
|
|
475
|
+
</body>
|
|
476
|
+
</html>
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
export async function buildPreviewCss(repoRoot, appPath) {
|
|
480
|
+
const fallbackCss = buildPreviewChromeCss();
|
|
481
|
+
try {
|
|
482
|
+
const postcss = (await import('postcss')).default;
|
|
483
|
+
const tailwind = (await import('@tailwindcss/postcss')).default;
|
|
484
|
+
const cliPackageRoot = findCliPackageRoot();
|
|
485
|
+
const globalsPath = repoRoot
|
|
486
|
+
? join(repoRoot, 'app', 'globals.css')
|
|
487
|
+
: join(cliPackageRoot || dirname(appPath), 'preview-runtime.css');
|
|
488
|
+
const globalsCss = repoRoot
|
|
489
|
+
? readFileSync(globalsPath, 'utf8')
|
|
490
|
+
: buildSnapshotTailwindCss();
|
|
491
|
+
const sourceCss = [
|
|
492
|
+
`@source ${JSON.stringify(appPath)};`,
|
|
493
|
+
...(repoRoot
|
|
494
|
+
? [
|
|
495
|
+
`@source ${JSON.stringify(join(repoRoot, 'components/**/*.{ts,tsx}'))};`,
|
|
496
|
+
`@source ${JSON.stringify(join(repoRoot, 'app/**/*.{ts,tsx}'))};`,
|
|
497
|
+
]
|
|
498
|
+
: []),
|
|
499
|
+
globalsCss,
|
|
500
|
+
fallbackCss,
|
|
501
|
+
].join('\n');
|
|
502
|
+
const result = await postcss([tailwind()]).process(sourceCss, { from: globalsPath });
|
|
503
|
+
return result.css;
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return fallbackCss;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function buildPreviewChromeCss() {
|
|
510
|
+
return `
|
|
511
|
+
:root {
|
|
512
|
+
color-scheme: light;
|
|
513
|
+
--background: #fbfaf8;
|
|
514
|
+
--foreground: #1f1a17;
|
|
515
|
+
--card: #ffffff;
|
|
516
|
+
--border: #e7ded6;
|
|
517
|
+
--muted: #f2eeea;
|
|
518
|
+
--muted-foreground: #756b62;
|
|
519
|
+
--primary: #d95f2b;
|
|
520
|
+
--primary-foreground: #fffaf7;
|
|
521
|
+
--radius: 10px;
|
|
522
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
523
|
+
}
|
|
524
|
+
* { box-sizing: border-box; }
|
|
525
|
+
body { margin: 0; background: var(--background); color: var(--foreground); }
|
|
526
|
+
button, input, textarea, select { font: inherit; }
|
|
527
|
+
.orizu-preview-shell { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; min-height: 100vh; padding: 24px; }
|
|
528
|
+
.orizu-preview-app { min-width: 0; padding: 24px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); }
|
|
529
|
+
.orizu-preview-submissions { border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); padding: 16px; }
|
|
530
|
+
.orizu-preview-submissions h2 { margin: 0 0 12px; font-size: 14px; }
|
|
531
|
+
.orizu-preview-submissions pre { overflow: auto; margin: 0; color: var(--muted-foreground); font-size: 12px; white-space: pre-wrap; }
|
|
532
|
+
@media (max-width: 900px) { .orizu-preview-shell { grid-template-columns: 1fr; padding: 12px; } }
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
function buildSnapshotTailwindCss() {
|
|
536
|
+
return `
|
|
537
|
+
@import 'tailwindcss';
|
|
538
|
+
|
|
539
|
+
@theme {
|
|
540
|
+
--color-background: #fbfaf8;
|
|
541
|
+
--color-foreground: #1f1a17;
|
|
542
|
+
--color-card: #ffffff;
|
|
543
|
+
--color-card-foreground: #1f1a17;
|
|
544
|
+
--color-popover: #ffffff;
|
|
545
|
+
--color-popover-foreground: #1f1a17;
|
|
546
|
+
--color-primary: #d95f2b;
|
|
547
|
+
--color-primary-foreground: #fffaf7;
|
|
548
|
+
--color-secondary: #f2eeea;
|
|
549
|
+
--color-secondary-foreground: #1f1a17;
|
|
550
|
+
--color-muted: #f2eeea;
|
|
551
|
+
--color-muted-foreground: #756b62;
|
|
552
|
+
--color-accent: #f5c15b;
|
|
553
|
+
--color-accent-foreground: #1f1a17;
|
|
554
|
+
--color-destructive: #c8442a;
|
|
555
|
+
--color-destructive-foreground: #ffffff;
|
|
556
|
+
--color-border: #e7ded6;
|
|
557
|
+
--color-input: #f2eeea;
|
|
558
|
+
--color-ring: #d95f2b;
|
|
559
|
+
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
560
|
+
--font-mono: "Geist Mono", Menlo, Monaco, Consolas, monospace;
|
|
561
|
+
--font-serif: Lora, Georgia, serif;
|
|
562
|
+
--radius-sm: 6px;
|
|
563
|
+
--radius-md: 8px;
|
|
564
|
+
--radius-lg: 10px;
|
|
565
|
+
--radius-xl: 14px;
|
|
566
|
+
}
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
function createPreviewSnapshotPlugin() {
|
|
570
|
+
const modules = buildPreviewSnapshotModules();
|
|
571
|
+
return {
|
|
572
|
+
name: 'orizu-preview-runtime-snapshot',
|
|
573
|
+
setup(build) {
|
|
574
|
+
build.onResolve({ filter: /^@\/components\// }, (args) => {
|
|
575
|
+
if (modules[args.path]) {
|
|
576
|
+
return { path: args.path, namespace: 'orizu-preview-snapshot' };
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
});
|
|
580
|
+
build.onLoad({ filter: /.*/, namespace: 'orizu-preview-snapshot' }, (args) => {
|
|
581
|
+
const contents = modules[args.path];
|
|
582
|
+
if (!contents) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
contents,
|
|
587
|
+
loader: 'tsx',
|
|
588
|
+
resolveDir: findCliPackageRoot() || process.cwd(),
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function buildPreviewSnapshotModules() {
|
|
595
|
+
const primitiveSource = `
|
|
596
|
+
import React from 'react'
|
|
597
|
+
|
|
598
|
+
function cn(...values) {
|
|
599
|
+
return values.filter(Boolean).join(' ')
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function primitive(tag, baseClass = '') {
|
|
603
|
+
return React.forwardRef(function SnapshotPrimitive({ children, className, asChild, ...props }, ref) {
|
|
604
|
+
const element = asChild && React.isValidElement(children)
|
|
605
|
+
? React.cloneElement(children, {
|
|
606
|
+
...props,
|
|
607
|
+
ref,
|
|
608
|
+
className: cn(baseClass, children.props?.className, className),
|
|
609
|
+
})
|
|
610
|
+
: React.createElement(tag, { ...props, ref, className: cn(baseClass, className) }, children)
|
|
611
|
+
return element
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function field(tag, baseClass = '') {
|
|
616
|
+
return React.forwardRef(function SnapshotField({ className, ...props }, ref) {
|
|
617
|
+
return React.createElement(tag, { ...props, ref, className: cn(baseClass, className) })
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
`;
|
|
621
|
+
const contentSource = `
|
|
622
|
+
import React from 'react'
|
|
623
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
624
|
+
export function TextContent({ content, children, className, ...props }) {
|
|
625
|
+
return <div {...props} className={cn('whitespace-pre-wrap text-sm leading-relaxed', className)}>{children ?? content}</div>
|
|
626
|
+
}
|
|
627
|
+
export default TextContent
|
|
628
|
+
`;
|
|
629
|
+
const codeBlockSource = `
|
|
630
|
+
import React from 'react'
|
|
631
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
632
|
+
export function CodeBlock({ code, children, className, ...props }) {
|
|
633
|
+
return <pre {...props} className={cn('overflow-auto rounded-md border bg-muted p-3 font-mono text-xs', className)}><code>{children ?? code}</code></pre>
|
|
634
|
+
}
|
|
635
|
+
export default CodeBlock
|
|
636
|
+
`;
|
|
637
|
+
const proseSource = `
|
|
638
|
+
import React from 'react'
|
|
639
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
640
|
+
export function Prose({ children, className, ...props }) {
|
|
641
|
+
return <div {...props} className={cn('prose prose-sm max-w-none text-foreground', className)}>{children}</div>
|
|
642
|
+
}
|
|
643
|
+
export default Prose
|
|
644
|
+
`;
|
|
645
|
+
const contentRendererSource = `
|
|
646
|
+
import React from 'react'
|
|
647
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
648
|
+
export function ContentRenderer({ content, children, className, ...props }) {
|
|
649
|
+
return <div {...props} className={cn('whitespace-pre-wrap text-sm leading-relaxed', className)}>{children ?? content}</div>
|
|
650
|
+
}
|
|
651
|
+
export default ContentRenderer
|
|
652
|
+
`;
|
|
653
|
+
const conversationSource = `
|
|
654
|
+
import React from 'react'
|
|
655
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
656
|
+
function MessageBlock({ children, className, role, message, ...props }) {
|
|
657
|
+
return <div {...props} className={cn('rounded-md border bg-card p-3 text-sm', className)}><div className="mb-1 text-xs font-semibold uppercase text-muted-foreground">{role}</div>{children ?? message?.content ?? message}</div>
|
|
658
|
+
}
|
|
659
|
+
export function ConversationView({ messages = [], children, className, ...props }) {
|
|
660
|
+
return <div {...props} className={cn('space-y-3', className)}>{children ?? messages.map((message, index) => <MessageBlock key={index} message={message} role={message?.role ?? 'message'} />)}</div>
|
|
661
|
+
}
|
|
662
|
+
export const ConversationMessageBlock = MessageBlock
|
|
663
|
+
export const AssistantMessageBlock = (props) => <MessageBlock {...props} role="assistant" />
|
|
664
|
+
export const UserMessageBlock = (props) => <MessageBlock {...props} role="user" />
|
|
665
|
+
export const SystemMessageBlock = (props) => <MessageBlock {...props} role="system" />
|
|
666
|
+
export const ContextMessageBlock = (props) => <MessageBlock {...props} role="context" />
|
|
667
|
+
export const ReasoningMessageBlock = (props) => <MessageBlock {...props} role="reasoning" />
|
|
668
|
+
export const ToolCallBlock = (props) => <MessageBlock {...props} role="tool call" />
|
|
669
|
+
export const ToolResultBlock = (props) => <MessageBlock {...props} role="tool result" />
|
|
670
|
+
export default ConversationView
|
|
671
|
+
`;
|
|
672
|
+
const behaviorSource = `
|
|
673
|
+
import React from 'react'
|
|
674
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
675
|
+
export function Annotatable({ children, className, ...props }) {
|
|
676
|
+
return <span {...props} className={cn('rounded-sm bg-yellow-50 ring-1 ring-yellow-200', className)}>{children}</span>
|
|
677
|
+
}
|
|
678
|
+
export function Reactable({ children, className, reactions, ...props }) {
|
|
679
|
+
return <span {...props} className={cn('inline-flex items-center gap-1', className)}>{children}{Array.isArray(reactions) ? reactions.map((reaction, index) => <span key={index} className="rounded-full border px-1 text-xs">{reaction?.emoji ?? reaction}</span>) : null}</span>
|
|
680
|
+
}
|
|
681
|
+
`;
|
|
682
|
+
const inputSource = `
|
|
683
|
+
import React from 'react'
|
|
684
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
685
|
+
export function CommentBox({ value, onChange, placeholder, className, ...props }) {
|
|
686
|
+
return <textarea {...props} value={value} placeholder={placeholder} onChange={(event) => onChange?.(event.target.value)} className={cn('min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm', className)} />
|
|
687
|
+
}
|
|
688
|
+
export function NumericRating({ value, onChange, max = 5, className, ...props }) {
|
|
689
|
+
return <div {...props} className={cn('inline-flex gap-1', className)}>{Array.from({ length: max }, (_, i) => i + 1).map(n => <button key={n} type="button" onClick={() => onChange?.(n)} className={cn('rounded-md border px-3 py-1 text-sm', value === n && 'bg-primary text-primary-foreground')}>{n}</button>)}</div>
|
|
690
|
+
}
|
|
691
|
+
export const StarRating = NumericRating
|
|
692
|
+
export const LikertScale = NumericRating
|
|
693
|
+
export const RatingSelector = NumericRating
|
|
694
|
+
export function ThumbsRating({ value, onChange, className, ...props }) {
|
|
695
|
+
return <div {...props} className={cn('inline-flex gap-2', className)}><button type="button" onClick={() => onChange?.('up')} className={cn('rounded-md border px-3 py-1', value === 'up' && 'bg-primary text-primary-foreground')}>Up</button><button type="button" onClick={() => onChange?.('down')} className={cn('rounded-md border px-3 py-1', value === 'down' && 'bg-primary text-primary-foreground')}>Down</button></div>
|
|
696
|
+
}
|
|
697
|
+
export function CriterionRating({ criteria = [], value = {}, onChange, className, ...props }) {
|
|
698
|
+
return <div {...props} className={cn('space-y-2', className)}>{criteria.map((criterion) => <label key={criterion} className="flex items-center justify-between gap-3 text-sm"><span>{criterion}</span><NumericRating value={value[criterion]} onChange={(rating) => onChange?.({ ...value, [criterion]: rating })} /></label>)}</div>
|
|
699
|
+
}
|
|
700
|
+
export function TagPicker({ options = [], value = [], onChange, className, ...props }) {
|
|
701
|
+
return <div {...props} className={cn('flex flex-wrap gap-2', className)}>{options.map(option => { const selected = value.includes(option); return <button key={option} type="button" onClick={() => onChange?.(selected ? value.filter(v => v !== option) : [...value, option])} className={cn('rounded-full border px-3 py-1 text-sm', selected && 'bg-primary text-primary-foreground')}>{option}</button> })}</div>
|
|
702
|
+
}
|
|
703
|
+
`;
|
|
704
|
+
const templateSource = `
|
|
705
|
+
import React from 'react'
|
|
706
|
+
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
707
|
+
function Template({ title, children, className, inputData, onComplete, ...props }) {
|
|
708
|
+
return <section {...props} className={cn('rounded-lg border bg-card p-4 text-card-foreground', className)}><h2 className="mb-3 text-lg font-semibold">{title ?? inputData?.title ?? 'Preview template'}</h2>{children}<button type="button" onClick={() => onComplete?.({ reviewed: true })} className="mt-4 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground">Submit</button></section>
|
|
709
|
+
}
|
|
710
|
+
export const TagSelector = Template
|
|
711
|
+
export const SideBySideComparison = Template
|
|
712
|
+
export const CorrectionTask = Template
|
|
713
|
+
export const CodeComparison = Template
|
|
714
|
+
export const ContextualQA = Template
|
|
715
|
+
export const RankingList = Template
|
|
716
|
+
export const SingleItemRater = Template
|
|
717
|
+
`;
|
|
718
|
+
return {
|
|
719
|
+
'@/components/ui/button': `${primitiveSource}\nexport const Button = primitive('button', 'inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-sm hover:opacity-90 disabled:opacity-50')`,
|
|
720
|
+
'@/components/ui/card': `${primitiveSource}\nexport const Card = primitive('div', 'rounded-lg border bg-card text-card-foreground shadow-sm')`,
|
|
721
|
+
'@/components/ui/carousel': `${primitiveSource}\nexport const Carousel = primitive('div', 'relative overflow-hidden')`,
|
|
722
|
+
'@/components/ui/checkbox': `${primitiveSource}\nexport const Checkbox = field('input', 'h-4 w-4 rounded border border-input')`,
|
|
723
|
+
'@/components/ui/form': `${primitiveSource}\nexport const Form = primitive('form', 'space-y-4')`,
|
|
724
|
+
'@/components/ui/input': `${primitiveSource}\nexport const Input = field('input', 'h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm')`,
|
|
725
|
+
'@/components/ui/label': `${primitiveSource}\nexport const Label = primitive('label', 'text-sm font-medium leading-none')`,
|
|
726
|
+
'@/components/ui/progress': `${primitiveSource}\nexport const Progress = field('progress', 'h-2 w-full overflow-hidden rounded-full')`,
|
|
727
|
+
'@/components/ui/radio-group': `${primitiveSource}\nexport const RadioGroup = primitive('div', 'grid gap-2')\nexport const RadioGroupItem = field('input', 'h-4 w-4')`,
|
|
728
|
+
'@/components/ui/scroll-area': `${primitiveSource}\nexport const ScrollArea = primitive('div', 'overflow-auto')`,
|
|
729
|
+
'@/components/ui/select': `${primitiveSource}\nexport const Select = primitive('select', 'h-10 rounded-md border border-input bg-background px-3 py-2 text-sm')\nexport const SelectTrigger = primitive('button', 'inline-flex h-10 items-center justify-between rounded-md border px-3 py-2 text-sm')\nexport const SelectValue = primitive('span')\nexport const SelectContent = primitive('div', 'rounded-md border bg-popover p-1')\nexport const SelectItem = primitive('option')`,
|
|
730
|
+
'@/components/ui/separator': `${primitiveSource}\nexport const Separator = primitive('hr', 'border-border')`,
|
|
731
|
+
'@/components/ui/switch': `${primitiveSource}\nexport const Switch = field('input', 'h-5 w-9 rounded-full')`,
|
|
732
|
+
'@/components/ui/table': `${primitiveSource}\nexport const Table = primitive('table', 'w-full caption-bottom text-sm')\nexport const TableHeader = primitive('thead')\nexport const TableBody = primitive('tbody')\nexport const TableFooter = primitive('tfoot')\nexport const TableRow = primitive('tr', 'border-b')\nexport const TableHead = primitive('th', 'h-10 px-2 text-left font-medium')\nexport const TableCell = primitive('td', 'p-2 align-middle')\nexport const TableCaption = primitive('caption', 'mt-4 text-sm text-muted-foreground')`,
|
|
733
|
+
'@/components/ui/tabs': `${primitiveSource}\nexport const Tabs = primitive('div')\nexport const TabsList = primitive('div', 'inline-flex rounded-md bg-muted p-1')\nexport const TabsTrigger = primitive('button', 'rounded-sm px-3 py-1 text-sm')\nexport const TabsContent = primitive('div', 'mt-2')`,
|
|
734
|
+
'@/components/ui/textarea': `${primitiveSource}\nexport const Textarea = field('textarea', 'min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm')`,
|
|
735
|
+
'@/components/ui/toggle': `${primitiveSource}\nexport const Toggle = primitive('button', 'inline-flex items-center rounded-md border px-3 py-2 text-sm')`,
|
|
736
|
+
'@/components/ui/toggle-group': `${primitiveSource}\nexport const ToggleGroup = primitive('div', 'inline-flex rounded-md border')\nexport const ToggleGroupItem = primitive('button', 'px-3 py-2 text-sm')`,
|
|
737
|
+
'@/components/ui/tooltip': `${primitiveSource}\nexport const TooltipProvider = primitive('div')\nexport const Tooltip = primitive('span')\nexport const TooltipTrigger = primitive('span')\nexport const TooltipContent = primitive('span', 'rounded-md border bg-popover px-2 py-1 text-xs')`,
|
|
738
|
+
'@/components/base/content/TextContent': contentSource,
|
|
739
|
+
'@/components/base/content/CodeBlock': codeBlockSource,
|
|
740
|
+
'@/components/base/content/ConversationView': conversationSource,
|
|
741
|
+
'@/components/base/content/ContentRenderer': contentRendererSource,
|
|
742
|
+
'@/components/base/content/Prose': proseSource,
|
|
743
|
+
'@/components/base/behaviors/Annotatable': `${behaviorSource}\nexport default Annotatable`,
|
|
744
|
+
'@/components/base/behaviors/Reactable': `${behaviorSource}\nexport default Reactable`,
|
|
745
|
+
'@/components/base/input/CommentBox': inputSource,
|
|
746
|
+
'@/components/base/input/CriterionRating': inputSource,
|
|
747
|
+
'@/components/base/input/LikertScale': inputSource,
|
|
748
|
+
'@/components/base/input/NumericRating': inputSource,
|
|
749
|
+
'@/components/base/input/RatingSelector': inputSource,
|
|
750
|
+
'@/components/base/input/StarRating': inputSource,
|
|
751
|
+
'@/components/base/input/TagPicker': inputSource,
|
|
752
|
+
'@/components/base/input/ThumbsRating': inputSource,
|
|
753
|
+
'@/components/base/ui/ComparisonPanel': `${primitiveSource}\nexport const ComparisonPanel = primitive('div', 'grid gap-4 rounded-lg border bg-card p-4 md:grid-cols-2')`,
|
|
754
|
+
'@/components/base/ui/DraggableItem': `${primitiveSource}\nexport const DraggableItem = primitive('div', 'cursor-grab rounded-md border bg-card p-3')`,
|
|
755
|
+
'@/components/templates/classification/TagSelector': templateSource,
|
|
756
|
+
'@/components/templates/comparison/SideBySideComparison': templateSource,
|
|
757
|
+
'@/components/templates/correction/CorrectionTask': templateSource,
|
|
758
|
+
'@/components/templates/code/CodeComparison': templateSource,
|
|
759
|
+
'@/components/templates/qa/ContextualQA': templateSource,
|
|
760
|
+
'@/components/templates/ranking/RankingList': templateSource,
|
|
761
|
+
'@/components/templates/rating/SingleItemRater': templateSource,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
export function getPreviewSnapshotModulePaths() {
|
|
765
|
+
return Object.keys(buildPreviewSnapshotModules()).sort();
|
|
766
|
+
}
|
|
767
|
+
async function serveDirectory(root) {
|
|
768
|
+
const server = createServer((request, response) => {
|
|
769
|
+
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
770
|
+
const requestedPath = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
771
|
+
const filePath = resolve(root, `.${requestedPath}`);
|
|
772
|
+
const relativePath = relative(root, filePath);
|
|
773
|
+
if (relativePath.startsWith('..') || isAbsolute(relativePath) || !existsSync(filePath)) {
|
|
774
|
+
response.statusCode = 404;
|
|
775
|
+
response.end('Not found');
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
if (!statSync(filePath).isFile()) {
|
|
780
|
+
response.statusCode = 404;
|
|
781
|
+
response.end('Not found');
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
response.statusCode = 404;
|
|
787
|
+
response.end('Not found');
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const ext = basename(filePath).split('.').pop();
|
|
791
|
+
const contentType = ext === 'html' ? 'text/html; charset=utf-8' :
|
|
792
|
+
ext === 'js' ? 'text/javascript; charset=utf-8' :
|
|
793
|
+
ext === 'css' ? 'text/css; charset=utf-8' :
|
|
794
|
+
'application/octet-stream';
|
|
795
|
+
response.setHeader('content-type', contentType);
|
|
796
|
+
try {
|
|
797
|
+
response.end(readFileSync(filePath));
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
response.statusCode = 500;
|
|
801
|
+
response.end('Internal server error');
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
const port = await listenOnAvailablePort(server);
|
|
805
|
+
const address = server.address();
|
|
806
|
+
if (!address || typeof address === 'string') {
|
|
807
|
+
throw new Error('Unable to start local preview server.');
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
port,
|
|
811
|
+
close: () => new Promise(resolveClose => server.close(() => resolveClose())),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
async function listenOnAvailablePort(server) {
|
|
815
|
+
const firstPort = 39000 + Math.floor(Math.random() * 1000);
|
|
816
|
+
for (let offset = 0; offset < 20; offset += 1) {
|
|
817
|
+
const port = firstPort + offset;
|
|
818
|
+
try {
|
|
819
|
+
await new Promise((resolveListen, reject) => {
|
|
820
|
+
server.once('error', reject);
|
|
821
|
+
server.listen(port, '127.0.0.1', () => {
|
|
822
|
+
server.off('error', reject);
|
|
823
|
+
resolveListen();
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
return port;
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
server.removeAllListeners('error');
|
|
830
|
+
if (error?.code !== 'EADDRINUSE')
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
throw new Error('Unable to find an available localhost port for app preview.');
|
|
835
|
+
}
|
|
836
|
+
async function loadPlaywright() {
|
|
837
|
+
const fromTest = await import('@playwright/test').catch(() => null);
|
|
838
|
+
if (fromTest?.chromium)
|
|
839
|
+
return fromTest;
|
|
840
|
+
const fromPlaywright = await import('playwright').catch(() => null);
|
|
841
|
+
if (fromPlaywright?.chromium)
|
|
842
|
+
return fromPlaywright;
|
|
843
|
+
throw new Error('Local preview requires Playwright. Install project dependencies with `bun install`, then rerun `orizu apps preview`.');
|
|
844
|
+
}
|
|
845
|
+
function resolveChromiumExecutablePath() {
|
|
846
|
+
const candidates = [
|
|
847
|
+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
|
848
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
849
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
850
|
+
'/usr/bin/google-chrome',
|
|
851
|
+
'/usr/bin/chromium',
|
|
852
|
+
'/usr/bin/chromium-browser',
|
|
853
|
+
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
854
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
855
|
+
].filter(Boolean);
|
|
856
|
+
return candidates.find(candidate => existsSync(candidate)) || null;
|
|
857
|
+
}
|