uilint-vision 0.2.140 → 0.2.142
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/dist/cli-manifest.d.ts +14 -0
- package/dist/cli-manifest.js +11 -0
- package/dist/cli-manifest.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/package.json +9 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI manifest for the uilint-vision plugin.
|
|
3
|
+
*
|
|
4
|
+
* Describes the CLI flag, help text, and registration entry point so the
|
|
5
|
+
* core CLI can discover and wire up this plugin without hardcoded knowledge.
|
|
6
|
+
*/
|
|
7
|
+
declare const cliManifest: {
|
|
8
|
+
readonly packageName: "uilint-vision";
|
|
9
|
+
readonly cliFlag: "vision";
|
|
10
|
+
readonly cliDescription: "Install vision analysis rules (non-interactive)";
|
|
11
|
+
readonly registerSpecifier: "uilint-vision/eslint-rules/register";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { cliManifest };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/cli-manifest.ts
|
|
2
|
+
var cliManifest = {
|
|
3
|
+
packageName: "uilint-vision",
|
|
4
|
+
cliFlag: "vision",
|
|
5
|
+
cliDescription: "Install vision analysis rules (non-interactive)",
|
|
6
|
+
registerSpecifier: "uilint-vision/eslint-rules/register"
|
|
7
|
+
};
|
|
8
|
+
export {
|
|
9
|
+
cliManifest
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=cli-manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli-manifest.ts"],"sourcesContent":["/**\n * CLI manifest for the uilint-vision plugin.\n *\n * Describes the CLI flag, help text, and registration entry point so the\n * core CLI can discover and wire up this plugin without hardcoded knowledge.\n */\nexport const cliManifest = {\n packageName: \"uilint-vision\",\n cliFlag: \"vision\",\n cliDescription: \"Install vision analysis rules (non-interactive)\",\n registerSpecifier: \"uilint-vision/eslint-rules/register\",\n} as const;\n"],"mappings":";AAMO,IAAM,cAAc;AAAA,EACzB,aAAa;AAAA,EACb,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,mBAAmB;AACrB;","names":[]}
|
package/dist/node.js
CHANGED
|
@@ -268,7 +268,7 @@ ${elements.join("\n")}
|
|
|
268
268
|
onProgress("(waiting for model output\u2026)", fullResponse);
|
|
269
269
|
emittedWaitingLine = true;
|
|
270
270
|
}
|
|
271
|
-
const thinking = chunk
|
|
271
|
+
const thinking = chunk.message?.thinking || "";
|
|
272
272
|
if (thinking) {
|
|
273
273
|
onProgress(lastLatestLine, fullResponse, void 0, thinking);
|
|
274
274
|
lastProgressAt = Date.now();
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/analyzer/vision-analyzer.ts","../src/vision-run.ts"],"sourcesContent":["/**\n * Vision analyzer for Ollama vision LLM analysis\n *\n * This module provides functionality to analyze screenshots using Ollama's\n * vision models (e.g., qwen3-vl, llava) to detect UI consistency issues\n * that are only visible in rendered output.\n *\n * Uses the official ollama npm package with the chat API for proper vision support.\n */\n\nimport { Ollama } from \"ollama\";\nimport type {\n OllamaClientOptions,\n StreamProgressCallback,\n LLMInstrumentationCallbacks,\n InstrumentationSpan,\n} from \"uilint-core\";\n\nimport type {\n ElementManifest,\n VisionIssue,\n} from \"../types.js\";\n\n/**\n * Internal result type for the analyzer\n * (Different from VisionAnalysisResult which includes route/timestamp/manifest)\n */\nexport interface AnalyzerResult {\n /** Detected issues */\n issues: VisionIssue[];\n /** Analysis time in milliseconds */\n analysisTime: number;\n /** Full prompt sent to the model (for debugging/reports) */\n prompt?: string;\n /** Raw LLM response (for debugging) */\n rawResponse?: string;\n}\n\n/**\n * Default vision model - qwen3-vl is recommended for UI analysis\n */\nexport const UILINT_DEFAULT_VISION_MODEL = \"qwen3-vl:8b-instruct\";\n\nfunction stripCodeFences(input: string): string {\n const trimmed = (input ?? \"\").trim();\n if (!trimmed.startsWith(\"```\")) return trimmed;\n const m = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n return m ? m[1]!.trim() : trimmed;\n}\n\nfunction extractJsonObject(input: string): string {\n const s = stripCodeFences(input).trim();\n if (!s) return s;\n if (s.startsWith(\"{\") && s.endsWith(\"}\")) return s;\n const start = s.indexOf(\"{\");\n const end = s.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return s.slice(start, end + 1).trim();\n }\n return s;\n}\n\n/**\n * Options for vision analysis\n */\nexport interface VisionAnalysisOptions {\n /** Style guide content (markdown) */\n styleGuide?: string | null;\n /** Progress callback for streaming */\n onProgress?: StreamProgressCallback;\n /** Custom prompt additions */\n additionalContext?: string;\n}\n\n/**\n * Vision analyzer client options\n */\nexport interface VisionAnalyzerOptions extends OllamaClientOptions {\n /** Vision model to use (default: qwen3-vl:30b) */\n visionModel?: string;\n}\n\nconst DEFAULT_BASE_URL = \"http://localhost:11434\";\nconst DEFAULT_TIMEOUT = 120000; // Vision models can be slower\n\n/**\n * Vision analyzer for UI screenshot analysis\n * Uses the ollama npm package with chat API for proper vision model support\n */\nexport class VisionAnalyzer {\n private baseUrl: string;\n private visionModel: string;\n private timeout: number;\n private instrumentation?: LLMInstrumentationCallbacks;\n private client: Ollama;\n\n constructor(options: VisionAnalyzerOptions = {}) {\n this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n this.visionModel = options.visionModel || UILINT_DEFAULT_VISION_MODEL;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n this.instrumentation = options.instrumentation;\n this.client = new Ollama({ host: this.baseUrl });\n }\n\n /**\n * Analyzes a screenshot with element manifest for UI consistency issues\n */\n async analyzeScreenshot(\n imageBase64: string,\n manifest: ElementManifest[],\n options: VisionAnalysisOptions = {}\n ): Promise<AnalyzerResult> {\n const startTime = Date.now();\n\n const prompt = this.buildVisionPrompt(manifest, options);\n\n // Start instrumentation span if available\n const spanResult = this.instrumentation?.onGenerationStart?.({\n name: \"vision-analyze\",\n model: this.visionModel,\n prompt,\n metadata: {\n manifestSize: manifest.length,\n hasStyleGuide: !!options.styleGuide,\n },\n });\n // Convert void to undefined for type compatibility\n const span: InstrumentationSpan | undefined = spanResult || undefined;\n\n try {\n const rawResponse = options.onProgress\n ? await this.chatVisionStreaming(\n imageBase64,\n prompt,\n options.onProgress,\n span\n )\n : await this.chatVision(imageBase64, prompt, span);\n\n const issues = this.parseVisionResponse(rawResponse, manifest);\n\n return {\n issues,\n analysisTime: Date.now() - startTime,\n prompt,\n rawResponse,\n };\n } catch (error) {\n const analysisTime = Date.now() - startTime;\n const err =\n error instanceof Error\n ? error\n : new Error(String(error ?? \"Unknown error\"));\n console.error(\"[VisionAnalyzer] Analysis failed\", {\n baseUrl: this.baseUrl,\n model: this.visionModel,\n manifestSize: manifest.length,\n analysisTime,\n error: err.message,\n });\n span?.end(\"\", { error: err.message });\n throw err;\n }\n }\n\n /**\n * Builds the vision analysis prompt\n */\n private buildVisionPrompt(\n manifest: ElementManifest[],\n options: VisionAnalysisOptions\n ): string {\n const styleGuideSection = options.styleGuide\n ? `## Style Guide\n${options.styleGuide}\n\n`\n : \"\";\n\n const additionalSection = options.additionalContext\n ? `## Additional Context\n${options.additionalContext}\n\n`\n : \"\";\n\n // Build element manifest section for context\n const manifestSection = this.buildManifestSection(manifest);\n\n return `You are a UI consistency analyzer examining a screenshot of a web page.\nAnalyze the visual appearance and identify any UI consistency issues.\n\n${styleGuideSection}${additionalSection}${manifestSection}\n\n## Task\n\nExamine the screenshot and identify visual consistency issues such as:\n1. **Spacing inconsistencies**: Elements with uneven padding, margins, or gaps\n2. **Alignment issues**: Elements that should be aligned but aren't\n3. **Color inconsistencies**: Similar colors that should be identical, or colors that don't match the style guide\n4. **Typography issues**: Inconsistent font sizes, weights, or families\n5. **Layout problems**: Broken layouts, overlapping elements, or visual hierarchy issues\n6. **Contrast issues**: Text that's hard to read or UI elements with insufficient contrast\n\nFor each issue found, reference the element by its visible text so we can map it back to the source code.\n\n## Response Format\n\nRespond with JSON ONLY. Return a single JSON object:\n\n\\`\\`\\`json\n{\n \"issues\": [\n {\n \"elementText\": \"Submit Order\",\n \"message\": \"Button has inconsistent padding compared to other buttons (appears larger)\",\n \"category\": \"spacing\",\n \"severity\": \"warning\"\n }\n ]\n}\n\\`\\`\\`\n\nCategories: spacing, alignment, color, typography, layout, contrast, visual-hierarchy\nSeverities: error (major issue), warning (should fix), info (minor/suggestion)\n\nIMPORTANT:\n- Reference elements by their visible text content\n- Be specific about what's wrong and what the expected state should be\n- Only report significant visual inconsistencies\n- If no issues are found, return {\"issues\": []}`;\n }\n\n /**\n * Builds the manifest section of the prompt\n */\n private buildManifestSection(manifest: ElementManifest[]): string {\n if (manifest.length === 0) {\n return \"\";\n }\n\n const elements = manifest.map((el) => {\n const parts = [`- \"${el.text}\"`];\n if (el.role) parts.push(`(${el.role})`);\n if (el.instanceCount && el.instanceCount > 1) {\n parts.push(`[${el.instanceCount} instances]`);\n }\n return parts.join(\" \");\n });\n\n return `## Page Elements\n\nThe following elements are visible on the page (reference by text when reporting issues):\n\n${elements.join(\"\\n\")}\n\n`;\n }\n\n /**\n * Non-streaming chat API call for vision\n */\n private async chatVision(\n imageBase64: string,\n prompt: string,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const response = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n format: \"json\",\n options: {\n // Increase context window for large prompts\n num_ctx: 8192,\n },\n });\n\n const output = response.message?.content || \"\";\n\n if (!output.trim()) {\n const diag = JSON.stringify(\n {\n done_reason: response.done_reason,\n model: response.model,\n prompt_eval_count: response.prompt_eval_count,\n eval_count: response.eval_count,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output. Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(output, {\n promptTokens: response.prompt_eval_count,\n completionTokens: response.eval_count,\n totalTokens:\n (response.prompt_eval_count || 0) + (response.eval_count || 0) ||\n undefined,\n });\n\n return output;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Streaming chat API call for vision\n * Uses ollama package's async iterator for streaming\n */\n private async chatVisionStreaming(\n imageBase64: string,\n prompt: string,\n onProgress: StreamProgressCallback,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n let promptTokens: number | undefined;\n let completionTokens: number | undefined;\n let chunksReceived = 0;\n let lastProgressAt = Date.now();\n let emittedWaitingLine = false;\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const stream = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n // Enable thinking for streaming so we can surface reasoning traces for thinking-capable models.\n // Models that don't support it should ignore it.\n think: false,\n stream: true,\n format: \"json\",\n options: {\n num_ctx: 8192,\n },\n });\n\n let fullResponse = \"\";\n let lastLatestLine = \"\";\n\n for await (const chunk of stream) {\n chunksReceived++;\n\n // Emit waiting message on first chunk if no content yet\n if (!emittedWaitingLine && !fullResponse.trim()) {\n onProgress(\"(waiting for model output…)\", fullResponse);\n emittedWaitingLine = true;\n }\n\n // Thinking-capable models stream `message.thinking` separately from `message.content`.\n const thinking = (chunk as any)?.message?.thinking || \"\";\n if (thinking) {\n onProgress(lastLatestLine, fullResponse, undefined, thinking);\n lastProgressAt = Date.now();\n }\n\n // Chat API uses message.content for text deltas (final answer)\n const content = chunk.message?.content || \"\";\n if (content) {\n fullResponse += content;\n\n // Get the latest line for progress display\n const responseLines = fullResponse.split(\"\\n\");\n const latestLine =\n responseLines[responseLines.length - 1] ||\n responseLines[responseLines.length - 2] ||\n \"\";\n\n // Stream the actual text delta to the callback\n lastLatestLine = latestLine.trim();\n onProgress(lastLatestLine, fullResponse, content);\n lastProgressAt = Date.now();\n }\n\n // Capture token counts from final chunk\n if (chunk.done) {\n promptTokens = chunk.prompt_eval_count;\n completionTokens = chunk.eval_count;\n }\n\n // Heartbeat if we're receiving chunks but no text output\n if (!fullResponse.trim() && Date.now() - lastProgressAt > 4000) {\n onProgress(\n `(streaming… received ${chunksReceived} chunks)`,\n fullResponse\n );\n lastProgressAt = Date.now();\n }\n }\n\n if (!fullResponse.trim()) {\n const diag = JSON.stringify(\n {\n chunksReceived,\n promptTokens,\n completionTokens,\n model: this.visionModel,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output (streaming). Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(fullResponse, {\n promptTokens,\n completionTokens,\n totalTokens: (promptTokens || 0) + (completionTokens || 0) || undefined,\n });\n\n return fullResponse;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parses the vision LLM response and matches elements to manifest\n */\n private parseVisionResponse(\n response: string,\n manifest: ElementManifest[]\n ): VisionIssue[] {\n try {\n const jsonText = extractJsonObject(response);\n const parsed = JSON.parse(jsonText);\n const rawIssues = parsed.issues || [];\n\n // Map elementText to dataLoc using manifest\n return rawIssues.map((issue: VisionIssue) => {\n const matchedElement = this.matchElementByText(\n issue.elementText,\n manifest\n );\n\n return {\n ...issue,\n dataLoc: matchedElement?.dataLoc,\n };\n });\n } catch {\n const s = response ?? \"\";\n const previewHead = s ? s.slice(0, 600) : \"\";\n const previewTail = s && s.length > 600 ? s.slice(-400) : \"\";\n throw new Error(\n `Vision model returned non-JSON output (expected JSON). length=${\n s.length\n } head=${JSON.stringify(previewHead)} tail=${JSON.stringify(\n previewTail\n )}`\n );\n }\n }\n\n /**\n * Matches element text from LLM response to manifest entries\n */\n private matchElementByText(\n elementText: string,\n manifest: ElementManifest[]\n ): ElementManifest | undefined {\n if (!elementText) return undefined;\n\n const normalizedSearch = elementText.toLowerCase().trim();\n\n // Exact match first\n for (const entry of manifest) {\n if (entry.text.toLowerCase().trim() === normalizedSearch) {\n return entry;\n }\n }\n\n // Partial match\n for (const entry of manifest) {\n const normalizedEntry = entry.text.toLowerCase().trim();\n if (\n normalizedEntry.includes(normalizedSearch) ||\n normalizedSearch.includes(normalizedEntry)\n ) {\n return entry;\n }\n }\n\n return undefined;\n }\n\n /**\n * Checks if the vision model is available\n */\n async isAvailable(): Promise<boolean> {\n try {\n const models = await this.client.list();\n return models.models.some(\n (m) =>\n m.name === this.visionModel ||\n m.name.startsWith(this.visionModel.split(\":\")[0])\n );\n } catch {\n return false;\n }\n }\n\n /**\n * Gets the current vision model\n */\n getModel(): string {\n return this.visionModel;\n }\n\n /**\n * Gets the Ollama base URL (for diagnostics)\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n\n /**\n * Sets the vision model\n */\n setModel(model: string): void {\n this.visionModel = model;\n }\n\n /**\n * Sets instrumentation callbacks\n */\n setInstrumentation(\n instrumentation: LLMInstrumentationCallbacks | undefined\n ): void {\n this.instrumentation = instrumentation;\n }\n}\n\n// Default singleton instance\nlet defaultAnalyzer: VisionAnalyzer | null = null;\n\n/**\n * Gets the default VisionAnalyzer instance\n */\nexport function getVisionAnalyzer(\n options?: VisionAnalyzerOptions\n): VisionAnalyzer {\n if (!defaultAnalyzer || options) {\n defaultAnalyzer = new VisionAnalyzer(options);\n }\n return defaultAnalyzer;\n}\n","/**\n * Vision analysis utilities for running vision analysis with Ollama.\n *\n * These utilities provide high-level orchestration for vision analysis,\n * including style guide resolution, Ollama readiness checks, and debug dump generation.\n */\n\nimport { dirname, join, parse, resolve } from \"path\";\nimport { existsSync, statSync, mkdirSync, writeFileSync } from \"fs\";\nimport {\n ensureOllamaReady,\n findStyleGuidePath,\n findUILintStyleGuideUpwards,\n readStyleGuide,\n type StreamProgressCallback,\n} from \"uilint-core/node\";\nimport {\n VisionAnalyzer,\n UILINT_DEFAULT_VISION_MODEL,\n type AnalyzerResult,\n} from \"./analyzer/vision-analyzer.js\";\nimport type { ElementManifest, VisionIssue } from \"./types.js\";\n\n/**\n * Path resolver function type.\n * Resolves a path specifier (which may include workspace-relative \"@\" prefixes or other formats)\n * into an absolute filesystem path.\n */\nexport type PathResolver = (spec: string, cwd: string) => string;\n\n/**\n * Default path resolver that just uses path.resolve.\n * CLI callers can provide a custom resolver that handles workspace-relative paths.\n */\nconst defaultPathResolver: PathResolver = (spec: string, cwd: string) => {\n const raw = (spec ?? \"\").trim();\n if (!raw) return resolve(cwd, spec);\n return resolve(cwd, raw);\n};\n\nexport type ResolveVisionStyleGuideArgs = {\n /** Project root / cwd used to resolve relative path specifiers */\n projectPath: string;\n /** Path to style guide file OR project directory */\n styleguide?: string;\n /** A starting point for upward search (directory). Defaults to projectPath. */\n startDir?: string;\n /** Optional custom path resolver (for handling workspace-relative paths like \"@apps/...\") */\n pathResolver?: PathResolver;\n};\n\nexport type ResolveVisionStyleGuideResult = {\n styleGuide: string | null;\n styleguideLocation: string | null;\n};\n\nexport async function resolveVisionStyleGuide(\n args: ResolveVisionStyleGuideArgs\n): Promise<ResolveVisionStyleGuideResult> {\n const projectPath = args.projectPath;\n const startDir = args.startDir ?? projectPath;\n const resolvePath = args.pathResolver ?? defaultPathResolver;\n\n if (args.styleguide) {\n const styleguideArg = resolvePath(args.styleguide, projectPath);\n if (existsSync(styleguideArg)) {\n const stat = statSync(styleguideArg);\n if (stat.isFile()) {\n return {\n styleguideLocation: styleguideArg,\n styleGuide: await readStyleGuide(styleguideArg),\n };\n }\n if (stat.isDirectory()) {\n const found = findStyleGuidePath(styleguideArg);\n return {\n styleguideLocation: found,\n styleGuide: found ? await readStyleGuide(found) : null,\n };\n }\n }\n return { styleGuide: null, styleguideLocation: null };\n }\n\n const upwards = findUILintStyleGuideUpwards(startDir);\n const fallback = upwards ?? findStyleGuidePath(projectPath);\n return {\n styleguideLocation: fallback,\n styleGuide: fallback ? await readStyleGuide(fallback) : null,\n };\n}\n\nexport type RunVisionAnalysisArgs = {\n /** base64 image data (NO data: prefix) */\n imageBase64: string;\n manifest: ElementManifest[];\n /** Used to resolve styleguide arg and as default search root */\n projectPath: string;\n /** Path to style guide file OR directory; if omitted uses upward search */\n styleguide?: string;\n /** Directory for upward styleguide search; defaults to projectPath */\n styleguideStartDir?: string;\n /**\n * Override resolved styleguide content (lets callers do logging/notes based on resolution once).\n * If provided (even null), styleguide resolution is skipped.\n */\n styleGuide?: string | null;\n /**\n * Override resolved styleguide location (pairs with styleGuide override).\n * If provided, this is returned in the result.\n */\n styleguideLocation?: string | null;\n /** Ollama base URL (default: http://localhost:11434) */\n baseUrl?: string;\n /** Vision model override (default: UILINT_DEFAULT_VISION_MODEL) */\n model?: string;\n /**\n * Optional analyzer instance (lets servers reuse a singleton and lets tests inject a mock).\n * If omitted, a new `VisionAnalyzer` is created with baseUrl+model.\n */\n analyzer?: Pick<VisionAnalyzer, \"analyzeScreenshot\">;\n /** Optional streaming progress callback passed into the analyzer */\n onProgress?: StreamProgressCallback;\n /** Optional coarse-grained phase callback (good for spinners / WS progress) */\n onPhase?: (phase: string) => void;\n /** When true, skip calling `ensureOllamaReady` (caller is responsible). */\n skipEnsureOllama?: boolean;\n /** Optional debug dump destination (file path or directory) */\n debugDump?: string;\n /** When true, include base64 image and full styleguide in debug dump */\n debugDumpIncludeSensitive?: boolean;\n /** Optional extra metadata to include in debug dumps */\n debugDumpMetadata?: Record<string, unknown>;\n /** Optional custom path resolver (for handling workspace-relative paths like \"@apps/...\") */\n pathResolver?: PathResolver;\n};\n\nexport type RunVisionAnalysisResult = {\n issues: VisionIssue[];\n analysisTime: number;\n prompt?: string;\n rawResponse?: string;\n styleguideLocation: string | null;\n visionModel: string;\n baseUrl: string;\n};\n\nconst ollamaReadyOnce = new Map<string, Promise<void>>();\n\nasync function ensureOllamaReadyCached(params: {\n model: string;\n baseUrl: string;\n}): Promise<void> {\n const key = `${params.baseUrl}::${params.model}`;\n const existing = ollamaReadyOnce.get(key);\n if (existing) return existing;\n\n const p = ensureOllamaReady({ model: params.model, baseUrl: params.baseUrl })\n .then(() => undefined)\n .catch((e) => {\n // If startup/pull fails, allow retry on next request.\n ollamaReadyOnce.delete(key);\n throw e;\n });\n ollamaReadyOnce.set(key, p);\n return p;\n}\n\nfunction writeVisionDebugDump(params: {\n dumpPath: string;\n now: Date;\n inputs: {\n imageBase64: string;\n manifest: ElementManifest[];\n styleguideLocation: string | null;\n styleGuide: string | null;\n };\n runtime: { visionModel: string; baseUrl: string };\n includeSensitive: boolean;\n metadata?: Record<string, unknown>;\n pathResolver?: PathResolver;\n}): string {\n const resolvePath = params.pathResolver ?? defaultPathResolver;\n const resolvedDirOrFile = resolvePath(params.dumpPath, process.cwd());\n const safeStamp = params.now.toISOString().replace(/[:.]/g, \"-\");\n const dumpFile =\n resolvedDirOrFile.endsWith(\".json\") || resolvedDirOrFile.endsWith(\".jsonl\")\n ? resolvedDirOrFile\n : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;\n\n mkdirSync(dirname(dumpFile), { recursive: true });\n writeFileSync(\n dumpFile,\n JSON.stringify(\n {\n version: 1,\n timestamp: params.now.toISOString(),\n runtime: params.runtime,\n metadata: params.metadata ?? null,\n inputs: {\n imageBase64: params.includeSensitive\n ? params.inputs.imageBase64\n : \"(omitted; set debugDumpIncludeSensitive=true)\",\n manifest: params.inputs.manifest,\n styleguideLocation: params.inputs.styleguideLocation,\n styleGuide: params.includeSensitive\n ? params.inputs.styleGuide\n : \"(omitted; set debugDumpIncludeSensitive=true)\",\n },\n },\n null,\n 2\n ),\n \"utf-8\"\n );\n\n return dumpFile;\n}\n\nexport async function runVisionAnalysis(\n args: RunVisionAnalysisArgs\n): Promise<RunVisionAnalysisResult> {\n const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;\n const baseUrl = args.baseUrl ?? \"http://localhost:11434\";\n\n let styleGuide: string | null = null;\n let styleguideLocation: string | null = null;\n\n if (args.styleGuide !== undefined) {\n styleGuide = args.styleGuide;\n styleguideLocation = args.styleguideLocation ?? null;\n } else {\n args.onPhase?.(\"Resolving styleguide...\");\n const resolved = await resolveVisionStyleGuide({\n projectPath: args.projectPath,\n styleguide: args.styleguide,\n startDir: args.styleguideStartDir,\n pathResolver: args.pathResolver,\n });\n styleGuide = resolved.styleGuide;\n styleguideLocation = resolved.styleguideLocation;\n }\n\n if (!args.skipEnsureOllama) {\n args.onPhase?.(\"Preparing Ollama...\");\n await ensureOllamaReadyCached({ model: visionModel, baseUrl });\n }\n\n if (args.debugDump) {\n writeVisionDebugDump({\n dumpPath: args.debugDump,\n now: new Date(),\n runtime: { visionModel, baseUrl },\n inputs: {\n imageBase64: args.imageBase64,\n manifest: args.manifest,\n styleguideLocation,\n styleGuide,\n },\n includeSensitive: Boolean(args.debugDumpIncludeSensitive),\n metadata: args.debugDumpMetadata,\n pathResolver: args.pathResolver,\n });\n }\n\n const analyzer =\n args.analyzer ??\n new VisionAnalyzer({\n baseUrl: args.baseUrl,\n visionModel,\n });\n\n args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);\n const result: AnalyzerResult = await analyzer.analyzeScreenshot(\n args.imageBase64,\n args.manifest,\n {\n styleGuide,\n onProgress: args.onProgress,\n }\n );\n\n args.onPhase?.(\n `Done (${result.issues.length} issues, ${result.analysisTime}ms)`\n );\n\n return {\n issues: result.issues,\n analysisTime: result.analysisTime,\n // Prompt is available in newer uilint-core versions; keep this resilient across versions.\n prompt: (result as any).prompt,\n rawResponse: result.rawResponse,\n styleguideLocation,\n visionModel,\n baseUrl,\n };\n}\n\nexport type WriteVisionMarkdownReportArgs = {\n /** Absolute path to the source image file */\n imagePath: string;\n /** Route label (optional; included in report) */\n route?: string;\n /** Unix ms timestamp (optional; included in report) */\n timestamp?: number;\n visionModel?: string;\n baseUrl?: string;\n analysisTimeMs?: number;\n prompt?: string | null;\n rawResponse?: string | null;\n /** Extra JSON-ish metadata to include */\n metadata?: Record<string, unknown>;\n /**\n * Optional output path; if omitted, writes alongside the image:\n * `<basename>.vision.md`\n */\n outPath?: string;\n};\n\nexport function writeVisionMarkdownReport(\n args: WriteVisionMarkdownReportArgs\n): {\n outPath: string;\n content: string;\n} {\n const p = parse(args.imagePath);\n const outPath = args.outPath ?? join(p.dir, `${p.name || p.base}.vision.md`);\n\n const lines: string[] = [];\n lines.push(`# UILint Vision Report`);\n lines.push(``);\n lines.push(`- Image: \\`${p.base}\\``);\n if (args.route) lines.push(`- Route: \\`${args.route}\\``);\n if (typeof args.timestamp === \"number\") {\n lines.push(`- Timestamp: \\`${new Date(args.timestamp).toISOString()}\\``);\n }\n if (args.visionModel) lines.push(`- Model: \\`${args.visionModel}\\``);\n if (args.baseUrl) lines.push(`- Ollama baseUrl: \\`${args.baseUrl}\\``);\n if (typeof args.analysisTimeMs === \"number\")\n lines.push(`- Analysis time: \\`${args.analysisTimeMs}ms\\``);\n lines.push(`- Generated: \\`${new Date().toISOString()}\\``);\n lines.push(``);\n\n if (args.metadata && Object.keys(args.metadata).length > 0) {\n lines.push(`## Metadata`);\n lines.push(``);\n lines.push(\"```json\");\n lines.push(JSON.stringify(args.metadata, null, 2));\n lines.push(\"```\");\n lines.push(``);\n }\n\n lines.push(`## Prompt`);\n lines.push(``);\n lines.push(\"```text\");\n lines.push((args.prompt ?? \"\").trim());\n lines.push(\"```\");\n lines.push(``);\n\n lines.push(`## Raw Response`);\n lines.push(``);\n lines.push(\"```text\");\n lines.push((args.rawResponse ?? \"\").trim());\n lines.push(\"```\");\n lines.push(``);\n\n const content = lines.join(\"\\n\");\n mkdirSync(dirname(outPath), { recursive: true });\n writeFileSync(outPath, content, \"utf-8\");\n return { outPath, content };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/analyzer/vision-analyzer.ts","../src/vision-run.ts"],"sourcesContent":["/**\n * Vision analyzer for Ollama vision LLM analysis\n *\n * This module provides functionality to analyze screenshots using Ollama's\n * vision models (e.g., qwen3-vl, llava) to detect UI consistency issues\n * that are only visible in rendered output.\n *\n * Uses the official ollama npm package with the chat API for proper vision support.\n */\n\nimport { Ollama } from \"ollama\";\nimport type {\n OllamaClientOptions,\n StreamProgressCallback,\n LLMInstrumentationCallbacks,\n InstrumentationSpan,\n} from \"uilint-core\";\n\nimport type {\n ElementManifest,\n VisionIssue,\n} from \"../types.js\";\n\n/**\n * Internal result type for the analyzer\n * (Different from VisionAnalysisResult which includes route/timestamp/manifest)\n */\nexport interface AnalyzerResult {\n /** Detected issues */\n issues: VisionIssue[];\n /** Analysis time in milliseconds */\n analysisTime: number;\n /** Full prompt sent to the model (for debugging/reports) */\n prompt?: string;\n /** Raw LLM response (for debugging) */\n rawResponse?: string;\n}\n\n/**\n * Default vision model - qwen3-vl is recommended for UI analysis\n */\nexport const UILINT_DEFAULT_VISION_MODEL = \"qwen3-vl:8b-instruct\";\n\nfunction stripCodeFences(input: string): string {\n const trimmed = (input ?? \"\").trim();\n if (!trimmed.startsWith(\"```\")) return trimmed;\n const m = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n return m ? m[1]!.trim() : trimmed;\n}\n\nfunction extractJsonObject(input: string): string {\n const s = stripCodeFences(input).trim();\n if (!s) return s;\n if (s.startsWith(\"{\") && s.endsWith(\"}\")) return s;\n const start = s.indexOf(\"{\");\n const end = s.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return s.slice(start, end + 1).trim();\n }\n return s;\n}\n\n/**\n * Options for vision analysis\n */\nexport interface VisionAnalysisOptions {\n /** Style guide content (markdown) */\n styleGuide?: string | null;\n /** Progress callback for streaming */\n onProgress?: StreamProgressCallback;\n /** Custom prompt additions */\n additionalContext?: string;\n}\n\n/**\n * Vision analyzer client options\n */\nexport interface VisionAnalyzerOptions extends OllamaClientOptions {\n /** Vision model to use (default: qwen3-vl:30b) */\n visionModel?: string;\n}\n\nconst DEFAULT_BASE_URL = \"http://localhost:11434\";\nconst DEFAULT_TIMEOUT = 120000; // Vision models can be slower\n\n/**\n * Vision analyzer for UI screenshot analysis\n * Uses the ollama npm package with chat API for proper vision model support\n */\nexport class VisionAnalyzer {\n private baseUrl: string;\n private visionModel: string;\n private timeout: number;\n private instrumentation?: LLMInstrumentationCallbacks;\n private client: Ollama;\n\n constructor(options: VisionAnalyzerOptions = {}) {\n this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n this.visionModel = options.visionModel || UILINT_DEFAULT_VISION_MODEL;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n this.instrumentation = options.instrumentation;\n this.client = new Ollama({ host: this.baseUrl });\n }\n\n /**\n * Analyzes a screenshot with element manifest for UI consistency issues\n */\n async analyzeScreenshot(\n imageBase64: string,\n manifest: ElementManifest[],\n options: VisionAnalysisOptions = {}\n ): Promise<AnalyzerResult> {\n const startTime = Date.now();\n\n const prompt = this.buildVisionPrompt(manifest, options);\n\n // Start instrumentation span if available\n const spanResult = this.instrumentation?.onGenerationStart?.({\n name: \"vision-analyze\",\n model: this.visionModel,\n prompt,\n metadata: {\n manifestSize: manifest.length,\n hasStyleGuide: !!options.styleGuide,\n },\n });\n // Convert void to undefined for type compatibility\n const span: InstrumentationSpan | undefined = spanResult || undefined;\n\n try {\n const rawResponse = options.onProgress\n ? await this.chatVisionStreaming(\n imageBase64,\n prompt,\n options.onProgress,\n span\n )\n : await this.chatVision(imageBase64, prompt, span);\n\n const issues = this.parseVisionResponse(rawResponse, manifest);\n\n return {\n issues,\n analysisTime: Date.now() - startTime,\n prompt,\n rawResponse,\n };\n } catch (error) {\n const analysisTime = Date.now() - startTime;\n const err =\n error instanceof Error\n ? error\n : new Error(String(error ?? \"Unknown error\"));\n console.error(\"[VisionAnalyzer] Analysis failed\", {\n baseUrl: this.baseUrl,\n model: this.visionModel,\n manifestSize: manifest.length,\n analysisTime,\n error: err.message,\n });\n span?.end(\"\", { error: err.message });\n throw err;\n }\n }\n\n /**\n * Builds the vision analysis prompt\n */\n private buildVisionPrompt(\n manifest: ElementManifest[],\n options: VisionAnalysisOptions\n ): string {\n const styleGuideSection = options.styleGuide\n ? `## Style Guide\n${options.styleGuide}\n\n`\n : \"\";\n\n const additionalSection = options.additionalContext\n ? `## Additional Context\n${options.additionalContext}\n\n`\n : \"\";\n\n // Build element manifest section for context\n const manifestSection = this.buildManifestSection(manifest);\n\n return `You are a UI consistency analyzer examining a screenshot of a web page.\nAnalyze the visual appearance and identify any UI consistency issues.\n\n${styleGuideSection}${additionalSection}${manifestSection}\n\n## Task\n\nExamine the screenshot and identify visual consistency issues such as:\n1. **Spacing inconsistencies**: Elements with uneven padding, margins, or gaps\n2. **Alignment issues**: Elements that should be aligned but aren't\n3. **Color inconsistencies**: Similar colors that should be identical, or colors that don't match the style guide\n4. **Typography issues**: Inconsistent font sizes, weights, or families\n5. **Layout problems**: Broken layouts, overlapping elements, or visual hierarchy issues\n6. **Contrast issues**: Text that's hard to read or UI elements with insufficient contrast\n\nFor each issue found, reference the element by its visible text so we can map it back to the source code.\n\n## Response Format\n\nRespond with JSON ONLY. Return a single JSON object:\n\n\\`\\`\\`json\n{\n \"issues\": [\n {\n \"elementText\": \"Submit Order\",\n \"message\": \"Button has inconsistent padding compared to other buttons (appears larger)\",\n \"category\": \"spacing\",\n \"severity\": \"warning\"\n }\n ]\n}\n\\`\\`\\`\n\nCategories: spacing, alignment, color, typography, layout, contrast, visual-hierarchy\nSeverities: error (major issue), warning (should fix), info (minor/suggestion)\n\nIMPORTANT:\n- Reference elements by their visible text content\n- Be specific about what's wrong and what the expected state should be\n- Only report significant visual inconsistencies\n- If no issues are found, return {\"issues\": []}`;\n }\n\n /**\n * Builds the manifest section of the prompt\n */\n private buildManifestSection(manifest: ElementManifest[]): string {\n if (manifest.length === 0) {\n return \"\";\n }\n\n const elements = manifest.map((el) => {\n const parts = [`- \"${el.text}\"`];\n if (el.role) parts.push(`(${el.role})`);\n if (el.instanceCount && el.instanceCount > 1) {\n parts.push(`[${el.instanceCount} instances]`);\n }\n return parts.join(\" \");\n });\n\n return `## Page Elements\n\nThe following elements are visible on the page (reference by text when reporting issues):\n\n${elements.join(\"\\n\")}\n\n`;\n }\n\n /**\n * Non-streaming chat API call for vision\n */\n private async chatVision(\n imageBase64: string,\n prompt: string,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const response = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n format: \"json\",\n options: {\n // Increase context window for large prompts\n num_ctx: 8192,\n },\n });\n\n const output = response.message?.content || \"\";\n\n if (!output.trim()) {\n const diag = JSON.stringify(\n {\n done_reason: response.done_reason,\n model: response.model,\n prompt_eval_count: response.prompt_eval_count,\n eval_count: response.eval_count,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output. Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(output, {\n promptTokens: response.prompt_eval_count,\n completionTokens: response.eval_count,\n totalTokens:\n (response.prompt_eval_count || 0) + (response.eval_count || 0) ||\n undefined,\n });\n\n return output;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Streaming chat API call for vision\n * Uses ollama package's async iterator for streaming\n */\n private async chatVisionStreaming(\n imageBase64: string,\n prompt: string,\n onProgress: StreamProgressCallback,\n span?: InstrumentationSpan\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n let promptTokens: number | undefined;\n let completionTokens: number | undefined;\n let chunksReceived = 0;\n let lastProgressAt = Date.now();\n let emittedWaitingLine = false;\n\n try {\n // Strip data URL prefix if present\n const base64Data = imageBase64.includes(\",\")\n ? imageBase64.split(\",\")[1]!\n : imageBase64;\n\n const stream = await this.client.chat({\n model: this.visionModel,\n messages: [\n {\n role: \"user\",\n content: prompt,\n images: [base64Data],\n },\n ],\n // Enable thinking for streaming so we can surface reasoning traces for thinking-capable models.\n // Models that don't support it should ignore it.\n think: false,\n stream: true,\n format: \"json\",\n options: {\n num_ctx: 8192,\n },\n });\n\n let fullResponse = \"\";\n let lastLatestLine = \"\";\n\n for await (const chunk of stream) {\n chunksReceived++;\n\n // Emit waiting message on first chunk if no content yet\n if (!emittedWaitingLine && !fullResponse.trim()) {\n onProgress(\"(waiting for model output…)\", fullResponse);\n emittedWaitingLine = true;\n }\n\n // Thinking-capable models stream `message.thinking` separately from `message.content`.\n const thinking = chunk.message?.thinking || \"\";\n if (thinking) {\n onProgress(lastLatestLine, fullResponse, undefined, thinking);\n lastProgressAt = Date.now();\n }\n\n // Chat API uses message.content for text deltas (final answer)\n const content = chunk.message?.content || \"\";\n if (content) {\n fullResponse += content;\n\n // Get the latest line for progress display\n const responseLines = fullResponse.split(\"\\n\");\n const latestLine =\n responseLines[responseLines.length - 1] ||\n responseLines[responseLines.length - 2] ||\n \"\";\n\n // Stream the actual text delta to the callback\n lastLatestLine = latestLine.trim();\n onProgress(lastLatestLine, fullResponse, content);\n lastProgressAt = Date.now();\n }\n\n // Capture token counts from final chunk\n if (chunk.done) {\n promptTokens = chunk.prompt_eval_count;\n completionTokens = chunk.eval_count;\n }\n\n // Heartbeat if we're receiving chunks but no text output\n if (!fullResponse.trim() && Date.now() - lastProgressAt > 4000) {\n onProgress(\n `(streaming… received ${chunksReceived} chunks)`,\n fullResponse\n );\n lastProgressAt = Date.now();\n }\n }\n\n if (!fullResponse.trim()) {\n const diag = JSON.stringify(\n {\n chunksReceived,\n promptTokens,\n completionTokens,\n model: this.visionModel,\n },\n null,\n 2\n );\n const errorMsg = `Vision model returned empty output (streaming). Diagnostics: ${diag}`;\n span?.end(\"\", { error: errorMsg });\n throw new Error(errorMsg);\n }\n\n span?.end(fullResponse, {\n promptTokens,\n completionTokens,\n totalTokens: (promptTokens || 0) + (completionTokens || 0) || undefined,\n });\n\n return fullResponse;\n } catch (error) {\n span?.end(\"\", { error: String(error) });\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parses the vision LLM response and matches elements to manifest\n */\n private parseVisionResponse(\n response: string,\n manifest: ElementManifest[]\n ): VisionIssue[] {\n try {\n const jsonText = extractJsonObject(response);\n const parsed = JSON.parse(jsonText);\n const rawIssues = parsed.issues || [];\n\n // Map elementText to dataLoc using manifest\n return rawIssues.map((issue: VisionIssue) => {\n const matchedElement = this.matchElementByText(\n issue.elementText,\n manifest\n );\n\n return {\n ...issue,\n dataLoc: matchedElement?.dataLoc,\n };\n });\n } catch {\n const s = response ?? \"\";\n const previewHead = s ? s.slice(0, 600) : \"\";\n const previewTail = s && s.length > 600 ? s.slice(-400) : \"\";\n throw new Error(\n `Vision model returned non-JSON output (expected JSON). length=${\n s.length\n } head=${JSON.stringify(previewHead)} tail=${JSON.stringify(\n previewTail\n )}`\n );\n }\n }\n\n /**\n * Matches element text from LLM response to manifest entries\n */\n private matchElementByText(\n elementText: string,\n manifest: ElementManifest[]\n ): ElementManifest | undefined {\n if (!elementText) return undefined;\n\n const normalizedSearch = elementText.toLowerCase().trim();\n\n // Exact match first\n for (const entry of manifest) {\n if (entry.text.toLowerCase().trim() === normalizedSearch) {\n return entry;\n }\n }\n\n // Partial match\n for (const entry of manifest) {\n const normalizedEntry = entry.text.toLowerCase().trim();\n if (\n normalizedEntry.includes(normalizedSearch) ||\n normalizedSearch.includes(normalizedEntry)\n ) {\n return entry;\n }\n }\n\n return undefined;\n }\n\n /**\n * Checks if the vision model is available\n */\n async isAvailable(): Promise<boolean> {\n try {\n const models = await this.client.list();\n return models.models.some(\n (m) =>\n m.name === this.visionModel ||\n m.name.startsWith(this.visionModel.split(\":\")[0])\n );\n } catch {\n return false;\n }\n }\n\n /**\n * Gets the current vision model\n */\n getModel(): string {\n return this.visionModel;\n }\n\n /**\n * Gets the Ollama base URL (for diagnostics)\n */\n getBaseUrl(): string {\n return this.baseUrl;\n }\n\n /**\n * Sets the vision model\n */\n setModel(model: string): void {\n this.visionModel = model;\n }\n\n /**\n * Sets instrumentation callbacks\n */\n setInstrumentation(\n instrumentation: LLMInstrumentationCallbacks | undefined\n ): void {\n this.instrumentation = instrumentation;\n }\n}\n\n// Default singleton instance\nlet defaultAnalyzer: VisionAnalyzer | null = null;\n\n/**\n * Gets the default VisionAnalyzer instance\n */\nexport function getVisionAnalyzer(\n options?: VisionAnalyzerOptions\n): VisionAnalyzer {\n if (!defaultAnalyzer || options) {\n defaultAnalyzer = new VisionAnalyzer(options);\n }\n return defaultAnalyzer;\n}\n","/**\n * Vision analysis utilities for running vision analysis with Ollama.\n *\n * These utilities provide high-level orchestration for vision analysis,\n * including style guide resolution, Ollama readiness checks, and debug dump generation.\n */\n\nimport { dirname, join, parse, resolve } from \"path\";\nimport { existsSync, statSync, mkdirSync, writeFileSync } from \"fs\";\nimport {\n ensureOllamaReady,\n findStyleGuidePath,\n findUILintStyleGuideUpwards,\n readStyleGuide,\n type StreamProgressCallback,\n} from \"uilint-core/node\";\nimport {\n VisionAnalyzer,\n UILINT_DEFAULT_VISION_MODEL,\n type AnalyzerResult,\n} from \"./analyzer/vision-analyzer.js\";\nimport type { ElementManifest, VisionIssue } from \"./types.js\";\n\n/**\n * Path resolver function type.\n * Resolves a path specifier (which may include workspace-relative \"@\" prefixes or other formats)\n * into an absolute filesystem path.\n */\nexport type PathResolver = (spec: string, cwd: string) => string;\n\n/**\n * Default path resolver that just uses path.resolve.\n * CLI callers can provide a custom resolver that handles workspace-relative paths.\n */\nconst defaultPathResolver: PathResolver = (spec: string, cwd: string) => {\n const raw = (spec ?? \"\").trim();\n if (!raw) return resolve(cwd, spec);\n return resolve(cwd, raw);\n};\n\nexport type ResolveVisionStyleGuideArgs = {\n /** Project root / cwd used to resolve relative path specifiers */\n projectPath: string;\n /** Path to style guide file OR project directory */\n styleguide?: string;\n /** A starting point for upward search (directory). Defaults to projectPath. */\n startDir?: string;\n /** Optional custom path resolver (for handling workspace-relative paths like \"@apps/...\") */\n pathResolver?: PathResolver;\n};\n\nexport type ResolveVisionStyleGuideResult = {\n styleGuide: string | null;\n styleguideLocation: string | null;\n};\n\nexport async function resolveVisionStyleGuide(\n args: ResolveVisionStyleGuideArgs\n): Promise<ResolveVisionStyleGuideResult> {\n const projectPath = args.projectPath;\n const startDir = args.startDir ?? projectPath;\n const resolvePath = args.pathResolver ?? defaultPathResolver;\n\n if (args.styleguide) {\n const styleguideArg = resolvePath(args.styleguide, projectPath);\n if (existsSync(styleguideArg)) {\n const stat = statSync(styleguideArg);\n if (stat.isFile()) {\n return {\n styleguideLocation: styleguideArg,\n styleGuide: await readStyleGuide(styleguideArg),\n };\n }\n if (stat.isDirectory()) {\n const found = findStyleGuidePath(styleguideArg);\n return {\n styleguideLocation: found,\n styleGuide: found ? await readStyleGuide(found) : null,\n };\n }\n }\n return { styleGuide: null, styleguideLocation: null };\n }\n\n const upwards = findUILintStyleGuideUpwards(startDir);\n const fallback = upwards ?? findStyleGuidePath(projectPath);\n return {\n styleguideLocation: fallback,\n styleGuide: fallback ? await readStyleGuide(fallback) : null,\n };\n}\n\nexport type RunVisionAnalysisArgs = {\n /** base64 image data (NO data: prefix) */\n imageBase64: string;\n manifest: ElementManifest[];\n /** Used to resolve styleguide arg and as default search root */\n projectPath: string;\n /** Path to style guide file OR directory; if omitted uses upward search */\n styleguide?: string;\n /** Directory for upward styleguide search; defaults to projectPath */\n styleguideStartDir?: string;\n /**\n * Override resolved styleguide content (lets callers do logging/notes based on resolution once).\n * If provided (even null), styleguide resolution is skipped.\n */\n styleGuide?: string | null;\n /**\n * Override resolved styleguide location (pairs with styleGuide override).\n * If provided, this is returned in the result.\n */\n styleguideLocation?: string | null;\n /** Ollama base URL (default: http://localhost:11434) */\n baseUrl?: string;\n /** Vision model override (default: UILINT_DEFAULT_VISION_MODEL) */\n model?: string;\n /**\n * Optional analyzer instance (lets servers reuse a singleton and lets tests inject a mock).\n * If omitted, a new `VisionAnalyzer` is created with baseUrl+model.\n */\n analyzer?: Pick<VisionAnalyzer, \"analyzeScreenshot\">;\n /** Optional streaming progress callback passed into the analyzer */\n onProgress?: StreamProgressCallback;\n /** Optional coarse-grained phase callback (good for spinners / WS progress) */\n onPhase?: (phase: string) => void;\n /** When true, skip calling `ensureOllamaReady` (caller is responsible). */\n skipEnsureOllama?: boolean;\n /** Optional debug dump destination (file path or directory) */\n debugDump?: string;\n /** When true, include base64 image and full styleguide in debug dump */\n debugDumpIncludeSensitive?: boolean;\n /** Optional extra metadata to include in debug dumps */\n debugDumpMetadata?: Record<string, unknown>;\n /** Optional custom path resolver (for handling workspace-relative paths like \"@apps/...\") */\n pathResolver?: PathResolver;\n};\n\nexport type RunVisionAnalysisResult = {\n issues: VisionIssue[];\n analysisTime: number;\n prompt?: string;\n rawResponse?: string;\n styleguideLocation: string | null;\n visionModel: string;\n baseUrl: string;\n};\n\nconst ollamaReadyOnce = new Map<string, Promise<void>>();\n\nasync function ensureOllamaReadyCached(params: {\n model: string;\n baseUrl: string;\n}): Promise<void> {\n const key = `${params.baseUrl}::${params.model}`;\n const existing = ollamaReadyOnce.get(key);\n if (existing) return existing;\n\n const p = ensureOllamaReady({ model: params.model, baseUrl: params.baseUrl })\n .then(() => undefined)\n .catch((e: unknown) => {\n // If startup/pull fails, allow retry on next request.\n ollamaReadyOnce.delete(key);\n throw e;\n });\n ollamaReadyOnce.set(key, p);\n return p;\n}\n\nfunction writeVisionDebugDump(params: {\n dumpPath: string;\n now: Date;\n inputs: {\n imageBase64: string;\n manifest: ElementManifest[];\n styleguideLocation: string | null;\n styleGuide: string | null;\n };\n runtime: { visionModel: string; baseUrl: string };\n includeSensitive: boolean;\n metadata?: Record<string, unknown>;\n pathResolver?: PathResolver;\n}): string {\n const resolvePath = params.pathResolver ?? defaultPathResolver;\n const resolvedDirOrFile = resolvePath(params.dumpPath, process.cwd());\n const safeStamp = params.now.toISOString().replace(/[:.]/g, \"-\");\n const dumpFile =\n resolvedDirOrFile.endsWith(\".json\") || resolvedDirOrFile.endsWith(\".jsonl\")\n ? resolvedDirOrFile\n : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;\n\n mkdirSync(dirname(dumpFile), { recursive: true });\n writeFileSync(\n dumpFile,\n JSON.stringify(\n {\n version: 1,\n timestamp: params.now.toISOString(),\n runtime: params.runtime,\n metadata: params.metadata ?? null,\n inputs: {\n imageBase64: params.includeSensitive\n ? params.inputs.imageBase64\n : \"(omitted; set debugDumpIncludeSensitive=true)\",\n manifest: params.inputs.manifest,\n styleguideLocation: params.inputs.styleguideLocation,\n styleGuide: params.includeSensitive\n ? params.inputs.styleGuide\n : \"(omitted; set debugDumpIncludeSensitive=true)\",\n },\n },\n null,\n 2\n ),\n \"utf-8\"\n );\n\n return dumpFile;\n}\n\nexport async function runVisionAnalysis(\n args: RunVisionAnalysisArgs\n): Promise<RunVisionAnalysisResult> {\n const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;\n const baseUrl = args.baseUrl ?? \"http://localhost:11434\";\n\n let styleGuide: string | null = null;\n let styleguideLocation: string | null = null;\n\n if (args.styleGuide !== undefined) {\n styleGuide = args.styleGuide;\n styleguideLocation = args.styleguideLocation ?? null;\n } else {\n args.onPhase?.(\"Resolving styleguide...\");\n const resolved = await resolveVisionStyleGuide({\n projectPath: args.projectPath,\n styleguide: args.styleguide,\n startDir: args.styleguideStartDir,\n pathResolver: args.pathResolver,\n });\n styleGuide = resolved.styleGuide;\n styleguideLocation = resolved.styleguideLocation;\n }\n\n if (!args.skipEnsureOllama) {\n args.onPhase?.(\"Preparing Ollama...\");\n await ensureOllamaReadyCached({ model: visionModel, baseUrl });\n }\n\n if (args.debugDump) {\n writeVisionDebugDump({\n dumpPath: args.debugDump,\n now: new Date(),\n runtime: { visionModel, baseUrl },\n inputs: {\n imageBase64: args.imageBase64,\n manifest: args.manifest,\n styleguideLocation,\n styleGuide,\n },\n includeSensitive: Boolean(args.debugDumpIncludeSensitive),\n metadata: args.debugDumpMetadata,\n pathResolver: args.pathResolver,\n });\n }\n\n const analyzer =\n args.analyzer ??\n new VisionAnalyzer({\n baseUrl: args.baseUrl,\n visionModel,\n });\n\n args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);\n const result: AnalyzerResult = await analyzer.analyzeScreenshot(\n args.imageBase64,\n args.manifest,\n {\n styleGuide,\n onProgress: args.onProgress,\n }\n );\n\n args.onPhase?.(\n `Done (${result.issues.length} issues, ${result.analysisTime}ms)`\n );\n\n return {\n issues: result.issues,\n analysisTime: result.analysisTime,\n // Prompt is available in newer uilint-core versions; keep this resilient across versions.\n prompt: result.prompt,\n rawResponse: result.rawResponse,\n styleguideLocation,\n visionModel,\n baseUrl,\n };\n}\n\nexport type WriteVisionMarkdownReportArgs = {\n /** Absolute path to the source image file */\n imagePath: string;\n /** Route label (optional; included in report) */\n route?: string;\n /** Unix ms timestamp (optional; included in report) */\n timestamp?: number;\n visionModel?: string;\n baseUrl?: string;\n analysisTimeMs?: number;\n prompt?: string | null;\n rawResponse?: string | null;\n /** Extra JSON-ish metadata to include */\n metadata?: Record<string, unknown>;\n /**\n * Optional output path; if omitted, writes alongside the image:\n * `<basename>.vision.md`\n */\n outPath?: string;\n};\n\nexport function writeVisionMarkdownReport(\n args: WriteVisionMarkdownReportArgs\n): {\n outPath: string;\n content: string;\n} {\n const p = parse(args.imagePath);\n const outPath = args.outPath ?? join(p.dir, `${p.name || p.base}.vision.md`);\n\n const lines: string[] = [];\n lines.push(`# UILint Vision Report`);\n lines.push(``);\n lines.push(`- Image: \\`${p.base}\\``);\n if (args.route) lines.push(`- Route: \\`${args.route}\\``);\n if (typeof args.timestamp === \"number\") {\n lines.push(`- Timestamp: \\`${new Date(args.timestamp).toISOString()}\\``);\n }\n if (args.visionModel) lines.push(`- Model: \\`${args.visionModel}\\``);\n if (args.baseUrl) lines.push(`- Ollama baseUrl: \\`${args.baseUrl}\\``);\n if (typeof args.analysisTimeMs === \"number\")\n lines.push(`- Analysis time: \\`${args.analysisTimeMs}ms\\``);\n lines.push(`- Generated: \\`${new Date().toISOString()}\\``);\n lines.push(``);\n\n if (args.metadata && Object.keys(args.metadata).length > 0) {\n lines.push(`## Metadata`);\n lines.push(``);\n lines.push(\"```json\");\n lines.push(JSON.stringify(args.metadata, null, 2));\n lines.push(\"```\");\n lines.push(``);\n }\n\n lines.push(`## Prompt`);\n lines.push(``);\n lines.push(\"```text\");\n lines.push((args.prompt ?? \"\").trim());\n lines.push(\"```\");\n lines.push(``);\n\n lines.push(`## Raw Response`);\n lines.push(``);\n lines.push(\"```text\");\n lines.push((args.rawResponse ?? \"\").trim());\n lines.push(\"```\");\n lines.push(``);\n\n const content = lines.join(\"\\n\");\n mkdirSync(dirname(outPath), { recursive: true });\n writeFileSync(outPath, content, \"utf-8\");\n return { outPath, content };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uilint-vision",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.142",
|
|
4
4
|
"description": "Vision-based UI consistency analysis for UILint",
|
|
5
5
|
"author": "Peter Suggate",
|
|
6
6
|
"repository": {
|
|
@@ -40,14 +40,18 @@
|
|
|
40
40
|
"./eslint-rules/semantic-vision": {
|
|
41
41
|
"types": "./dist/eslint-rules/semantic-vision.d.ts",
|
|
42
42
|
"import": "./dist/eslint-rules/semantic-vision.js"
|
|
43
|
+
},
|
|
44
|
+
"./cli-manifest": {
|
|
45
|
+
"types": "./dist/cli-manifest.d.ts",
|
|
46
|
+
"import": "./dist/cli-manifest.js"
|
|
43
47
|
}
|
|
44
48
|
},
|
|
45
49
|
"files": [
|
|
46
50
|
"dist"
|
|
47
51
|
],
|
|
48
52
|
"peerDependencies": {
|
|
49
|
-
"uilint-core": "0.2.
|
|
50
|
-
"uilint-eslint": "0.2.
|
|
53
|
+
"uilint-core": "0.2.142",
|
|
54
|
+
"uilint-eslint": "0.2.142"
|
|
51
55
|
},
|
|
52
56
|
"dependencies": {
|
|
53
57
|
"html-to-image": "^1.11.13"
|
|
@@ -64,6 +68,8 @@
|
|
|
64
68
|
"build": "tsup",
|
|
65
69
|
"dev": "tsup --watch",
|
|
66
70
|
"test": "vitest",
|
|
71
|
+
"lint": "eslint src/",
|
|
72
|
+
"lint:strict": "eslint src/ --max-warnings 0",
|
|
67
73
|
"typecheck": "tsc --noEmit"
|
|
68
74
|
}
|
|
69
75
|
}
|