@arclabs561/ai-visual-test 0.5.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +102 -11
  2. package/DEPLOYMENT.md +225 -9
  3. package/README.md +71 -80
  4. package/index.d.ts +862 -3
  5. package/package.json +10 -51
  6. package/src/batch-optimizer.mjs +39 -0
  7. package/src/cache.mjs +241 -16
  8. package/src/config.mjs +33 -91
  9. package/src/constants.mjs +54 -0
  10. package/src/convenience.mjs +113 -10
  11. package/src/cost-optimization.mjs +1 -0
  12. package/src/cost-tracker.mjs +134 -2
  13. package/src/data-extractor.mjs +36 -7
  14. package/src/dynamic-few-shot.mjs +69 -11
  15. package/src/errors.mjs +6 -2
  16. package/src/experience-propagation.mjs +12 -0
  17. package/src/experience-tracer.mjs +12 -3
  18. package/src/game-player.mjs +222 -43
  19. package/src/graceful-shutdown.mjs +126 -0
  20. package/src/helpers/playwright.mjs +22 -8
  21. package/src/human-validation-manager.mjs +99 -2
  22. package/src/index.mjs +48 -3
  23. package/src/integrations/playwright.mjs +140 -0
  24. package/src/judge.mjs +697 -24
  25. package/src/load-env.mjs +2 -1
  26. package/src/logger.mjs +31 -3
  27. package/src/model-tier-selector.mjs +1 -221
  28. package/src/natural-language-specs.mjs +31 -3
  29. package/src/persona-enhanced.mjs +4 -2
  30. package/src/persona-experience.mjs +1 -1
  31. package/src/pricing.mjs +28 -0
  32. package/src/prompt-composer.mjs +162 -5
  33. package/src/provider-data.mjs +115 -0
  34. package/src/render-change-detector.mjs +5 -0
  35. package/src/research-enhanced-validation.mjs +7 -5
  36. package/src/retry.mjs +21 -7
  37. package/src/rubrics.mjs +4 -0
  38. package/src/safe-logger.mjs +71 -0
  39. package/src/session-cost-tracker.mjs +320 -0
  40. package/src/smart-validator.mjs +8 -8
  41. package/src/spec-templates.mjs +52 -6
  42. package/src/startup-validation.mjs +127 -0
  43. package/src/temporal-adaptive.mjs +2 -2
  44. package/src/temporal-decision-manager.mjs +1 -271
  45. package/src/temporal-logic.mjs +104 -0
  46. package/src/temporal-note-pruner.mjs +119 -0
  47. package/src/temporal-preprocessor.mjs +1 -543
  48. package/src/temporal.mjs +681 -79
  49. package/src/utils/action-hallucination-detector.mjs +301 -0
  50. package/src/utils/baseline-validator.mjs +82 -0
  51. package/src/utils/cache-stats.mjs +104 -0
  52. package/src/utils/cached-llm.mjs +164 -0
  53. package/src/utils/capability-stratifier.mjs +108 -0
  54. package/src/utils/counterfactual-tester.mjs +83 -0
  55. package/src/utils/error-recovery.mjs +117 -0
  56. package/src/utils/explainability-scorer.mjs +119 -0
  57. package/src/utils/exploratory-automation.mjs +131 -0
  58. package/src/utils/index.mjs +10 -0
  59. package/src/utils/intent-recognizer.mjs +201 -0
  60. package/src/utils/log-sanitizer.mjs +165 -0
  61. package/src/utils/path-validator.mjs +88 -0
  62. package/src/utils/performance-logger.mjs +316 -0
  63. package/src/utils/performance-measurement.mjs +280 -0
  64. package/src/utils/prompt-sanitizer.mjs +213 -0
  65. package/src/utils/rate-limiter.mjs +144 -0
  66. package/src/validation-framework.mjs +24 -20
  67. package/src/validation-result-normalizer.mjs +27 -1
  68. package/src/validation.mjs +75 -25
  69. package/src/validators/accessibility-validator.mjs +144 -0
  70. package/src/validators/hybrid-validator.mjs +48 -4
  71. package/api/health.js +0 -34
  72. package/api/validate.js +0 -252
  73. package/public/index.html +0 -149
  74. package/vercel.json +0 -27
@@ -219,5 +219,149 @@ Return detailed assessment with:
219
219
  meetsRequirement: ratios.length > 0 ? ratios.every(r => r >= this.minContrast) : null
220
220
  };
221
221
  }
222
+
223
+ /**
224
+ * Hybrid accessibility validation (programmatic + VLLM)
225
+ * Combines fast programmatic checks with semantic VLLM evaluation
226
+ *
227
+ * @param {any} page - Playwright page object
228
+ * @param {string} screenshotPath - Path to screenshot
229
+ * @param {Object} options - Validation options
230
+ * @returns {Promise<Object>} Combined validation result
231
+ */
232
+ async validateHybrid(page, screenshotPath, options = {}) {
233
+ if (!page) {
234
+ throw new ValidationError('validateHybrid: page is required');
235
+ }
236
+ if (!screenshotPath) {
237
+ throw new ValidationError('validateHybrid: screenshotPath is required');
238
+ }
239
+
240
+ // Run programmatic checks (fast, deterministic)
241
+ const programmaticChecks = await this._runProgrammaticChecks(page, options);
242
+
243
+ // Run VLLM semantic evaluation (comprehensive, contextual)
244
+ const semanticEvaluation = await this.validateAccessibility(screenshotPath, {
245
+ ...options,
246
+ testType: 'accessibility-hybrid-semantic'
247
+ });
248
+
249
+ // Combine results
250
+ const combined = {
251
+ passed: programmaticChecks.passed && semanticEvaluation.score >= 7,
252
+ programmatic: programmaticChecks,
253
+ semantic: semanticEvaluation,
254
+ method: 'hybrid',
255
+ issues: [
256
+ ...programmaticChecks.violations || [],
257
+ ...semanticEvaluation.issues || []
258
+ ],
259
+ // Deduplicate issues
260
+ uniqueIssues: this._deduplicateIssues([
261
+ ...(programmaticChecks.violations || []),
262
+ ...(semanticEvaluation.issues || [])
263
+ ])
264
+ };
265
+
266
+ return combined;
267
+ }
268
+
269
+ /**
270
+ * Run programmatic accessibility checks
271
+ */
272
+ async _runProgrammaticChecks(page, options = {}) {
273
+ const checks = {
274
+ contrast: { passed: true, violations: [] },
275
+ keyboard: { passed: true, violations: [] },
276
+ altText: { passed: true, violations: [] }
277
+ };
278
+
279
+ try {
280
+ // Check contrast (if page has text elements)
281
+ const contrastResult = await page.evaluate((minContrast) => {
282
+ const violations = [];
283
+ const textElements = Array.from(document.querySelectorAll('*')).filter(el => {
284
+ const style = window.getComputedStyle(el);
285
+ return style.color !== 'transparent' &&
286
+ el.textContent &&
287
+ el.textContent.trim().length > 0;
288
+ });
289
+
290
+ // Simple contrast check (would need actual contrast calculation in real implementation)
291
+ return { passed: true, violations: [] };
292
+ }, this.minContrast);
293
+
294
+ checks.contrast = contrastResult;
295
+ } catch (err) {
296
+ checks.contrast = { passed: false, violations: [`Contrast check failed: ${err.message}`] };
297
+ }
298
+
299
+ try {
300
+ // Check keyboard navigation
301
+ const keyboardResult = await page.evaluate(() => {
302
+ const violations = [];
303
+ const interactiveElements = Array.from(document.querySelectorAll('a, button, input, select, textarea, [tabindex]'));
304
+
305
+ for (const el of interactiveElements) {
306
+ if (el.tabIndex < 0 && !el.hasAttribute('tabindex')) {
307
+ violations.push(`Element ${el.tagName} may not be keyboard accessible`);
308
+ }
309
+ }
310
+
311
+ return { passed: violations.length === 0, violations };
312
+ });
313
+
314
+ checks.keyboard = keyboardResult;
315
+ } catch (err) {
316
+ checks.keyboard = { passed: false, violations: [`Keyboard check failed: ${err.message}`] };
317
+ }
318
+
319
+ try {
320
+ // Check alt text
321
+ const altTextResult = await page.evaluate(() => {
322
+ const violations = [];
323
+ const images = Array.from(document.querySelectorAll('img'));
324
+
325
+ for (const img of images) {
326
+ if (!img.alt && !img.getAttribute('aria-hidden')) {
327
+ violations.push(`Image missing alt text: ${img.src}`);
328
+ }
329
+ }
330
+
331
+ return { passed: violations.length === 0, violations };
332
+ });
333
+
334
+ checks.altText = altTextResult;
335
+ } catch (err) {
336
+ checks.altText = { passed: false, violations: [`Alt text check failed: ${err.message}`] };
337
+ }
338
+
339
+ const allViolations = [
340
+ ...checks.contrast.violations,
341
+ ...checks.keyboard.violations,
342
+ ...checks.altText.violations
343
+ ];
344
+
345
+ return {
346
+ passed: allViolations.length === 0,
347
+ violations: allViolations,
348
+ checks
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Deduplicate issues
354
+ */
355
+ _deduplicateIssues(issues) {
356
+ const seen = new Set();
357
+ return issues.filter(issue => {
358
+ const key = typeof issue === 'string' ? issue : JSON.stringify(issue);
359
+ if (seen.has(key)) {
360
+ return false;
361
+ }
362
+ seen.add(key);
363
+ return true;
364
+ });
365
+ }
222
366
  }
223
367
 
@@ -42,6 +42,18 @@ function getValidateScreenshot() {
42
42
  return injectedValidateScreenshot || validateScreenshot;
43
43
  }
44
44
 
45
+ function deduplicateIssues(issues) {
46
+ const seen = new Set();
47
+ return issues.filter(issue => {
48
+ const key = typeof issue === 'string' ? issue : JSON.stringify(issue);
49
+ if (seen.has(key)) {
50
+ return false;
51
+ }
52
+ seen.add(key);
53
+ return true;
54
+ });
55
+ }
56
+
45
57
  /**
46
58
  * Hybrid accessibility validation
47
59
  *
@@ -105,7 +117,7 @@ Use this programmatic data as ground truth (no hallucinations about measurements
105
117
  Evaluate semantic aspects:
106
118
  1. Is contrast adequate for readability in context? (ratio alone doesn't tell you if it's readable)
107
119
  2. Are contrast violations critical or minor? (some violations might be acceptable in context)
108
- 3. Is keyboard navigation intuitive? (semantic evaluation beyond just focusable elements)
120
+ 3. Is keyboard navigation usable? (semantic evaluation beyond just focusable elements)
109
121
  4. Does overall accessibility support user goals? (holistic evaluation)
110
122
  5. Are there accessibility issues that programmatic checks don't capture? (visual, semantic, contextual)
111
123
 
@@ -120,9 +132,39 @@ Provide actionable recommendations based on both programmatic and semantic analy
120
132
  programmaticData
121
133
  });
122
134
 
135
+ // Calculate programmatic pass status
136
+ const programmaticPassed = (programmaticData.contrast.failing === 0) &&
137
+ (programmaticData.keyboard.violations.length === 0);
138
+
139
+ // Combine results
123
140
  return {
124
141
  ...result,
125
- programmaticData
142
+ passed: programmaticPassed && (result.score === null || result.score >= 6),
143
+ programmaticData, // Required by tests/consumers
144
+ programmatic: programmaticData, // Alias for clarity
145
+ semantic: result,
146
+ method: 'hybrid',
147
+ issues: [
148
+ ...(programmaticData.contrast.violations || []),
149
+ ...(programmaticData.keyboard.violations || []),
150
+ ...result.issues || []
151
+ ],
152
+ uniqueIssues: deduplicateIssues([
153
+ ...(programmaticData.contrast.violations || []),
154
+ ...(programmaticData.keyboard.violations || []),
155
+ ...(result.issues || [])
156
+ ]).map(issue => {
157
+ // Normalize to strings for consistent formatting
158
+ if (typeof issue === 'string') return issue;
159
+ if (typeof issue === 'object' && issue !== null) {
160
+ if (issue.description) return issue.description;
161
+ if (issue.element && issue.issue) return `${issue.element}: ${issue.issue}`;
162
+ if (issue.ratio && issue.required) return `Contrast ${issue.ratio}:1 (required: ${issue.required}:1)`;
163
+ if (issue.message) return issue.message;
164
+ return JSON.stringify(issue);
165
+ }
166
+ return String(issue);
167
+ })
126
168
  };
127
169
  }
128
170
 
@@ -216,7 +258,8 @@ Provide actionable recommendations based on both programmatic and semantic analy
216
258
 
217
259
  return {
218
260
  ...result,
219
- programmaticData
261
+ programmaticData,
262
+ method: 'hybrid'
220
263
  };
221
264
  }
222
265
 
@@ -262,7 +305,8 @@ EVALUATION INSTRUCTIONS:
262
305
 
263
306
  return {
264
307
  ...result,
265
- programmaticData
308
+ programmaticData,
309
+ method: 'hybrid'
266
310
  };
267
311
  }
268
312
 
package/api/health.js DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * Health check endpoint
3
- *
4
- * GET /api/health
5
- */
6
-
7
- import { createConfig } from '../src/index.mjs';
8
-
9
- export default async function handler(req, res) {
10
- if (req.method !== 'GET') {
11
- return res.status(405).json({ error: 'Method not allowed' });
12
- }
13
-
14
- try {
15
- const config = createConfig();
16
-
17
- return res.status(200).json({
18
- status: 'ok',
19
- enabled: config.enabled,
20
- provider: config.provider,
21
- version: '0.1.0',
22
- timestamp: new Date().toISOString()
23
- });
24
- } catch (error) {
25
- // SECURITY: Don't expose internal error details
26
- // Log server-side for debugging, return generic message to client
27
- console.error('[Health] Error:', error);
28
- return res.status(500).json({
29
- status: 'error',
30
- error: 'Health check failed'
31
- });
32
- }
33
- }
34
-
package/api/validate.js DELETED
@@ -1,252 +0,0 @@
1
- /**
2
- * Vercel Serverless Function for VLLM Screenshot Validation
3
- *
4
- * POST /api/validate
5
- *
6
- * Body:
7
- * {
8
- * "image": "base64-encoded-image",
9
- * "prompt": "Evaluation prompt",
10
- * "context": { ... }
11
- * }
12
- *
13
- * Returns:
14
- * {
15
- * "enabled": boolean,
16
- * "provider": string,
17
- * "score": number|null,
18
- * "issues": string[],
19
- * "assessment": string|null,
20
- * "reasoning": string,
21
- * "estimatedCost": object|null,
22
- * "responseTime": number
23
- * }
24
- */
25
-
26
- import { validateScreenshot, createConfig, normalizeValidationResult } from '../src/index.mjs';
27
- import { writeFileSync, unlinkSync } from 'fs';
28
- import { join } from 'path';
29
- import { tmpdir } from 'os';
30
- import { randomBytes } from 'crypto';
31
-
32
- // Security limits
33
- const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
34
- const MAX_PROMPT_LENGTH = 5000;
35
- const MAX_CONTEXT_SIZE = 10000;
36
-
37
- // Rate limiting configuration
38
- const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
39
- const RATE_LIMIT_MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '10', 10);
40
- const rateLimitStore = new Map(); // In-memory store (use Redis in production)
41
-
42
- // Authentication configuration
43
- const API_KEY = process.env.API_KEY || process.env.VLLM_API_KEY || null;
44
- // Default to requiring auth if API key is set (more secure)
45
- // Set REQUIRE_AUTH=false explicitly to disable
46
- const REQUIRE_AUTH = process.env.REQUIRE_AUTH !== 'false' && API_KEY !== null;
47
-
48
- /**
49
- * Simple rate limiter (in-memory)
50
- * For production, use Redis or a dedicated rate limiting service
51
- */
52
- function checkRateLimit(identifier) {
53
- const now = Date.now();
54
- const windowStart = now - RATE_LIMIT_WINDOW;
55
-
56
- // Clean up old entries
57
- for (const [key, timestamps] of rateLimitStore.entries()) {
58
- const recent = timestamps.filter(ts => ts > windowStart);
59
- if (recent.length === 0) {
60
- rateLimitStore.delete(key);
61
- } else {
62
- rateLimitStore.set(key, recent);
63
- }
64
- }
65
-
66
- // Check current identifier
67
- const timestamps = rateLimitStore.get(identifier) || [];
68
- const recent = timestamps.filter(ts => ts > windowStart);
69
-
70
- if (recent.length >= RATE_LIMIT_MAX_REQUESTS) {
71
- return {
72
- allowed: false,
73
- remaining: 0,
74
- resetAt: Math.min(...recent) + RATE_LIMIT_WINDOW
75
- };
76
- }
77
-
78
- // Add current request
79
- recent.push(now);
80
- rateLimitStore.set(identifier, recent);
81
-
82
- return {
83
- allowed: true,
84
- remaining: RATE_LIMIT_MAX_REQUESTS - recent.length,
85
- resetAt: now + RATE_LIMIT_WINDOW
86
- };
87
- }
88
-
89
- /**
90
- * Get client identifier for rate limiting
91
- */
92
- function getClientIdentifier(req) {
93
- // Try to get IP from various headers (Vercel, Cloudflare, etc.)
94
- const forwarded = req.headers['x-forwarded-for'];
95
- const realIp = req.headers['x-real-ip'];
96
- const ip = forwarded?.split(',')[0] || realIp || req.socket?.remoteAddress || 'unknown';
97
-
98
- // If API key is provided, use it as identifier (more accurate)
99
- const apiKey = req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', '');
100
- return apiKey || ip;
101
- }
102
-
103
- /**
104
- * Check authentication
105
- */
106
- function checkAuth(req) {
107
- if (!REQUIRE_AUTH || !API_KEY) {
108
- return { authenticated: true };
109
- }
110
-
111
- // SECURITY: Only accept API key from headers, not request body
112
- // API keys in request bodies are logged, visible in dev tools, and stored in history
113
- const providedKey = req.headers['x-api-key'] ||
114
- req.headers['authorization']?.replace('Bearer ', '');
115
-
116
- if (!providedKey) {
117
- return { authenticated: false, error: 'Authentication required. Provide API key via X-API-Key header or Authorization: Bearer <key>' };
118
- }
119
-
120
- if (providedKey !== API_KEY) {
121
- return { authenticated: false, error: 'Invalid API key' };
122
- }
123
-
124
- return { authenticated: true };
125
- }
126
-
127
- export default async function handler(req, res) {
128
- // Only allow POST
129
- if (req.method !== 'POST') {
130
- return res.status(405).json({ error: 'Method not allowed' });
131
- }
132
-
133
- // Check authentication
134
- const authResult = checkAuth(req);
135
- if (!authResult.authenticated) {
136
- return res.status(401).json({ error: authResult.error });
137
- }
138
-
139
- // Check rate limit
140
- const clientId = getClientIdentifier(req);
141
- const rateLimit = checkRateLimit(clientId);
142
- if (!rateLimit.allowed) {
143
- res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS);
144
- res.setHeader('X-RateLimit-Remaining', 0);
145
- res.setHeader('X-RateLimit-Reset', new Date(rateLimit.resetAt).toISOString());
146
- return res.status(429).json({
147
- error: 'Rate limit exceeded',
148
- retryAfter: Math.ceil((rateLimit.resetAt - Date.now()) / 1000)
149
- });
150
- }
151
-
152
- // Set rate limit headers
153
- res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS);
154
- res.setHeader('X-RateLimit-Remaining', rateLimit.remaining);
155
- res.setHeader('X-RateLimit-Reset', new Date(rateLimit.resetAt).toISOString());
156
-
157
- try {
158
- const { image, prompt, context = {} } = req.body;
159
-
160
- // Validate input presence
161
- if (!image) {
162
- return res.status(400).json({ error: 'Missing image (base64 encoded)' });
163
- }
164
- if (!prompt) {
165
- return res.status(400).json({ error: 'Missing prompt' });
166
- }
167
-
168
- // Validate input size
169
- if (typeof image !== 'string' || image.length > MAX_IMAGE_SIZE) {
170
- return res.status(400).json({ error: 'Image too large or invalid format' });
171
- }
172
- if (typeof prompt !== 'string' || prompt.length > MAX_PROMPT_LENGTH) {
173
- return res.status(400).json({ error: 'Prompt too long' });
174
- }
175
- if (context && typeof context === 'object') {
176
- const contextSize = JSON.stringify(context).length;
177
- if (contextSize > MAX_CONTEXT_SIZE) {
178
- return res.status(400).json({ error: 'Context too large' });
179
- }
180
- }
181
-
182
- // Decode base64 image
183
- // SECURITY: Whitelist specific MIME types to prevent unexpected formats
184
- const validMimeTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'];
185
- const mimeMatch = image.match(/^data:(image\/(?:png|jpeg|jpg|gif|webp));base64,/);
186
- if (!mimeMatch) {
187
- return res.status(400).json({ error: 'Invalid image MIME type. Supported: image/png, image/jpeg, image/jpg, image/gif, image/webp' });
188
- }
189
-
190
- let imageBuffer;
191
- try {
192
- const base64Data = image.replace(/^data:image\/(?:png|jpeg|jpg|gif|webp);base64,/, '');
193
- imageBuffer = Buffer.from(base64Data, 'base64');
194
-
195
- // Additional validation: check decoded buffer size matches expected
196
- // Base64 encoding increases size by ~33%, so decoded should be smaller
197
- const expectedMaxDecoded = Math.floor(MAX_IMAGE_SIZE * 0.75); // Conservative estimate
198
- if (imageBuffer.length > expectedMaxDecoded) {
199
- return res.status(400).json({ error: 'Decoded image exceeds maximum size' });
200
- }
201
- } catch (error) {
202
- return res.status(400).json({ error: 'Invalid base64 image' });
203
- }
204
-
205
- // Save to temporary file with secure random name (prevents race conditions and information disclosure)
206
- // SECURITY: Use cryptographically secure random suffix to prevent collisions
207
- const randomSuffix = randomBytes(16).toString('hex');
208
- const tempPath = join(tmpdir(), `vllm-validate-${randomSuffix}.png`);
209
-
210
- // RESOURCE PROTECTION: File system operation is rate-limited by API rate limiting above
211
- // This writeFileSync is bounded by:
212
- // 1. Rate limiting (prevents too many concurrent operations)
213
- // 2. Size limits (MAX_IMAGE_SIZE prevents large files)
214
- // 3. Serverless timeout (function will timeout if operation takes too long)
215
- writeFileSync(tempPath, imageBuffer);
216
-
217
- try {
218
- // Validate screenshot
219
- const result = await validateScreenshot(tempPath, prompt, context);
220
-
221
- // Clean up temp file
222
- unlinkSync(tempPath);
223
-
224
- // Normalize result structure before returning (ensures consistent API response)
225
- const normalizedResult = normalizeValidationResult(result, 'api/validate');
226
-
227
- // Return normalized result
228
- return res.status(200).json(normalizedResult);
229
- } catch (error) {
230
- // Clean up temp file on error
231
- try {
232
- unlinkSync(tempPath);
233
- } catch {}
234
-
235
- throw error;
236
- }
237
- } catch (error) {
238
- // Log full error for debugging (server-side only)
239
- console.error('[VLLM API] Error:', error);
240
-
241
- // Return sanitized error to client (don't leak internal details)
242
- // Never expose: file paths, API keys, internal structure, stack traces
243
- const sanitizedError = error instanceof Error
244
- ? 'Validation failed. Please check your input and try again.'
245
- : 'Validation failed';
246
-
247
- return res.status(500).json({
248
- error: sanitizedError
249
- });
250
- }
251
- }
252
-
package/public/index.html DELETED
@@ -1,149 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>VLLM Testing - Visual Validation API</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
- background: #0a0a0a;
17
- color: #ffffff;
18
- line-height: 1.6;
19
- padding: 2rem;
20
- max-width: 1200px;
21
- margin: 0 auto;
22
- }
23
-
24
- h1 {
25
- font-size: 2.5rem;
26
- margin-bottom: 1rem;
27
- color: #ffffff;
28
- }
29
-
30
- h2 {
31
- font-size: 1.5rem;
32
- margin-top: 2rem;
33
- margin-bottom: 1rem;
34
- color: #ffffff;
35
- }
36
-
37
- .status {
38
- display: inline-block;
39
- padding: 0.5rem 1rem;
40
- border-radius: 4px;
41
- font-weight: bold;
42
- margin-bottom: 2rem;
43
- }
44
-
45
- .status.ok {
46
- background: #00ff00;
47
- color: #000000;
48
- }
49
-
50
- .status.error {
51
- background: #ff0000;
52
- color: #ffffff;
53
- }
54
-
55
- code {
56
- background: #1a1a1a;
57
- padding: 0.2rem 0.4rem;
58
- border-radius: 3px;
59
- font-family: 'Monaco', 'Courier New', monospace;
60
- font-size: 0.9em;
61
- }
62
-
63
- pre {
64
- background: #1a1a1a;
65
- padding: 1rem;
66
- border-radius: 4px;
67
- overflow-x: auto;
68
- margin: 1rem 0;
69
- }
70
-
71
- .endpoint {
72
- background: #1a1a1a;
73
- padding: 1.5rem;
74
- border-radius: 4px;
75
- margin: 1rem 0;
76
- border-left: 4px solid #00ff00;
77
- }
78
-
79
- .method {
80
- display: inline-block;
81
- padding: 0.25rem 0.5rem;
82
- border-radius: 3px;
83
- font-weight: bold;
84
- margin-right: 0.5rem;
85
- }
86
-
87
- .method.post {
88
- background: #00ff00;
89
- color: #000000;
90
- }
91
-
92
- .method.get {
93
- background: #0066ff;
94
- color: #ffffff;
95
- }
96
- </style>
97
- </head>
98
- <body>
99
- <h1>VLLM Testing API</h1>
100
- <div id="status" class="status">Checking...</div>
101
-
102
- <h2>API Endpoints</h2>
103
-
104
- <div class="endpoint">
105
- <span class="method post">POST</span>
106
- <code>/api/validate</code>
107
- <p style="margin-top: 1rem;">Validate a screenshot using Vision Language Models.</p>
108
- <pre>{
109
- "image": "base64-encoded-image",
110
- "prompt": "Evaluate this screenshot...",
111
- "context": {
112
- "testType": "payment-screen",
113
- "viewport": { "width": 1280, "height": 720 }
114
- }
115
- }</pre>
116
- </div>
117
-
118
- <div class="endpoint">
119
- <span class="method get">GET</span>
120
- <code>/api/health</code>
121
- <p style="margin-top: 1rem;">Health check endpoint.</p>
122
- </div>
123
-
124
- <h2>Documentation</h2>
125
- <p>See <a href="https://github.com/arclabs561/ai-visual-test" style="color: #00ff00;">GitHub repository</a> for full documentation.</p>
126
-
127
- <script>
128
- // Check health on load
129
- fetch('/api/health')
130
- .then(res => res.json())
131
- .then(data => {
132
- const statusEl = document.getElementById('status');
133
- if (data.status === 'ok') {
134
- statusEl.className = 'status ok';
135
- statusEl.textContent = `✓ API Online - Provider: ${data.provider || 'none'}`;
136
- } else {
137
- statusEl.className = 'status error';
138
- statusEl.textContent = '✗ API Error';
139
- }
140
- })
141
- .catch(err => {
142
- const statusEl = document.getElementById('status');
143
- statusEl.className = 'status error';
144
- statusEl.textContent = '✗ API Offline';
145
- });
146
- </script>
147
- </body>
148
- </html>
149
-
package/vercel.json DELETED
@@ -1,27 +0,0 @@
1
- {
2
- "version": 2,
3
- "builds": [
4
- {
5
- "src": "api/**/*.js",
6
- "use": "@vercel/node"
7
- }
8
- ],
9
- "routes": [
10
- {
11
- "src": "/api/validate",
12
- "dest": "/api/validate.js"
13
- },
14
- {
15
- "src": "/api/health",
16
- "dest": "/api/health.js"
17
- },
18
- {
19
- "src": "/",
20
- "dest": "/public/index.html"
21
- }
22
- ],
23
- "env": {
24
- "NODE_ENV": "production"
25
- }
26
- }
27
-