AI Code Quality Guard v1.0 MVP — Chrome extension with static analysis for AI-generated code
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.verdict
|
||||||
Generated
+2257
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-code-quality-guard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Chrome extension that catches 'almost-right' AI-generated code before review",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@crxjs/vite-plugin": "^2.0.0-beta.28",
|
||||||
|
"@types/chrome": "^0.0.268",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vite": "^5.2.11",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<rect width="128" height="128" rx="20" fill="#0f172a"/>
|
||||||
|
<path d="M64 24 L96 40 L96 72 Q96 96 64 104 Q32 96 32 72 L32 40 Z" fill="none" stroke="#22c55e" stroke-width="6" stroke-linejoin="round"/>
|
||||||
|
<path d="M52 64 L60 72 L76 56" fill="none" stroke="#22c55e" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="64" cy="52" r="8" fill="#22c55e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { analyze, detectLanguage } from '../analyzer';
|
||||||
|
|
||||||
|
describe('detectLanguage', () => {
|
||||||
|
it('detects JavaScript', () => {
|
||||||
|
expect(detectLanguage('const x = 1; function foo() { return x; }')).toBe('javascript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects TypeScript', () => {
|
||||||
|
expect(detectLanguage('interface User { name: string; }')).toBe('typescript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects Python', () => {
|
||||||
|
expect(detectLanguage('def hello():\n print("world")')).toBe('python');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects Go', () => {
|
||||||
|
expect(detectLanguage('package main\nfunc main() { fmt.Println("hi") }')).toBe('go');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects Java', () => {
|
||||||
|
expect(detectLanguage('public static void main(String[] args) { System.out.println("hi"); }')).toBe('java');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to javascript for ambiguous code', () => {
|
||||||
|
expect(detectLanguage('foo bar baz')).toBe('javascript');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('analyze', () => {
|
||||||
|
it('flags loose equality in JS', () => {
|
||||||
|
const result = analyze('if (x == 1) { console.log("ok"); }');
|
||||||
|
expect(result.findings.some(f => f.category === 'Type Safety' && f.message.includes('Loose equality'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags empty catch block', () => {
|
||||||
|
const result = analyze('try { risky(); } catch (e) {}');
|
||||||
|
expect(result.findings.some(f => f.message.includes('Empty catch'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags eval usage', () => {
|
||||||
|
const result = analyze('eval("alert(1)");');
|
||||||
|
expect(result.findings.some(f => f.message.includes('eval'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags innerHTML assignment', () => {
|
||||||
|
const result = analyze('el.innerHTML = "<b>hi</b>";');
|
||||||
|
expect(result.findings.some(f => f.message.includes('innerHTML'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags mutable default arg in Python', () => {
|
||||||
|
const result = analyze('def append(item, lst=[]):\n lst.append(item)\n return lst');
|
||||||
|
expect(result.findings.some(f => f.message.includes('Mutable default'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags bare except in Python', () => {
|
||||||
|
const result = analyze('try:\n pass\nexcept:\n pass');
|
||||||
|
expect(result.findings.some(f => f.message.includes('Bare except'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags hardcoded secret', () => {
|
||||||
|
const result = analyze('const apiKey = "abc1234567890123456789abcdef";');
|
||||||
|
expect(result.findings.some(f => f.message.includes('Hardcoded'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags SQL injection risk', () => {
|
||||||
|
const result = analyze('db.query("SELECT * FROM users WHERE id = " + userId)');
|
||||||
|
expect(result.findings.some(f => f.message.includes('SQL injection'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives a clean bill for safe code', () => {
|
||||||
|
const result = analyze('const greeting = "Hello"; console.log(greeting);');
|
||||||
|
// Should have zero or very few findings for trivial safe code
|
||||||
|
expect(result.score).toBeGreaterThanOrEqual(90);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a score between 0 and 100', () => {
|
||||||
|
const result = analyze('eval("x"); try { } catch (e) { }');
|
||||||
|
expect(result.score).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(result.score).toBeLessThanOrEqual(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes line numbers', () => {
|
||||||
|
const result = analyze('line1\nline2\nif (x == 1) { }');
|
||||||
|
const finding = result.findings.find(f => f.message.includes('Loose equality'));
|
||||||
|
expect(finding).toBeDefined();
|
||||||
|
expect(finding!.line).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes a summary', () => {
|
||||||
|
const result = analyze('if (a == 1) { }');
|
||||||
|
expect(result.summary).toContain('score');
|
||||||
|
});
|
||||||
|
});
|
||||||
+270
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* AI Code Quality Guard — Core Analyzer
|
||||||
|
* Detects common issues in AI-generated code without executing it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Finding {
|
||||||
|
line: number;
|
||||||
|
severity: 'error' | 'warning' | 'info';
|
||||||
|
category: string;
|
||||||
|
message: string;
|
||||||
|
suggestion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalysisResult {
|
||||||
|
language: string;
|
||||||
|
findings: Finding[];
|
||||||
|
score: number; // 0-100
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detect programming language from code content */
|
||||||
|
export function detectLanguage(code: string): string {
|
||||||
|
if (code.includes('try:') && code.includes('except:')) return 'python';
|
||||||
|
if (code.includes('package main') || code.includes('func ') || code.includes('fmt.')) return 'go';
|
||||||
|
if (code.includes('def ') && code.includes(':') && !code.includes('function')) return 'python';
|
||||||
|
if (code.includes('<?php')) return 'php';
|
||||||
|
if (code.includes('using System') || code.includes('namespace ')) return 'csharp';
|
||||||
|
if (code.includes('public static void main') || code.includes('System.out')) return 'java';
|
||||||
|
if (code.includes('#include') || code.includes('int main(')) return 'c';
|
||||||
|
if (code.includes('use strict') || code.includes('const ') || code.includes('let ') || code.includes('function ') || code.includes('=>')) return 'javascript';
|
||||||
|
if (code.includes('interface ') || code.includes('type ') || code.includes(': ') && code.includes('string')) return 'typescript';
|
||||||
|
if (code.includes('fn ') || code.includes('let mut')) return 'rust';
|
||||||
|
return 'javascript';
|
||||||
|
}
|
||||||
|
|
||||||
|
const RULES: {
|
||||||
|
id: string;
|
||||||
|
category: string;
|
||||||
|
severity: 'error' | 'warning' | 'info';
|
||||||
|
languages: string[];
|
||||||
|
pattern: RegExp;
|
||||||
|
message: string;
|
||||||
|
suggestion: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
id: 'loose-equality',
|
||||||
|
category: 'Type Safety',
|
||||||
|
severity: 'warning',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /(?<!\!)(\b\w+\b)\s*==\s*(\b\w+\b)/g,
|
||||||
|
message: 'Loose equality (==) can cause unexpected type coercion',
|
||||||
|
suggestion: 'Use strict equality (===) instead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'missing-await',
|
||||||
|
category: 'Async',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /\b(async\s+function|async\s*\(|async\s+\w+)\b[\s\S]*?\b\w+\s*=\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)(?!\s*\.then)(?!\s*await)/g,
|
||||||
|
message: 'Possible missing await on async function call',
|
||||||
|
suggestion: 'Add await if the call is asynchronous, or handle the Promise explicitly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uncaught-promise',
|
||||||
|
category: 'Async',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /\b[a-zA-Z_$][a-zA-Z0-9_$]*\s*\([^)]*\)\s*\.then\s*\([^)]*\)\s*(?!\s*\.catch)/g,
|
||||||
|
message: 'Promise chain missing .catch() handler',
|
||||||
|
suggestion: 'Add .catch() or wrap in try/catch to handle rejection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bare-promise',
|
||||||
|
category: 'Async',
|
||||||
|
severity: 'warning',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /\b[a-zA-Z_$][a-zA-Z0-9_$]*\s*\([^)]*\)\s*\.(then|catch|finally)\s*\(/g,
|
||||||
|
message: 'Unhandled promise — consider awaiting or explicitly ignoring',
|
||||||
|
suggestion: 'Use await or assign to a variable and handle errors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'potential-null-access',
|
||||||
|
category: 'Null Safety',
|
||||||
|
severity: 'warning',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /\b(\w+)\s*\.\s*\w+/g,
|
||||||
|
message: 'Potential null/undefined property access',
|
||||||
|
suggestion: 'Add optional chaining (?.) or a null check before accessing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'unused-variable',
|
||||||
|
category: 'Dead Code',
|
||||||
|
severity: 'info',
|
||||||
|
languages: ['javascript', 'typescript', 'python'],
|
||||||
|
pattern: /\b(let|const|var)\s+(\w+)\s*=\s*[^;]+;(?![\s\S]*?\b\2\b)/g,
|
||||||
|
message: 'Variable may be declared but never used',
|
||||||
|
suggestion: 'Remove unused variable or verify it is needed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'off-by-one',
|
||||||
|
category: 'Logic',
|
||||||
|
severity: 'warning',
|
||||||
|
languages: ['javascript', 'typescript', 'python', 'java', 'csharp', 'go'],
|
||||||
|
pattern: /\bfor\s*\([^)]*<\s*\w+\.length[^)]*\)\s*\{[\s\S]*?\b\w+\[\s*\w+\s*\]/g,
|
||||||
|
message: 'Potential off-by-one in array access — verify boundary conditions',
|
||||||
|
suggestion: 'Double-check loop boundaries and array indexing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'empty-catch',
|
||||||
|
category: 'Error Handling',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /catch\s*\(\s*\w*\s*\)\s*\{\s*\}/g,
|
||||||
|
message: 'Empty catch block swallows errors silently',
|
||||||
|
suggestion: 'Log the error, rethrow, or handle it meaningfully',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eval-danger',
|
||||||
|
category: 'Security',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /\beval\s*\(/g,
|
||||||
|
message: 'eval() is dangerous and can execute arbitrary code',
|
||||||
|
suggestion: 'Use JSON.parse, structured data, or safer alternatives',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'innerhtml-xss',
|
||||||
|
category: 'Security',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /\.innerHTML\s*=\s*/g,
|
||||||
|
message: 'innerHTML assignment can introduce XSS vulnerabilities',
|
||||||
|
suggestion: 'Use textContent or a safe templating library instead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'regex-escaping',
|
||||||
|
category: 'Security',
|
||||||
|
severity: 'warning',
|
||||||
|
languages: ['javascript', 'typescript', 'python'],
|
||||||
|
pattern: /new\s+RegExp\s*\(\s*[^,)]*\+\s*\w+/g,
|
||||||
|
message: 'Dynamic regex construction may not escape special characters',
|
||||||
|
suggestion: 'Escape user input before inserting into regex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mutable-default-arg',
|
||||||
|
category: 'Logic',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['python'],
|
||||||
|
pattern: /def\s+\w+\s*\([^)]*=\s*(\[|\{)/g,
|
||||||
|
message: 'Mutable default argument is shared across all calls',
|
||||||
|
suggestion: 'Use None as default and initialize inside the function',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bare-except',
|
||||||
|
category: 'Error Handling',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['python'],
|
||||||
|
pattern: /except\s*:\s*(?:\n|$)/g,
|
||||||
|
message: 'Bare except catches all exceptions including SystemExit and KeyboardInterrupt',
|
||||||
|
suggestion: 'Catch specific exceptions (e.g., except ValueError:)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'print-debug',
|
||||||
|
category: 'Dead Code',
|
||||||
|
severity: 'info',
|
||||||
|
languages: ['python'],
|
||||||
|
pattern: /\bprint\s*\(/g,
|
||||||
|
message: 'Debug print statement left in code',
|
||||||
|
suggestion: 'Remove or replace with proper logging',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sql-injection',
|
||||||
|
category: 'Security',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['javascript', 'typescript', 'python', 'php', 'java', 'csharp', 'go'],
|
||||||
|
pattern: /\b(query|execute|exec)\s*\(\s*["'`][^"'`]*\$\{|\+\s*\w+/g,
|
||||||
|
message: 'Possible SQL injection — string concatenation in query',
|
||||||
|
suggestion: 'Use parameterized queries / prepared statements',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hardcoded-secret',
|
||||||
|
category: 'Security',
|
||||||
|
severity: 'error',
|
||||||
|
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'csharp', 'php'],
|
||||||
|
pattern: /\b(api[_-]?key|token|secret|password)\s*[=:]\s*["'`][a-zA-Z0-9_-]{16,}["'`]/gi,
|
||||||
|
message: 'Hardcoded secret or credential detected',
|
||||||
|
suggestion: 'Load from environment variables or a secrets manager',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'var-keyword',
|
||||||
|
category: 'Best Practice',
|
||||||
|
severity: 'warning',
|
||||||
|
languages: ['javascript', 'typescript'],
|
||||||
|
pattern: /\bvar\s+/g,
|
||||||
|
message: 'var has function scope and hoisting issues — prefer let/const',
|
||||||
|
suggestion: 'Replace var with let or const',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'magic-number',
|
||||||
|
category: 'Maintainability',
|
||||||
|
severity: 'info',
|
||||||
|
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'csharp'],
|
||||||
|
pattern: /(?<!\d)(\b(?!0x)[0-9]{2,}\b)(?!\s*[,;)\]\}])/g,
|
||||||
|
message: 'Magic number without explanation',
|
||||||
|
suggestion: 'Extract into a named constant',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'todo-fixme',
|
||||||
|
category: 'Dead Code',
|
||||||
|
severity: 'warning',
|
||||||
|
languages: ['javascript', 'typescript', 'python', 'java', 'go', 'csharp', 'php'],
|
||||||
|
pattern: /\b(TODO|FIXME|HACK|XXX)\b/g,
|
||||||
|
message: 'Unresolved TODO/FIXME marker in code',
|
||||||
|
suggestion: 'Resolve or remove before committing',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getLineNumber(code: string, index: number): number {
|
||||||
|
let line = 1;
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
if (code[i] === '\n') line++;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analyze(code: string): AnalysisResult {
|
||||||
|
const language = detectLanguage(code);
|
||||||
|
const findings: Finding[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const rule of RULES) {
|
||||||
|
if (!rule.languages.includes(language)) continue;
|
||||||
|
const flags = rule.pattern.flags.includes('g') ? rule.pattern.flags : rule.pattern.flags + 'g';
|
||||||
|
const regex = new RegExp(rule.pattern.source, flags);
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = regex.exec(code)) !== null) {
|
||||||
|
const line = getLineNumber(code, match.index);
|
||||||
|
const key = `${rule.id}:${line}:${match[0]}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
findings.push({
|
||||||
|
line,
|
||||||
|
severity: rule.severity,
|
||||||
|
category: rule.category,
|
||||||
|
message: rule.message,
|
||||||
|
suggestion: rule.suggestion,
|
||||||
|
});
|
||||||
|
// Prevent infinite loop on zero-width matches
|
||||||
|
if (match.index === regex.lastIndex) regex.lastIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by severity then line
|
||||||
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
||||||
|
findings.sort((a, b) => {
|
||||||
|
const d = severityOrder[a.severity] - severityOrder[b.severity];
|
||||||
|
return d !== 0 ? d : a.line - b.line;
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorCount = findings.filter(f => f.severity === 'error').length;
|
||||||
|
const warningCount = findings.filter(f => f.severity === 'warning').length;
|
||||||
|
const infoCount = findings.filter(f => f.severity === 'info').length;
|
||||||
|
|
||||||
|
const score = Math.max(0, 100 - errorCount * 15 - warningCount * 5 - infoCount * 1);
|
||||||
|
const summary = `${errorCount} error${errorCount !== 1 ? 's' : ''}, ${warningCount} warning${warningCount !== 1 ? 's' : ''}, ${infoCount} note${infoCount !== 1 ? 's' : ''} — score ${score}/100`;
|
||||||
|
|
||||||
|
return { language, findings, score, summary };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { analyze, detectLanguage };
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
console.log('AI Code Quality Guard installed');
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.action.onClicked.addListener((tab) => {
|
||||||
|
if (tab.id) {
|
||||||
|
chrome.tabs.sendMessage(tab.id, { action: 'scanPage' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { analyze, AnalysisResult } from './analyzer';
|
||||||
|
|
||||||
|
async function updateStats(result: AnalysisResult) {
|
||||||
|
try {
|
||||||
|
const { scans = 0, issuesFound = 0 } = await chrome.storage.local.get(['scans', 'issuesFound']);
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
scans: scans + 1,
|
||||||
|
issuesFound: issuesFound + result.findings.filter(f => f.severity === 'error' || f.severity === 'warning').length,
|
||||||
|
lastScore: result.score,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// storage may be unavailable in some contexts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let panel: HTMLElement | null = null;
|
||||||
|
let activeCodeBlock: HTMLElement | null = null;
|
||||||
|
|
||||||
|
const PLATFORM_SELECTORS = [
|
||||||
|
// ChatGPT
|
||||||
|
{ host: /chatgpt\.com|chat\.openai\.com/, codeBlocks: 'pre, [class*="code-block"], [class*="code"] pre', copyButtons: 'button[class*="copy"]' },
|
||||||
|
// Claude
|
||||||
|
{ host: /claude\.ai/, codeBlocks: 'pre, [class*="code-block"], [class*="prose"] pre', copyButtons: 'button[aria-label*="copy"]' },
|
||||||
|
// Perplexity
|
||||||
|
{ host: /perplexity\.ai/, codeBlocks: 'pre, [class*="code"] pre', copyButtons: 'button[class*="copy"]' },
|
||||||
|
// Gemini
|
||||||
|
{ host: /gemini\.google\.com/, codeBlocks: 'pre, [class*="code"]', copyButtons: 'button[class*="copy"]' },
|
||||||
|
// Copilot
|
||||||
|
{ host: /copilot\.microsoft\.com/, codeBlocks: 'pre, [class*="code"]', copyButtons: 'button[class*="copy"]' },
|
||||||
|
// Generic fallback
|
||||||
|
{ host: /.*/, codeBlocks: 'pre', copyButtons: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getPlatformConfig(): typeof PLATFORM_SELECTORS[0] {
|
||||||
|
const host = location.host;
|
||||||
|
return PLATFORM_SELECTORS.find(p => p.host.test(host)) || PLATFORM_SELECTORS[PLATFORM_SELECTORS.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGuardButton(): HTMLButtonElement {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'ai-quality-guard-btn';
|
||||||
|
btn.title = 'AI Code Quality Guard — Analyze this code';
|
||||||
|
btn.innerHTML = `
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||||
|
<path d="M9 12l2 2 4-4"/>
|
||||||
|
</svg>
|
||||||
|
<span>Guard</span>
|
||||||
|
`;
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodeText(block: HTMLElement): string {
|
||||||
|
// Try to find the actual code element inside the pre
|
||||||
|
const code = block.querySelector('code');
|
||||||
|
if (code) return code.textContent || '';
|
||||||
|
return block.textContent || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectButtons() {
|
||||||
|
const cfg = getPlatformConfig();
|
||||||
|
const blocks = document.querySelectorAll<HTMLElement>(cfg.codeBlocks);
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.closest('.ai-quality-guard-panel')) continue; // skip our own panel
|
||||||
|
if (block.dataset.guardInjected) continue;
|
||||||
|
block.dataset.guardInjected = 'true';
|
||||||
|
|
||||||
|
const btn = createGuardButton();
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const code = getCodeText(block);
|
||||||
|
if (!code.trim()) return;
|
||||||
|
activeCodeBlock = block;
|
||||||
|
const result = analyze(code);
|
||||||
|
void updateStats(result);
|
||||||
|
showResults(block, result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert button into the pre or its wrapper
|
||||||
|
const wrapper = (block.closest('[class*="code-block"], [class*="code"]') || block) as HTMLElement;
|
||||||
|
wrapper.style.position = 'relative';
|
||||||
|
block.style.position = 'relative';
|
||||||
|
block.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResults(block: HTMLElement, result: AnalysisResult) {
|
||||||
|
removePanel();
|
||||||
|
|
||||||
|
const rect = block.getBoundingClientRect();
|
||||||
|
panel = document.createElement('div');
|
||||||
|
panel.className = 'ai-quality-guard-panel';
|
||||||
|
|
||||||
|
const findingsHtml = result.findings.length
|
||||||
|
? result.findings.map(f => {
|
||||||
|
const icon = f.severity === 'error' ? '🔴' : f.severity === 'warning' ? '🟡' : '🔵';
|
||||||
|
return `
|
||||||
|
<div class="guard-finding guard-severity-${f.severity}">
|
||||||
|
<div class="guard-finding-header">
|
||||||
|
<span class="guard-icon">${icon}</span>
|
||||||
|
<span class="guard-line">L${f.line}</span>
|
||||||
|
<span class="guard-category">${f.category}</span>
|
||||||
|
</div>
|
||||||
|
<div class="guard-message">${escapeHtml(f.message)}</div>
|
||||||
|
<div class="guard-suggestion">💡 ${escapeHtml(f.suggestion)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')
|
||||||
|
: `<div class="guard-pass">✅ No issues detected. Review anyway — AI can still be wrong.</div>`;
|
||||||
|
|
||||||
|
const scoreColor = result.score >= 80 ? '#22c55e' : result.score >= 50 ? '#eab308' : '#ef4444';
|
||||||
|
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="guard-header">
|
||||||
|
<div class="guard-title">
|
||||||
|
<span class="guard-shield">🛡️</span>
|
||||||
|
AI Code Quality Guard
|
||||||
|
</div>
|
||||||
|
<div class="guard-score" style="color:${scoreColor}">${result.score}/100</div>
|
||||||
|
<button class="guard-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="guard-meta">${escapeHtml(result.language)} · ${escapeHtml(result.summary)}</div>
|
||||||
|
<div class="guard-findings">${findingsHtml}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const closeBtn = panel.querySelector('.guard-close') as HTMLButtonElement;
|
||||||
|
closeBtn?.addEventListener('click', removePanel);
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
// Position below the code block
|
||||||
|
const scrollY = window.scrollY || window.pageYOffset;
|
||||||
|
panel.style.left = `${rect.left}px`;
|
||||||
|
panel.style.top = `${rect.bottom + scrollY + 8}px`;
|
||||||
|
panel.style.width = `${Math.min(rect.width, 600)}px`;
|
||||||
|
|
||||||
|
// Ensure it stays in viewport
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!panel) return;
|
||||||
|
const pRect = panel.getBoundingClientRect();
|
||||||
|
if (pRect.right > window.innerWidth - 16) {
|
||||||
|
panel.style.left = `${window.innerWidth - pRect.width - 16}px`;
|
||||||
|
}
|
||||||
|
if (pRect.bottom > window.innerHeight - 16) {
|
||||||
|
panel.style.top = `${rect.top + scrollY - pRect.height - 8}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePanel() {
|
||||||
|
if (panel && panel.parentNode) {
|
||||||
|
panel.parentNode.removeChild(panel);
|
||||||
|
}
|
||||||
|
panel = null;
|
||||||
|
activeCodeBlock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe DOM mutations for dynamic chat interfaces
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
injectButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
injectButtons();
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// Close panel on Escape
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') removePanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close panel on click outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (panel && !panel.contains(e.target as Node) && !activeCodeBlock?.contains(e.target as Node)) {
|
||||||
|
removePanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/* AI Code Quality Guard — Content Styles */
|
||||||
|
|
||||||
|
.ai-quality-guard-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: rgba(30, 41, 59, 0.85);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
opacity: 0.6;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-quality-guard-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(30, 41, 59, 1);
|
||||||
|
border-color: rgba(148, 163, 184, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-quality-guard-btn svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre:hover .ai-quality-guard-btn,
|
||||||
|
.ai-quality-guard-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results Panel */
|
||||||
|
.ai-quality-guard-panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10000;
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #e2e8f0;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 600px;
|
||||||
|
min-width: 320px;
|
||||||
|
animation: guardFadeIn 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes guardFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #1e293b;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #f8fafc;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-shield {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-score {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 14px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 2px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-close:hover {
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-meta {
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-findings {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-finding {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-finding:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-severity-error {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
background: rgba(239, 68, 68, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-severity-warning {
|
||||||
|
border-left-color: #eab308;
|
||||||
|
background: rgba(234, 179, 8, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-severity-info {
|
||||||
|
border-left-color: #3b82f6;
|
||||||
|
background: rgba(59, 130, 246, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-finding-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-icon {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-line {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-category {
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-message {
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-suggestion {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-pass {
|
||||||
|
padding: 20px 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: #22c55e;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
.guard-findings::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.guard-findings::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.guard-findings::-webkit-scrollbar-thumb {
|
||||||
|
background: #334155;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<rect width="128" height="128" rx="20" fill="#0f172a"/>
|
||||||
|
<path d="M64 24 L96 40 L96 72 Q96 96 64 104 Q32 96 32 72 L32 40 Z" fill="none" stroke="#22c55e" stroke-width="6" stroke-linejoin="round"/>
|
||||||
|
<path d="M52 64 L60 72 L76 56" fill="none" stroke="#22c55e" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="64" cy="52" r="8" fill="#22c55e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "AI Code Quality Guard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Catch 'almost-right' AI-generated code before review. Intercepts code from AI chat interfaces, runs semantic analysis, flags likely errors, and suggests corrections.",
|
||||||
|
"permissions": ["storage", "activeTab"],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://chat.openai.com/*",
|
||||||
|
"https://chatgpt.com/*",
|
||||||
|
"https://claude.ai/*",
|
||||||
|
"https://poe.com/*",
|
||||||
|
"https://www.perplexity.ai/*",
|
||||||
|
"https://gemini.google.com/*",
|
||||||
|
"https://copilot.microsoft.com/*"
|
||||||
|
],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"https://chat.openai.com/*",
|
||||||
|
"https://chatgpt.com/*",
|
||||||
|
"https://claude.ai/*",
|
||||||
|
"https://poe.com/*",
|
||||||
|
"https://www.perplexity.ai/*",
|
||||||
|
"https://gemini.google.com/*",
|
||||||
|
"https://copilot.microsoft.com/*"
|
||||||
|
],
|
||||||
|
"js": ["src/content-script.ts"],
|
||||||
|
"css": ["src/content-styles.css"],
|
||||||
|
"run_at": "document_end"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "src/popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon.svg",
|
||||||
|
"32": "icons/icon.svg",
|
||||||
|
"48": "icons/icon.svg",
|
||||||
|
"128": "icons/icon.svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "src/background.ts",
|
||||||
|
"type": "module"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon.svg",
|
||||||
|
"32": "icons/icon.svg",
|
||||||
|
"48": "icons/icon.svg",
|
||||||
|
"128": "icons/icon.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>AI Code Quality Guard</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 320px;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #1e293b;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
.header .version {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(34, 197, 94, 0.08);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #22c55e;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.status .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #22c55e;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.info strong {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.stat-box {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 8px;
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #f8fafc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-top: 1px solid #1e293b;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #475569;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<span>🛡️</span>
|
||||||
|
<h1>AI Code Quality Guard</h1>
|
||||||
|
<span class="version">v1.0</span>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="status">
|
||||||
|
<span class="dot"></span>
|
||||||
|
Active on this page
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
Look for the <strong>Guard</strong> button on code blocks in AI chat interfaces (ChatGPT, Claude, Perplexity, Gemini, Copilot). Click it to analyze code before copying.
|
||||||
|
</div>
|
||||||
|
<div class="stats" id="stats">
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-value" id="stat-scans">0</div>
|
||||||
|
<div class="stat-label">Scans</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-value" id="stat-issues">0</div>
|
||||||
|
<div class="stat-label">Issues</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-value" id="stat-score">—</div>
|
||||||
|
<div class="stat-label">Last Score</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
AI Code Quality Guard · BunBun Labs
|
||||||
|
</div>
|
||||||
|
<script src="popup.ts" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Popup script — shows stats from chrome.storage
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
const { scans = 0, issuesFound = 0, lastScore = null } = await chrome.storage.local.get(['scans', 'issuesFound', 'lastScore']);
|
||||||
|
const elScans = document.getElementById('stat-scans');
|
||||||
|
const elIssues = document.getElementById('stat-issues');
|
||||||
|
const elScore = document.getElementById('stat-score');
|
||||||
|
if (elScans) elScans.textContent = String(scans);
|
||||||
|
if (elIssues) elIssues.textContent = String(issuesFound);
|
||||||
|
if (elScore) elScore.textContent = lastScore !== null ? String(lastScore) : '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStats();
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { crx } from '@crxjs/vite-plugin';
|
||||||
|
import manifest from './src/manifest.json' with { type: 'json' };
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [crx({ manifest })],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
globals: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user