Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import chat from './routes/chat.js';
import stream from './routes/stream.js';
import session from './routes/session.js';
import flow from './routes/flow.js';
import cognitiveWalkthrough from './routes/cognitive-walkthrough.js';
import pureScoring from './routes/pure-scoring.js';
import brandConsistency from './routes/brand-consistency.js';
import copyTone from './routes/copy-tone.js';
import personaResearch from './routes/persona-research.js';
import responsive from './routes/responsive.js';
import a11ySpec from './routes/a11y-spec.js';
import darkMode from './routes/dark-mode.js';
import pageSweep from './routes/page-sweep.js';
import { cleanupExpiredSessions } from './db/queries.js';
import { mkdirSync, existsSync } from 'fs';
import { dirname } from 'path';
Expand Down Expand Up @@ -65,6 +74,15 @@ app.route('/api', chat);
app.route('/api', stream);
app.route('/api', session);
app.route('/api', flow);
app.route('/api', cognitiveWalkthrough);
app.route('/api', pureScoring);
app.route('/api', brandConsistency);
app.route('/api', copyTone);
app.route('/api', personaResearch);
app.route('/api', responsive);
app.route('/api', a11ySpec);
app.route('/api', darkMode);
app.route('/api', pageSweep);

// Root
app.get('/', (c) => c.json({ name: 'FigmaLint Design Review API', version: '1.0.0' }));
Expand Down
67 changes: 67 additions & 0 deletions backend/src/prompts/a11y-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Build the AI prompt for generating a comprehensive accessibility specification.
*/
export function buildA11ySpecPrompt(
componentInfo: string,
lintSummary: string,
): string {
return `You are a WCAG 2.2 accessibility specialist generating a comprehensive accessibility specification from a UI design.

## Component Information
${componentInfo}

## Lint Summary (deterministic analysis)
${lintSummary}

## Your Task
Analyze the screenshot and generate a complete accessibility specification. This spec will be used by developers to implement proper ARIA attributes, keyboard navigation, focus management, and screen reader support.

For each section, be specific to what you see in the design — don't generate generic boilerplate.

Respond in this exact JSON format:
{
"landmarks": [
{ "role": "<ARIA landmark role: banner|navigation|main|complementary|contentinfo|search|form|region>", "label": "<accessible name for the landmark>", "element": "<which UI element this maps to>" }
],
"headingStructure": [
{ "level": 1, "text": "<heading text>", "element": "<which UI element>" }
],
"focusOrder": [
{ "order": 1, "element": "<element description>", "type": "<interactive|informational|container>", "notes": "<special focus management notes>" }
],
"ariaAnnotations": [
{
"element": "<element description>",
"role": "<ARIA role if non-default>",
"ariaLabel": "<aria-label value if needed>",
"ariaDescribedBy": "<id of describing element if applicable>",
"ariaLive": "<polite|assertive if applicable>",
"notes": "<implementation notes>"
}
],
"keyboardShortcuts": [
{ "key": "<key combination e.g. Enter, Space, Escape, Tab, Arrow keys>", "action": "<what happens>", "element": "<which element>" }
],
"liveRegions": [
{ "element": "<element>", "type": "polite|assertive", "trigger": "<what causes the update>" }
],
"colorContrastReport": [
{ "element": "<element>", "foreground": "<color>", "background": "<color>", "ratio": 4.5, "passes": "AA|AAA|fail" }
],
"recommendations": [
{
"title": "<concise title>",
"description": "<detailed recommendation>",
"wcagCriterion": "<e.g. 1.4.3 Contrast (Minimum)>",
"level": "A|AA|AAA"
}
]
}

Important:
- For focusOrder, list ALL interactive elements in the logical tab order you observe
- For ariaAnnotations, include every element that needs non-default ARIA attributes
- For colorContrastReport, estimate contrast ratios from what you see — flag anything that appears to have low contrast
- For recommendations, reference specific WCAG 2.2 success criteria
- Be thorough but avoid false positives — only flag real issues`;
}
63 changes: 63 additions & 0 deletions backend/src/prompts/attention.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { GROUNDING_INSTRUCTIONS } from './shared/grounding-instructions.js';

/**
* Attention/visual-hierarchy analysis prompt.
* Used by the extended analyzer to evaluate where the eye is drawn.
*/
export function buildAttentionPrompt(componentInfo: string): string {
return `Analyze the visual attention flow in this UI screenshot.

## Context
${componentInfo}

## Evaluation Criteria

### 1. Focal Point Clarity
- Is there one clear primary focal point?
- Does the visual hierarchy guide the eye in a logical sequence (primary -> secondary -> tertiary)?
- Are F-pattern or Z-pattern reading flows supported?

### 2. Visual Weight Distribution
- Is visual weight (size, color, contrast) distributed intentionally?
- Do important elements have the most visual weight?
- Are decorative elements competing with functional ones?

### 3. CTA Prominence
- Is the primary call-to-action the most visually prominent interactive element?
- Is there clear differentiation between primary, secondary, and tertiary actions?
- Can a user identify the next step within 3 seconds?

### 4. Information Density
- Is the content density appropriate for the page type?
- Are there clear content groups separated by whitespace?
- Is progressive disclosure used where appropriate?

## Response Format (JSON)
{
"focalPoint": {
"exists": true|false,
"element": "<description of primary focal point>",
"strength": "strong|moderate|weak"
},
"readingFlow": {
"pattern": "F-pattern|Z-pattern|scattered|linear",
"blockers": ["<elements that interrupt natural flow>"]
},
"ctaProminence": {
"rating": "pass|needs_improvement|fail",
"primaryCta": "<description>",
"competingElements": ["<element competing for attention>"]
},
"findings": [
{
"finding": "<specific observation>",
"confidence": 0.0,
"evidence": "<element or region reference>",
"category": "attention",
"severity": "critical|warning|info"
}
],
"summary": "<2-3 sentence summary>"
}
${GROUNDING_INSTRUCTIONS}`;
}
122 changes: 122 additions & 0 deletions backend/src/prompts/brand-consistency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Brand Consistency Analysis prompt.
* Evaluates a design screenshot against a structured brand guide.
*/

export interface BrandGuide {
colors: Record<string, { hex: string; tolerance: number; usage: string }>;
typography: {
heading: { family: string; weights: number[] };
body: { family: string; weights: number[] };
};
spacing: { base: number; scale: number[] };
personality: string[]; // e.g., ['professional', 'approachable', 'modern']
rules?: Array<{ id: string; description: string; severity: 'error' | 'warning' }>;
}

export function buildBrandConsistencyPrompt(
brandGuide: BrandGuide,
lintContext: string,
): string {
// Serialize color palette for the prompt
const colorLines = Object.entries(brandGuide.colors)
.map(
([name, c]) =>
` - ${name}: ${c.hex} (tolerance: +/-${c.tolerance}%, usage: ${c.usage})`,
)
.join('\n');

// Serialize typography
const typoLines = [
` Headings: ${brandGuide.typography.heading.family} [weights: ${brandGuide.typography.heading.weights.join(', ')}]`,
` Body: ${brandGuide.typography.body.family} [weights: ${brandGuide.typography.body.weights.join(', ')}]`,
].join('\n');

// Serialize spacing
const spacingLine = ` Base unit: ${brandGuide.spacing.base}px, scale: [${brandGuide.spacing.scale.join(', ')}]`;

// Serialize personality
const personalityLine = brandGuide.personality.join(', ');

// Serialize custom rules
const rulesBlock =
brandGuide.rules && brandGuide.rules.length > 0
? `\n## Custom Brand Rules\n${brandGuide.rules.map((r) => ` - [${r.severity.toUpperCase()}] ${r.id}: ${r.description}`).join('\n')}\n`
: '';

return `You are a brand design auditor. Evaluate the screenshot against the brand guidelines provided below. Be precise and cite specific visual evidence from the screenshot.

## Brand Color Palette
${colorLines}

## Typography Specifications
${typoLines}

## Spacing System
${spacingLine}

## Brand Personality Keywords
${personalityLine}
${rulesBlock}
## Automated Lint Context
${lintContext}

## Evaluation Criteria

### 1. Color Palette Adherence
Examine every visible color in the UI. Compare each against the brand palette. Flag colors that fall outside the specified tolerance. Note which element uses the off-brand color and what the expected color should be.

### 2. Typography Compliance
Check all visible text for correct font family and weight usage. Headings must use the heading family/weights. Body text must use the body family/weights. Flag any text that appears to use a non-specified font or weight.

### 3. Spacing System Consistency
Check that spacing between elements aligns with the base grid and scale values. Flag values that do not correspond to any value in the spacing scale. Estimate spacing in pixels from the screenshot.

### 4. Brand Personality Match
Assess whether the overall visual impression matches the listed personality keywords. Consider color warmth/coolness, typography tone, whitespace usage, imagery style, and overall aesthetic. Rate the match as "strong", "moderate", or "weak" with specific evidence.

### 5. Visual Language Consistency
Evaluate iconography style (outlined vs filled, rounded vs sharp), illustration style if present, and photography treatment. Flag inconsistencies within the screenshot.

Respond in this exact JSON format:
{
"overallScore": <0-100>,
"colorCompliance": {
"score": <0-100>,
"violations": [
{
"element": "<element description>",
"found": "<hex or description of found color>",
"expected": "<expected brand color name and hex>",
"tolerance": <tolerance percentage that was exceeded>
}
]
},
"typographyCompliance": {
"score": <0-100>,
"violations": [
{
"element": "<element description>",
"found": "<found font/weight>",
"expected": "<expected font/weight>"
}
]
},
"spacingCompliance": {
"score": <0-100>,
"offGridValues": [<pixel values that don't match the spacing scale>]
},
"personalityMatch": {
"rating": "strong|moderate|weak",
"evidence": ["<specific observation 1>", "<specific observation 2>"]
},
"recommendations": [
{
"title": "<short title>",
"description": "<actionable recommendation>",
"severity": "error|warning|info"
}
],
"summary": "<3-5 sentence overall brand consistency assessment>"
}`;
}
68 changes: 64 additions & 4 deletions backend/src/prompts/chat-followup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
/**
* Build a structured follow-up prompt for the conversational chat phase.
*
* Injects the current analysis context (score, top issues, recent fixes)
* and tells the model exactly what capabilities the user can ask about.
*/
export function buildFollowupPrompt(
sessionContext: string,
analysisJson?: string,
Comment on lines 8 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd 'session\.ts$' backend/src/services --exec sed -n '1,260p' {}

echo
rg -n -C3 --type ts '\bai_review\b|\baiReview\b|saveAnalysisResult\s*\(|loadSession\s*\(' backend/src

Repository: lemone112/figmalint

Length of output: 13474


В saveAnalysisResult() не сериализуется aiReview перед сохранением в БД.

analyzer.ts передаёт aiReview как объект AiReviewResult, но saveAnalysisResult() (строка 25) сохраняет его напрямую без JSON.stringify(). База данных ожидает ai_review: string | null (backend/src/db/queries.ts:34), поэтому объект будет сохранён как "[object Object]" или аналогичная поломанная представление. Когда позже loadSession() возвращает эту строку и buildFollowupPrompt() пытается её распарсить, JSON.parse() молча падает в catch, и данные AiReviewResult теряются.

Исправление: в saveAnalysisResult() нужно добавить условное сериализацию для aiReview и lintResult:

ai_review: typeof aiReview === 'string' ? aiReview : JSON.stringify(aiReview),
lint_result: typeof lintResult === 'string' ? lintResult : JSON.stringify(lintResult),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/prompts/chat-followup.ts` around lines 8 - 9, saveAnalysisResult
currently writes aiReview and lintResult objects directly to the DB causing them
to be stored as "[object Object]" and later fail to parse in
loadSession/buildFollowupPrompt; update saveAnalysisResult to conditionally
serialize these fields before saving (e.g., for aiReview and lintResult use
typeof value === 'string' ? value : JSON.stringify(value)) so the ai_review
column receives a proper JSON string or null.

): string {
return `You are a design review assistant helping a designer improve their component.
// Extract top issues from the analysis JSON if available
let topIssuesSummary = '';
if (analysisJson) {
try {
const analysis = JSON.parse(analysisJson);
const failCategories: string[] = [];
const needsImprovementCategories: string[] = [];

Session context:
${sessionContext}
for (const [key, value] of Object.entries(analysis)) {
const cat = value as Record<string, unknown> | undefined;
if (cat && typeof cat === 'object' && 'rating' in cat) {
if (cat.rating === 'fail') failCategories.push(key);
else if (cat.rating === 'needs_improvement') needsImprovementCategories.push(key);
}
}

Respond naturally and helpfully. Be specific about what to fix and why. Keep responses concise.`;
if (failCategories.length > 0) {
topIssuesSummary += `\nFailing categories: ${failCategories.join(', ')}`;
}
if (needsImprovementCategories.length > 0) {
topIssuesSummary += `\nNeeds improvement: ${needsImprovementCategories.join(', ')}`;
}
} catch {
// Analysis JSON not parseable — proceed without it
}
}

return `You are a senior design review assistant embedded in FigmaLint, a Figma plugin that analyzes design quality. You are in a follow-up conversation after an initial analysis has been completed.

## Current Analysis Context
${sessionContext}${topIssuesSummary}

## Your Capabilities
When the user asks questions, you can help with:

1. **Explain findings** — Explain why a specific issue matters and cite design principles (Gestalt, WCAG, Nielsen heuristics)
2. **Prioritize fixes** — Help the user decide which issues to fix first based on user impact
3. **Suggest alternatives** — Propose concrete design changes (spacing values, color adjustments, layout modifications)
4. **Compare with best practices** — Reference how top products (Stripe, Linear, Vercel, Figma) solve similar problems
5. **Deep-dive a category** — Provide more detail on any review category (visual hierarchy, color harmony, etc.)
6. **Re-evaluate after changes** — When the user says they've made changes, acknowledge and suggest re-running analysis

## Response Guidelines
- Be specific: reference exact elements from the analysis ("the submit button", "the header spacing")
- Be concise: 2-4 sentences for simple questions, structured lists for complex ones
- Be actionable: every suggestion should be implementable in Figma
- Reference the current score and issues when relevant
- If the user asks about something outside the analysis scope, be honest about limitations

## Structured Response
When the user requests a specific action (like "list all critical issues" or "prioritize fixes"), respond in JSON:
{
"action": "<action type>",
"items": [
{ "title": "<short title>", "description": "<detail>", "priority": "high|medium|low" }
]
}

For conversational questions, respond in plain text. Do not use JSON for casual conversation.`;
}
Loading
Loading