diff --git a/.prettierignore b/.prettierignore index 1a99321..8eafe70 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ dist/ node_modules/ package-lock.json +paper/eval/results/ diff --git a/knip.json b/knip.json index 3f7d474..a974341 100644 --- a/knip.json +++ b/knip.json @@ -1,3 +1,3 @@ { - "ignore": ["dist/**"] + "ignore": ["dist/**", "paper/eval/results/**"] } diff --git a/package.json b/package.json index ba0d4fa..c9ee2bf 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "lint:fix": "eslint --fix . && prettier --write .", "format": "prettier --write .", "format:check": "prettier --check .", + "eval:prompt-grid": "npm run build && tsx scripts/run-prompt-grid-eval.ts", + "paper:stats": "tsx scripts/paper-stats.ts", "sync": "tsx scripts/sync.ts", "sync:check": "tsx scripts/sync.ts && git diff --exit-code -- src/rules/index.ts README.md" }, diff --git a/paper/.gitignore b/paper/.gitignore new file mode 100644 index 0000000..f9458e7 --- /dev/null +++ b/paper/.gitignore @@ -0,0 +1,9 @@ +*.aux +*.bbl +*.blg +*.fdb_latexmk +*.fls +*.log +*.out +*.pdf +eval/results/ diff --git a/paper/Makefile b/paper/Makefile new file mode 100644 index 0000000..1873b11 --- /dev/null +++ b/paper/Makefile @@ -0,0 +1,9 @@ +PDF=main.pdf + +.PHONY: all clean + +all: + latexmk -pdf -interaction=nonstopmode main.tex + +clean: + latexmk -C main.tex diff --git a/paper/README.md b/paper/README.md new file mode 100644 index 0000000..bc8fa33 --- /dev/null +++ b/paper/README.md @@ -0,0 +1,100 @@ +# Laint Paper Draft + +This directory contains an initial arXiv-style paper draft for laint. + +## Current Shape + +The draft is intentionally framed as a research/tool paper, not a product announcement. The strongest publishable angle is: + +> Agent-oriented linting for generated JSX/TSX applications catches framework-specific web, mobile, and backend failures earlier than conventional build/type/runtime feedback. + +## Before Submission + +- Add real authors and affiliations. +- Decide whether this targets arXiv only, a workshop, or both. +- Run the prompt-to-code detector-quality evaluation described in `main.tex`. +- Replace the evaluation-plan section with measured results. +- Add citations to relevant program-repair and LLM-code-generation work. +- Build the PDF from `main.tex` and inspect it before submission. + +## Version Pinning + +This draft pins its rule counts and preliminary benchmark results to `main` commit +`6a60a0295955ee6cc1d639c88955ea50722e3516` from 2026-05-14. + +For future papers or follow-up benchmark runs, record: + +- The exact `main` commit or benchmark tag used for the laint rule corpus. +- The prompt suite version. +- The model IDs and provider versions used for generation. +- The run date and output directory. + +A future tag scheme such as `benchmark/agent-oriented-linting-2026-05` or +`paper/agent-oriented-linting-v1` would make these runs easier to cite without +depending on floating branch names. + +## Reproducing Paper Numbers + +Every numeric claim in the draft should either be calculated from repository +source or from a checked-in benchmark artifact. + +Rule corpus counts, severity counts, platform counts, and the category table are +calculated from `src/rules/*` metadata: + +```bash +npm run paper:stats +``` + +The preliminary prompt-grid numbers in `main.tex` are calculated from the +archived run artifact at `paper/eval/artifacts/initial-grid/results.json`: + +```bash +npm run paper:stats -- --eval paper/eval/artifacts/initial-grid/results.json +``` + +There is also a larger raw grid artifact at +`paper/eval/artifacts/full-grid-2026-05-17/results.json`: + +```bash +npm run paper:stats -- --eval paper/eval/artifacts/full-grid-2026-05-17/results.json +``` + +This raw run covers 6 prompts and 7 configured model aliases. Moonshot/Kimi failed +all 6 generations due provider authentication/network errors, so use this +artifact as raw evidence rather than final paper numbers until the Moonshot +credential path is fixed or the reported model grid is explicitly scoped to the +6 working model aliases. + +The generated app files under `paper/eval/results/` remain ignored because they +are working outputs. If a benchmark run contributes numbers to a paper, archive +the corresponding `results.json` under `paper/eval/artifacts//` or +attach it to a tagged release before citing the numbers. + +## Suggested Evaluation Data + +- A prompt suite covering web, mobile, and backend app-building tasks. +- Generated JSX/TSX outputs from one or more LLMs. +- Laint findings for each generated output. +- Human labels for whether each finding is a valid, invalid, or ambiguous violation. +- Missed-defect labels for recall, when an independently reviewed corpus is available. +- TypeScript, framework build, web preview, mobile simulator/device preview, and runtime outcomes. +- Repair iteration counts after lint feedback. + +## Prompt Grid + +Run a small prompt-to-code grid with Doppler-provided model keys: + +```bash +doppler run --project flux-worker --config dev -- npm run eval:prompt-grid +``` + +Useful options: + +```bash +npm run eval:prompt-grid -- --limit 2 +npm run eval:prompt-grid -- --models openai-gpt-5.5,anthropic-sonnet-4.6,google-3.1-pro +npm run eval:prompt-grid -- --out paper/eval/results/my-run +``` + +The runner writes raw generated files, `results.json`, `summary.md`, and `labels.todo.jsonl` +under `paper/eval/results/`. That directory is intentionally ignored by git. diff --git a/paper/eval/artifacts/full-grid-2026-05-17/results.json b/paper/eval/artifacts/full-grid-2026-05-17/results.json new file mode 100644 index 0000000..371ec80 --- /dev/null +++ b/paper/eval/artifacts/full-grid-2026-05-17/results.json @@ -0,0 +1,4662 @@ +{ + "summary": { + "generations": 42, + "totalFindings": 476, + "parseErrors": 2, + "generationErrors": 6, + "byRule": { + "no-inline-styles": 102, + "no-silent-skip": 87, + "no-type-assertion": 64, + "no-emoji-icons": 37, + "no-optional-props": 29, + "scrollview-horizontal-flexgrow": 25, + "prefer-named-params": 17, + "no-safeareaview": 14, + "browser-api-in-useeffect": 12, + "no-stylesheet-create": 12, + "textinput-keyboard-avoiding": 12, + "catch-must-log-to-sentry": 11, + "prefer-lucide-icons": 9, + "expo-image-import": 9, + "prefer-guard-clauses": 8, + "safe-json-parse": 6, + "no-tailwind-animation-classes": 5, + "no-magic-env-strings": 4, + "no-string-coerce-error": 3, + "no-manual-retry-loop": 3, + "logger-error-with-err": 3, + "no-complex-jsx-expressions": 1, + "no-react-query-missing": 1, + "url-params-must-encode": 1, + "no-nested-try-catch": 1 + }, + "byModel": { + "anthropic-sonnet-4.6": 127, + "anthropic-opus-4.6": 123, + "openai-gpt-5.5": 78, + "openai-gpt-5.4": 59, + "google-3.1-pro": 47, + "google-2.5-flash": 42, + "moonshot-kimi-k2.6": 0 + }, + "byPrompt": { + "taskflow-web": 125, + "chat-web": 92, + "insurance-reports-backend": 84, + "event-planner-mobile": 77, + "beauty-shop-mobile": 61, + "wallet-api-backend": 37 + } + }, + "records": [ + { + "prompt": { + "id": "taskflow-web", + "platform": "web", + "source": "refactor-bench", + "description": "React task management component with CRUD, search, filtering, modals, themes, and persistent UI preferences.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "openai-gpt-5.5", + "provider": "openai", + "model": "gpt-5.5" + }, + "outputPath": "paper/eval/results/full-grid-2026-05-17/generations/openai-gpt-5.5/taskflow-web/page.tsx", + "code": "\"use client\";\n\nimport { FormEvent, useEffect, useMemo, useState } from \"react\";\n\ntype Status = \"todo\" | \"doing\" | \"done\";\ntype Priority = \"low\" | \"medium\" | \"high\";\ntype Theme = \"light\" | \"dark\" | \"system\";\n\ntype Task = {\n id: string;\n title: string;\n notes: string;\n status: Status;\n priority: Priority;\n dueDate: string;\n createdAt: string;\n};\n\ntype Prefs = {\n theme: Theme;\n status: \"all\" | Status;\n priority: \"all\" | Priority;\n sort: \"newest\" | \"due\" | \"priority\";\n};\n\nconst TASKS_KEY = \"task-manager.tasks\";\nconst PREFS_KEY = \"task-manager.prefs\";\n\nconst defaultPrefs: Prefs = { theme: \"system\", status: \"all\", priority: \"all\", sort: \"newest\" };\nconst seedTasks: Task[] = [\n {\n id: \"seed-1\",\n title: \"Plan weekly priorities\",\n notes: \"Review backlog and pick the top three outcomes.\",\n status: \"todo\",\n priority: \"high\",\n dueDate: new Date().toISOString().slice(0, 10),\n createdAt: new Date().toISOString(),\n },\n];\n\nconst emptyTask = (): Task => ({\n id: \"\",\n title: \"\",\n notes: \"\",\n status: \"todo\",\n priority: \"medium\",\n dueDate: \"\",\n createdAt: \"\",\n});\n\nfunction uid() {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2);\n}\n\nfunction Modal({\n title,\n children,\n onClose,\n}: {\n title: string;\n children: React.ReactNode;\n onClose: () => void;\n}) {\n return (\n
\n
\n
\n

{title}

\n \n
\n {children}\n
\n
\n );\n}\n\nexport default function Page() {\n const [tasks, setTasks] = useState(seedTasks);\n const [prefs, setPrefs] = useState(defaultPrefs);\n const [query, setQuery] = useState(\"\");\n const [ready, setReady] = useState(false);\n const [editing, setEditing] = useState(null);\n const [deleting, setDeleting] = useState(null);\n\n useEffect(() => {\n try {\n const savedTasks = localStorage.getItem(TASKS_KEY);\n const savedPrefs = localStorage.getItem(PREFS_KEY);\n if (savedTasks) setTasks(JSON.parse(savedTasks));\n if (savedPrefs) setPrefs({ ...defaultPrefs, ...JSON.parse(savedPrefs) });\n } finally {\n setReady(true);\n }\n }, []);\n\n useEffect(() => {\n const apply = () => {\n const dark =\n prefs.theme === \"dark\" ||\n (prefs.theme === \"system\" && window.matchMedia(\"(prefers-color-scheme: dark)\").matches);\n document.documentElement.classList.toggle(\"dark\", dark);\n };\n apply();\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n media.addEventListener(\"change\", apply);\n return () => media.removeEventListener(\"change\", apply);\n }, [prefs.theme]);\n\n useEffect(() => {\n if (ready) localStorage.setItem(TASKS_KEY, JSON.stringify(tasks));\n }, [tasks, ready]);\n\n useEffect(() => {\n if (ready) localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));\n }, [prefs, ready]);\n\n const visibleTasks = useMemo(() => {\n const priorityWeight: Record = { high: 3, medium: 2, low: 1 };\n const q = query.trim().toLowerCase();\n return tasks\n .filter((task) => prefs.status === \"all\" || task.status === prefs.status)\n .filter((task) => prefs.priority === \"all\" || task.priority === prefs.priority)\n .filter((task) => !q || `${task.title} ${task.notes}`.toLowerCase().includes(q))\n .sort((a, b) => {\n if (prefs.sort === \"due\") return (a.dueDate || \"9999\").localeCompare(b.dueDate || \"9999\");\n if (prefs.sort === \"priority\") return priorityWeight[b.priority] - priorityWeight[a.priority];\n return b.createdAt.localeCompare(a.createdAt);\n });\n }, [tasks, prefs, query]);\n\n const counts = useMemo(\n () => ({\n all: tasks.length,\n todo: tasks.filter((t) => t.status === \"todo\").length,\n doing: tasks.filter((t) => t.status === \"doing\").length,\n done: tasks.filter((t) => t.status === \"done\").length,\n }),\n [tasks],\n );\n\n function saveTask(form: Task) {\n const clean = { ...form, title: form.title.trim(), notes: form.notes.trim() };\n if (!clean.title) return;\n setTasks((items) =>\n clean.id\n ? items.map((item) => (item.id === clean.id ? clean : item))\n : [{ ...clean, id: uid(), createdAt: new Date().toISOString() }, ...items],\n );\n setEditing(null);\n }\n\n function toggleDone(task: Task) {\n setTasks((items) =>\n items.map((item) =>\n item.id === task.id ? { ...item, status: item.status === \"done\" ? \"todo\" : \"done\" } : item,\n ),\n );\n }\n\n return (\n
\n
\n
\n
\n

Task Manager

\n

Focus on what moves next.

\n

\n {counts.done} completed · {counts.todo + counts.doing} active · preferences saved locally\n

\n
\n setEditing(emptyTask())}\n className=\"rounded-xl bg-indigo-600 px-4 py-2 font-medium text-white shadow-sm hover:bg-indigo-500\"\n >\n New task\n \n
\n\n
\n setQuery(e.target.value)}\n placeholder=\"Search tasks...\"\n className=\"rounded-xl border border-zinc-200 bg-transparent px-3 py-2 outline-none focus:border-indigo-500 dark:border-zinc-700\"\n />\n \n \n \n \n
\n\n
\n {visibleTasks.map((task) => (\n
\n
\n
\n
\n \n \n {task.priority}\n \n {task.dueDate && Due {task.dueDate}}\n
\n

\n {task.title}\n

\n {task.notes &&

{task.notes}

}\n
\n
\n \n \n
\n
\n
\n ))}\n {!visibleTasks.length && (\n
\n No tasks match your current search and filters.\n
\n )}\n
\n
\n\n {editing && setEditing(null)} onSave={saveTask} />}\n\n {deleting && (\n setDeleting(null)}>\n

\n This will permanently remove “{deleting.title}”.\n

\n
\n \n {\n setTasks((items) => items.filter((item) => item.id !== deleting.id));\n setDeleting(null);\n }}\n >\n Delete\n \n
\n
\n )}\n
\n );\n}\n\nfunction TaskForm({ task, onSave, onClose }: { task: Task; onSave: (task: Task) => void; onClose: () => void }) {\n const [draft, setDraft] = useState(task);\n\n function submit(e: FormEvent) {\n e.preventDefault();\n onSave(draft);\n }\n\n return (\n \n
\n setDraft((d) => ({ ...d, title: e.target.value }))}\n placeholder=\"Task title\"\n className=\"rounded-xl border border-zinc-200 bg-transparent px-3 py-2 outline-none focus:border-indigo-500 dark:border-zinc-700\"\n />\n setDraft((d) => ({ ...d, notes: e.target.value }))}\n placeholder=\"Notes\"\n rows={4}\n className=\"rounded-xl border border-zinc-200 bg-transparent px-3 py-2 outline-none focus:border-indigo-500 dark:border-zinc-700\"\n />\n
\n \n \n setDraft((d) => ({ ...d, dueDate: e.target.value }))} className=\"rounded-xl border border-zinc-200 bg-transparent px-3 py-2 dark:border-zinc-700\" />\n
\n
\n \n \n
\n \n
\n );\n}", + "lintResults": [ + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 72, + "column": 124, + "severity": "warning" + }, + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 222, + "column": 48, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 94, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 95, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 115, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 119, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 191, + "column": 89, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 197, + "column": 93, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 203, + "column": 85, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 208, + "column": 87, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 301, + "column": 89, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 306, + "column": 93, + "severity": "warning" + }, + { + "rule": "prefer-guard-clauses", + "message": "Invert this condition and return early instead of wrapping the entire function body in an if statement", + "line": 115, + "column": 4, + "severity": "warning" + }, + { + "rule": "prefer-guard-clauses", + "message": "Invert this condition and return early instead of wrapping the entire function body in an if statement", + "line": 119, + "column": 4, + "severity": "warning" + } + ], + "finishReason": "stop", + "parseError": null, + "generationError": null, + "usage": { + "prompt_tokens": 139, + "completion_tokens": 4209, + "total_tokens": 4348, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 512, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + } + }, + { + "prompt": { + "id": "taskflow-web", + "platform": "web", + "source": "refactor-bench", + "description": "React task management component with CRUD, search, filtering, modals, themes, and persistent UI preferences.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "openai-gpt-5.4", + "provider": "openai", + "model": "gpt-5.4-2026-03-05" + }, + "outputPath": "paper/eval/results/full-grid-2026-05-17/generations/openai-gpt-5.4/taskflow-web/page.tsx", + "code": "'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\n\ntype Status = 'all' | 'open' | 'done';\ntype Task = {\n id: string;\n title: string;\n notes: string;\n done: boolean;\n createdAt: number;\n};\n\nconst TASKS_KEY = 'tm.tasks';\nconst PREFS_KEY = 'tm.prefs';\n\nfunction uid() {\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction Modal({\n open,\n title,\n children,\n onClose,\n}: {\n open: boolean;\n title: string;\n children: React.ReactNode;\n onClose: () => void;\n}) {\n if (!open) return null;\n return (\n
\n e.stopPropagation()}\n >\n
\n

{title}

\n \n
\n {children}\n
\n \n );\n}\n\nexport default function Page() {\n const [ready, setReady] = useState(false);\n const [tasks, setTasks] = useState([]);\n const [query, setQuery] = useState('');\n const [status, setStatus] = useState('all');\n const [theme, setTheme] = useState<'light' | 'dark'>('light');\n const [compact, setCompact] = useState(false);\n const [modal, setModal] = useState<{ mode: 'create' | 'edit'; task?: Task } | null>(null);\n const [form, setForm] = useState({ title: '', notes: '' });\n\n useEffect(() => {\n const rawTasks = localStorage.getItem(TASKS_KEY);\n const rawPrefs = localStorage.getItem(PREFS_KEY);\n if (rawTasks) setTasks(JSON.parse(rawTasks));\n if (rawPrefs) {\n const prefs = JSON.parse(rawPrefs);\n setTheme(prefs.theme || 'light');\n setCompact(!!prefs.compact);\n setStatus(prefs.status || 'all');\n }\n setReady(true);\n }, []);\n\n useEffect(() => {\n if (!ready) return;\n localStorage.setItem(TASKS_KEY, JSON.stringify(tasks));\n }, [tasks, ready]);\n\n useEffect(() => {\n if (!ready) return;\n localStorage.setItem(PREFS_KEY, JSON.stringify({ theme, compact, status }));\n document.documentElement.classList.toggle('dark', theme === 'dark');\n }, [theme, compact, status, ready]);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n return tasks\n .filter((t) => (status === 'all' ? true : status === 'done' ? t.done : !t.done))\n .filter((t) => !q || `${t.title} ${t.notes}`.toLowerCase().includes(q))\n .sort((a, b) => Number(a.done) - Number(b.done) || b.createdAt - a.createdAt);\n }, [tasks, query, status]);\n\n const stats = useMemo(\n () => ({\n total: tasks.length,\n open: tasks.filter((t) => !t.done).length,\n done: tasks.filter((t) => t.done).length,\n }),\n [tasks]\n );\n\n const openCreate = () => {\n setForm({ title: '', notes: '' });\n setModal({ mode: 'create' });\n };\n\n const openEdit = (task: Task) => {\n setForm({ title: task.title, notes: task.notes });\n setModal({ mode: 'edit', task });\n };\n\n const saveTask = () => {\n const title = form.title.trim();\n if (!title) return;\n if (modal?.mode === 'edit' && modal.task) {\n setTasks((prev) => prev.map((t) => (t.id === modal.task!.id ? { ...t, title, notes: form.notes.trim() } : t)));\n } else {\n setTasks((prev) => [{ id: uid(), title, notes: form.notes.trim(), done: false, createdAt: Date.now() }, ...prev]);\n }\n setModal(null);\n };\n\n const toggleTask = (id: string) => setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));\n const removeTask = (id: string) => setTasks((prev) => prev.filter((t) => t.id !== id));\n const clearDone = () => setTasks((prev) => prev.filter((t) => !t.done));\n\n return (\n
\n
\n
\n
\n
\n

Task Manager

\n

CRUD, search, filters, theme, and persistent preferences.

\n
\n
\n \n \n
\n
\n\n
\n setQuery(e.target.value)}\n placeholder=\"Search tasks...\"\n className=\"rounded-lg border border-black/10 bg-transparent px-3 py-2 outline-none ring-0 placeholder:text-zinc-400 focus:border-zinc-400 dark:border-white/10\"\n />\n setStatus(e.target.value as Status)}\n className=\"rounded-lg border border-black/10 bg-transparent px-3 py-2 dark:border-white/10\"\n >\n \n \n \n \n \n \n
\n\n
\n Total: {stats.total}\n Open: {stats.open}\n Done: {stats.done}\n
\n
\n\n
\n {filtered.length === 0 ? (\n
\n No tasks found.\n
\n ) : (\n filtered.map((task) => (\n \n
\n toggleTask(task.id)} className=\"mt-1 h-4 w-4\" />\n
\n
{task.title}
\n {task.notes &&

{task.notes}

}\n
{new Date(task.createdAt).toLocaleString()}
\n
\n
\n \n \n
\n
\n
\n ))\n )}\n
\n \n\n setModal(null)}>\n
\n setForm((f) => ({ ...f, title: e.target.value }))}\n placeholder=\"Title\"\n className=\"w-full rounded-lg border border-black/10 bg-transparent px-3 py-2 dark:border-white/10\"\n />\n setForm((f) => ({ ...f, notes: e.target.value }))}\n placeholder=\"Notes\"\n rows={4}\n className=\"w-full rounded-lg border border-black/10 bg-transparent px-3 py-2 dark:border-white/10\"\n />\n
\n \n \n
\n
\n
\n
\n );\n}", + "lintResults": [ + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 41, + "column": 117, + "severity": "warning" + }, + { + "rule": "no-optional-props", + "message": "Avoid optional properties (?:). Use an explicit union with null instead (e.g. prop: string | null)", + "line": 58, + "column": 64, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 64, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 65, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 155, + "column": 41, + "severity": "warning" + }, + { + "rule": "safe-json-parse", + "message": "Wrap JSON.parse() in a try-catch block to handle malformed input.", + "line": 64, + "column": 27, + "severity": "warning" + }, + { + "rule": "safe-json-parse", + "message": "Wrap JSON.parse() in a try-catch block to handle malformed input.", + "line": 66, + "column": 20, + "severity": "warning" + } + ], + "finishReason": "stop", + "parseError": null, + "generationError": null, + "usage": { + "prompt_tokens": 139, + "completion_tokens": 2584, + "total_tokens": 2723, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + } + }, + { + "prompt": { + "id": "taskflow-web", + "platform": "web", + "source": "refactor-bench", + "description": "React task management component with CRUD, search, filtering, modals, themes, and persistent UI preferences.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "anthropic-sonnet-4.6", + "provider": "anthropic", + "model": "claude-sonnet-4-6" + }, + "outputPath": "paper/eval/results/full-grid-2026-05-17/generations/anthropic-sonnet-4.6/taskflow-web/page.tsx", + "code": "\"use client\";\n\nimport { useState, useEffect, useCallback } from \"react\";\n\ntype Priority = \"low\" | \"medium\" | \"high\";\ntype Status = \"todo\" | \"in-progress\" | \"done\";\ninterface Task { id: string; title: string; description: string; priority: Priority; status: Status; createdAt: number; }\n\nconst STORAGE_KEY = \"tasks_v1\";\nconst PREFS_KEY = \"ui_prefs_v1\";\nconst uid = () => Math.random().toString(36).slice(2);\nconst priorityColor: Record = { low: \"#22c55e\", medium: \"#f59e0b\", high: \"#ef4444\" };\n\nexport default function Page() {\n const [tasks, setTasks] = useState([]);\n const [search, setSearch] = useState(\"\");\n const [filterStatus, setFilterStatus] = useState(\"all\");\n const [filterPriority, setFilterPriority] = useState(\"all\");\n const [dark, setDark] = useState(false);\n const [modal, setModal] = useState<{ open: boolean; task?: Task }>({ open: false });\n const [form, setForm] = useState({ title: \"\", description: \"\", priority: \"medium\" as Priority, status: \"todo\" as Status });\n\n useEffect(() => {\n try { setTasks(JSON.parse(localStorage.getItem(STORAGE_KEY) || \"[]\")); } catch { setTasks([]); }\n try { const p = JSON.parse(localStorage.getItem(PREFS_KEY) || \"{}\"); setDark(!!p.dark); } catch {}\n }, []);\n\n useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)); }, [tasks]);\n useEffect(() => { localStorage.setItem(PREFS_KEY, JSON.stringify({ dark })); document.body.style.background = dark ? \"#111\" : \"#f8fafc\"; }, [dark]);\n\n const openCreate = () => { setForm({ title: \"\", description: \"\", priority: \"medium\", status: \"todo\" }); setModal({ open: true }); };\n const openEdit = (t: Task) => { setForm({ title: t.title, description: t.description, priority: t.priority, status: t.status }); setModal({ open: true, task: t }); };\n const closeModal = () => setModal({ open: false });\n\n const saveTask = useCallback(() => {\n if (!form.title.trim()) return;\n if (modal.task) {\n setTasks(ts => ts.map(t => t.id === modal.task!.id ? { ...t, ...form } : t));\n } else {\n setTasks(ts => [...ts, { id: uid(), ...form, createdAt: Date.now() }]);\n }\n closeModal();\n }, [form, modal.task]);\n\n const deleteTask = (id: string) => setTasks(ts => ts.filter(t => t.id !== id));\n\n const visible = tasks.filter(t =>\n (filterStatus === \"all\" || t.status === filterStatus) &&\n (filterPriority === \"all\" || t.priority === filterPriority) &&\n (t.title.toLowerCase().includes(search.toLowerCase()) || t.description.toLowerCase().includes(search.toLowerCase()))\n ).sort((a, b) => b.createdAt - a.createdAt);\n\n const bg = dark ? \"#1e1e2e\" : \"#fff\";\n const fg = dark ? \"#cdd6f4\" : \"#1e293b\";\n const border = dark ? \"#313244\" : \"#e2e8f0\";\n const inputStyle = { background: dark ? \"#313244\" : \"#f1f5f9\", color: fg, border: `1px solid ${border}`, borderRadius: 6, padding: \"6px 10px\", width: \"100%\", boxSizing: \"border-box\" as const };\n const btnPrimary = { background: \"#6366f1\", color: \"#fff\", border: \"none\", borderRadius: 6, padding: \"7px 16px\", cursor: \"pointer\" };\n const btnSecondary = { background: \"transparent\", color: fg, border: `1px solid ${border}`, borderRadius: 6, padding: \"7px 16px\", cursor: \"pointer\" };\n\n return (\n
\n
\n
\n

Task Manager

\n
\n \n \n
\n
\n\n
\n setSearch(e.target.value)} style={{ ...inputStyle, width: 220 }} />\n \n \n {visible.length} task{visible.length !== 1 ? \"s\" : \"\"}\n
\n\n {visible.length === 0 &&
No tasks found.
}\n\n
\n {visible.map(t => (\n
\n
\n
\n
\n {t.title}\n {t.status}\n {t.priority}\n
\n {t.description &&

{t.description}

}\n
\n
\n \n \n
\n
\n ))}\n
\n
\n\n {modal.open && (\n
\n
e.stopPropagation()}>\n

{modal.task ? \"Edit Task\" : \"New Task\"}

\n
\n setForm(f => ({ ...f, title: e.target.value }))} style={inputStyle} />\n \n
\n
\n \n Cancel\n \n \n {task ? 'Save Changes' : 'Create Task'}\n \n
\n \n
\n
\n );\n}", + "lintResults": [ + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 163, + "column": 33, + "severity": "warning" + }, + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 163, + "column": 45, + "severity": "warning" + }, + { + "rule": "no-optional-props", + "message": "Avoid optional properties (?:). Use an explicit union with null instead (e.g. prop: string | null)", + "line": 9, + "column": 2, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 28, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 32, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 80, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 88, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 248, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 31, + "column": 24, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 147, + "column": 45, + "severity": "warning" + }, + { + "rule": "safe-json-parse", + "message": "Wrap JSON.parse() in a try-catch block to handle malformed input.", + "line": 29, + "column": 15, + "severity": "warning" + } + ], + "finishReason": "stop", + "parseError": null, + "generationError": null, + "usage": { + "prompt_tokens": 145, + "completion_tokens": 7192, + "total_tokens": 7337, + "completion_tokens_details": { + "reasoning_tokens": 4208, + "audio_tokens": 0 + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + } + } + }, + { + "prompt": { + "id": "taskflow-web", + "platform": "web", + "source": "refactor-bench", + "description": "React task management component with CRUD, search, filtering, modals, themes, and persistent UI preferences.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "moonshot-kimi-k2.6", + "provider": "moonshot", + "model": "kimi-k2.6" + }, + "outputPath": null, + "code": null, + "lintResults": [], + "finishReason": null, + "parseError": null, + "generationError": "HTTP 401: {\"error\":{\"message\":\"Invalid response received from moonshot: {\\\"error\\\":{\\\"message\\\":\\\"Invalid Authentication\\\",\\\"type\\\":\\\"invalid_authentication_error\\\"}}\",\"type\":null,\"param\":null,\"code\":null},\"provider\":\"moonshot\"}", + "usage": null + }, + { + "prompt": { + "id": "chat-web", + "platform": "web", + "source": "refactor-bench", + "description": "Realtime chat application page with auth gate, message history, typing indicators, local draft persistence, and theme switching.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "openai-gpt-5.5", + "provider": "openai", + "model": "gpt-5.5" + }, + "outputPath": "paper/eval/results/full-grid-2026-05-17/generations/openai-gpt-5.5/chat-web/page.tsx", + "code": "\"use client\";\n\nimport { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport type { RealtimeChannel, User } from \"@supabase/supabase-js\";\nimport { createClient } from \"@/lib/supabase/client\";\n\ntype Theme = \"light\" | \"dark\";\ntype Message = {\n id: string;\n room_id: string;\n user_id: string;\n username: string | null;\n content: string;\n created_at: string;\n};\n\nconst ROOM_ID = \"general\";\nconst MESSAGE_LIMIT = 100;\n\nfunction displayName(user: User) {\n return (\n user.user_metadata?.full_name ||\n user.user_metadata?.name ||\n user.email?.split(\"@\")[0] ||\n \"Anonymous\"\n );\n}\n\nfunction timeLabel(value: string) {\n return new Intl.DateTimeFormat(undefined, { hour: \"numeric\", minute: \"2-digit\" }).format(\n new Date(value),\n );\n}\n\nexport default function Page() {\n const router = useRouter();\n const supabase = useMemo(() => createClient(), []);\n const [user, setUser] = useState(null);\n const [authLoading, setAuthLoading] = useState(true);\n const [messagesLoading, setMessagesLoading] = useState(false);\n const [messages, setMessages] = useState([]);\n const [draft, setDraft] = useState(\"\");\n const [sending, setSending] = useState(false);\n const [error, setError] = useState(null);\n const [typingUsers, setTypingUsers] = useState>({});\n const [theme, setTheme] = useState(\"light\");\n const bottomRef = useRef(null);\n const typingChannel = useRef(null);\n const typingTimers = useRef(new Map>());\n const stopTypingTimer = useRef | null>(null);\n const lastTypingAt = useRef(0);\n\n const draftKey = user ? `chat:${ROOM_ID}:draft:${user.id}` : \"\";\n const name = user ? displayName(user) : \"\";\n\n useEffect(() => {\n const saved = localStorage.getItem(\"theme\") as Theme | null;\n setTheme(saved ?? (matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\"));\n }, []);\n\n useEffect(() => {\n document.documentElement.classList.toggle(\"dark\", theme === \"dark\");\n localStorage.setItem(\"theme\", theme);\n }, [theme]);\n\n useEffect(() => {\n let mounted = true;\n\n supabase.auth.getUser().then(({ data }) => {\n if (!mounted) return;\n setUser(data.user);\n setAuthLoading(false);\n if (!data.user) router.replace(\"/login?next=/\");\n });\n\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setUser(session?.user ?? null);\n if (!session?.user) router.replace(\"/login?next=/\");\n });\n\n return () => {\n mounted = false;\n subscription.unsubscribe();\n };\n }, [router, supabase]);\n\n useEffect(() => {\n if (!user || !draftKey) return;\n setDraft(localStorage.getItem(draftKey) ?? \"\");\n }, [draftKey, user]);\n\n useEffect(() => {\n if (!draftKey) return;\n if (draft.trim()) localStorage.setItem(draftKey, draft);\n else localStorage.removeItem(draftKey);\n }, [draft, draftKey]);\n\n useEffect(() => {\n if (!user) return;\n let active = true;\n setMessagesLoading(true);\n setError(null);\n\n supabase\n .from(\"messages\")\n .select(\"id, room_id, user_id, username, content, created_at\")\n .eq(\"room_id\", ROOM_ID)\n .order(\"created_at\", { ascending: true })\n .limit(MESSAGE_LIMIT)\n .then(({ data, error }) => {\n if (!active) return;\n if (error) setError(error.message);\n else setMessages((data ?? []) as Message[]);\n setMessagesLoading(false);\n });\n\n const messageChannel = supabase\n .channel(`messages:${ROOM_ID}`)\n .on(\n \"postgres_changes\",\n { event: \"INSERT\", schema: \"public\", table: \"messages\", filter: `room_id=eq.${ROOM_ID}` },\n (payload) => {\n const next = payload.new as Message;\n setMessages((current) =>\n current.some((message) => message.id === next.id) ? current : [...current, next],\n );\n },\n )\n .subscribe();\n\n const channel = supabase\n .channel(`typing:${ROOM_ID}`, { config: { broadcast: { self: false } } })\n .on(\"broadcast\", { event: \"typing\" }, ({ payload }) => {\n const userId = String(payload?.userId ?? \"\");\n const username = String(payload?.name ?? \"Someone\");\n if (!userId || userId === user.id) return;\n\n typingTimers.current.get(userId)?.clear?.();\n clearTimeout(typingTimers.current.get(userId));\n\n if (payload?.typing) {\n setTypingUsers((current) => ({ ...current, [userId]: username }));\n const timer = setTimeout(() => {\n setTypingUsers((current) => {\n const copy = { ...current };\n delete copy[userId];\n return copy;\n });\n typingTimers.current.delete(userId);\n }, 3500);\n typingTimers.current.set(userId, timer);\n } else {\n setTypingUsers((current) => {\n const copy = { ...current };\n delete copy[userId];\n return copy;\n });\n }\n })\n .subscribe();\n\n typingChannel.current = channel;\n\n return () => {\n active = false;\n supabase.removeChannel(messageChannel);\n supabase.removeChannel(channel);\n typingChannel.current = null;\n typingTimers.current.forEach(clearTimeout);\n typingTimers.current.clear();\n };\n }, [supabase, user]);\n\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"end\" });\n }, [messages, typingUsers]);\n\n const broadcastTyping = useCallback(\n (typing: boolean) => {\n if (!typingChannel.current || !user) return;\n typingChannel.current.send({\n type: \"broadcast\",\n event: \"typing\",\n payload: { userId: user.id, name, typing },\n });\n },\n [name, user],\n );\n\n const onDraftChange = (value: string) => {\n setDraft(value);\n const now = Date.now();\n\n if (value.trim() && now - lastTypingAt.current > 1200) {\n lastTypingAt.current = now;\n broadcastTyping(true);\n }\n\n if (stopTypingTimer.current) clearTimeout(stopTypingTimer.current);\n stopTypingTimer.current = setTimeout(() => broadcastTyping(false), 1600);\n };\n\n const sendMessage = async (event: FormEvent) => {\n event.preventDefault();\n const content = draft.trim();\n if (!content || !user || sending) return;\n\n setSending(true);\n setError(null);\n broadcastTyping(false);\n\n const { data, error } = await supabase\n .from(\"messages\")\n .insert({ room_id: ROOM_ID, user_id: user.id, username: name, content })\n .select(\"id, room_id, user_id, username, content, created_at\")\n .single();\n\n setSending(false);\n\n if (error) {\n setError(error.message);\n return;\n }\n\n setDraft(\"\");\n if (draftKey) localStorage.removeItem(draftKey);\n if (data) {\n setMessages((current) =>\n current.some((message) => message.id === data.id) ? current : [...current, data as Message],\n );\n }\n };\n\n const signOut = async () => {\n await supabase.auth.signOut();\n router.replace(\"/login?next=/\");\n };\n\n const typers = Object.values(typingUsers);\n\n if (authLoading || !user) {\n return (\n
\n
\n

{authLoading ? \"Checking session…\" : \"Redirecting…\"}

\n {!authLoading && (\n \n Sign in to continue\n \n )}\n
\n
\n );\n }\n\n return (\n
\n
\n
\n
\n

Team Chat

\n

Signed in as {name}

\n
\n
\n setTheme((current) => (current === \"dark\" ? \"light\" : \"dark\"))}\n type=\"button\"\n >\n {theme === \"dark\" ? \"Light\" : \"Dark\"}\n \n \n Sign out\n \n
\n
\n\n
\n {messagesLoading ? (\n

Loading messages…

\n ) : messages.length === 0 ? (\n

No messages yet. Start the conversation.

\n ) : (\n
\n {messages.map((message) => {\n const mine = message.user_id === user.id;\n return (\n
\n \n
\n {mine ? \"You\" : message.username || \"Anonymous\"}\n {timeLabel(message.created_at)}\n
\n

{message.content}

\n
\n \n );\n })}\n
\n )}\n {typers.length > 0 && (\n

\n {typers.slice(0, 2).join(\", \")}\n {typers.length > 2 ? ` and ${typers.length - 2} more` : \"\"} typing…\n

\n )}\n
\n
\n\n {error && (\n
\n {error}\n
\n )}\n\n
\n
\n broadcastTyping(false)}\n onChange={(event) => onDraftChange(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" && !event.shiftKey) sendMessage(event);\n }}\n placeholder=\"Write a message…\"\n rows={1}\n value={draft}\n />\n \n {sending ? \"Sending…\" : \"Send\"}\n \n
\n
\n
\n
\n );\n}", + "lintResults": [ + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 230, + "column": 18, + "severity": "warning" + }, + { + "rule": "no-complex-jsx-expressions", + "message": "Avoid complex expressions in JSX. Extract to a variable for better readability.", + "line": 287, + "column": 10, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 75, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 82, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 198, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 203, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 230, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 231, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 337, + "column": 16, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 59, + "column": 18, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 117, + "column": 25, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 127, + "column": 23, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 233, + "column": 83, + "severity": "warning" + }, + { + "rule": "prefer-guard-clauses", + "message": "Invert this condition and return early instead of wrapping the entire function body in an if statement", + "line": 337, + "column": 16, + "severity": "warning" + } + ], + "finishReason": "stop", + "parseError": null, + "generationError": null, + "usage": { + "prompt_tokens": 141, + "completion_tokens": 4388, + "total_tokens": 4529, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 1375, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + } + }, + { + "prompt": { + "id": "chat-web", + "platform": "web", + "source": "refactor-bench", + "description": "Realtime chat application page with auth gate, message history, typing indicators, local draft persistence, and theme switching.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "openai-gpt-5.4", + "provider": "openai", + "model": "gpt-5.4-2026-03-05" + }, + "outputPath": "paper/eval/results/full-grid-2026-05-17/generations/openai-gpt-5.4/chat-web/page.tsx", + "code": "'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport Link from 'next/link';\n\ntype User = { id: string; name: string; email: string };\ntype Message = { id: string; userId: string; userName: string; text: string; createdAt: string };\ntype TypingState = Record;\n\nconst DRAFT_KEY = 'chat:draft';\nconst THEME_KEY = 'chat:theme';\n\nfunction uid() {\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction timeLabel(iso: string) {\n return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n}\n\nexport default function Page() {\n const [user, setUser] = useState(null);\n const [checkingAuth, setCheckingAuth] = useState(true);\n const [messages, setMessages] = useState([]);\n const [typing, setTyping] = useState({});\n const [text, setText] = useState('');\n const [theme, setTheme] = useState<'light' | 'dark'>('light');\n const listRef = useRef(null);\n const socketRef = useRef(null);\n const typingTimer = useRef(null);\n\n useEffect(() => {\n const savedTheme = (localStorage.getItem(THEME_KEY) as 'light' | 'dark' | null) || 'light';\n setTheme(savedTheme);\n document.documentElement.classList.toggle('dark', savedTheme === 'dark');\n setText(localStorage.getItem(DRAFT_KEY) || '');\n\n let cancelled = false;\n (async () => {\n try {\n const res = await fetch('/api/auth/me', { credentials: 'include' });\n if (!res.ok) throw new Error('unauthorized');\n const me = (await res.json()) as User;\n if (!cancelled) setUser(me);\n } catch {\n if (!cancelled) setUser(null);\n } finally {\n if (!cancelled) setCheckingAuth(false);\n }\n })();\n return () => {\n cancelled = true;\n socketRef.current?.close();\n if (typingTimer.current) window.clearTimeout(typingTimer.current);\n };\n }, []);\n\n useEffect(() => {\n if (!user) return;\n let closed = false;\n\n (async () => {\n const res = await fetch('/api/chat/messages', { cache: 'no-store' });\n if (res.ok && !closed) setMessages(await res.json());\n })();\n\n const protocol = location.protocol === 'https:' ? 'wss' : 'ws';\n const ws = new WebSocket(`${protocol}://${location.host}/api/chat/ws`);\n socketRef.current = ws;\n\n ws.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n if (msg.type === 'message') setMessages((prev) => [...prev, msg.payload]);\n if (msg.type === 'typing') {\n setTyping((prev) => {\n const next = { ...prev };\n if (msg.payload.isTyping) next[msg.payload.userId] = msg.payload.userName;\n else delete next[msg.payload.userId];\n return next;\n });\n }\n };\n\n return () => {\n closed = true;\n ws.close();\n };\n }, [user]);\n\n useEffect(() => {\n localStorage.setItem(DRAFT_KEY, text);\n }, [text]);\n\n useEffect(() => {\n document.documentElement.classList.toggle('dark', theme === 'dark');\n localStorage.setItem(THEME_KEY, theme);\n }, [theme]);\n\n useEffect(() => {\n listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth' });\n }, [messages, typing]);\n\n const typingNames = useMemo(\n () => Object.entries(typing).filter(([id]) => id !== user?.id).map(([, name]) => name),\n [typing, user?.id]\n );\n\n const sendTyping = (isTyping: boolean) => {\n if (socketRef.current?.readyState !== WebSocket.OPEN || !user) return;\n socketRef.current.send(JSON.stringify({ type: 'typing', payload: { userId: user.id, userName: user.name, isTyping } }));\n };\n\n const onChange = (value: string) => {\n setText(value);\n sendTyping(value.trim().length > 0);\n if (typingTimer.current) window.clearTimeout(typingTimer.current);\n typingTimer.current = window.setTimeout(() => sendTyping(false), 1200);\n };\n\n const sendMessage = async () => {\n const body = text.trim();\n if (!body || !user) return;\n const optimistic: Message = {\n id: uid(),\n userId: user.id,\n userName: user.name,\n text: body,\n createdAt: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, optimistic]);\n setText('');\n localStorage.removeItem(DRAFT_KEY);\n sendTyping(false);\n\n try {\n const res = await fetch('/api/chat/messages', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text: body }),\n });\n if (!res.ok) throw new Error('send failed');\n } catch {\n setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));\n setText(body);\n }\n };\n\n if (checkingAuth) {\n return
Loading…
;\n }\n\n if (!user) {\n return (\n
\n
\n

Sign in to chat

\n

Your conversation history and presence are available after authentication.

\n
\n Login\n Home\n
\n
\n
\n );\n }\n\n return (\n
\n
\n
\n
\n

Realtime Chat

\n

Signed in as {user.name}

\n
\n setTheme((t) => (t === 'light' ? 'dark' : 'light'))}\n className=\"rounded-lg border border-neutral-300 px-3 py-2 text-sm dark:border-neutral-700\"\n >\n {theme === 'light' ? 'Dark' : 'Light'} mode\n \n
\n\n
\n
\n {messages.length === 0 &&

No messages yet. Start the conversation.

}\n {messages.map((m) => {\n const mine = m.userId === user.id;\n return (\n
\n
\n
\n {mine ? 'You' : m.userName}\n \n {timeLabel(m.createdAt)}\n
\n

{m.text}

\n
\n
\n );\n })}\n
\n\n
\n {typingNames.length > 0 && `${typingNames.join(', ')} ${typingNames.length > 1 ? 'are' : 'is'} typing…`}\n
\n\n
\n
\n onChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n void sendMessage();\n }\n }}\n placeholder=\"Write a message…\"\n className=\"min-h-[52px] flex-1 resize-none rounded-xl border border-neutral-300 bg-transparent px-4 py-3 outline-none ring-0 placeholder:text-neutral-400 focus:border-blue-500 dark:border-neutral-700\"\n />\n void sendMessage()}\n disabled={!text.trim()}\n className=\"rounded-xl bg-blue-600 px-5 py-3 font-medium text-white disabled:cursor-not-allowed disabled:opacity-50\"\n >\n Send\n \n
\n

Press Enter to send, Shift+Enter for a new line. Drafts are saved locally.

\n
\n
\n
\n
\n );\n}", + "lintResults": [ + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'window' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 116, + "column": 29, + "severity": "warning" + }, + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'window' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 117, + "column": 26, + "severity": "warning" + }, + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 132, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-react-query-missing", + "message": "Use @tanstack/react-query for data fetching instead of fetch() in useEffect. It provides caching, loading states, and error handling.", + "line": 41, + "column": 26, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 44, + "column": 8, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 46, + "column": 8, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 48, + "column": 8, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 54, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 64, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 73, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 74, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 116, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 213, + "column": 18, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 33, + "column": 24, + "severity": "warning" + }, + { + "rule": "no-type-assertion", + "message": "Avoid type assertions with \"as\". Use type narrowing, type guards, or proper typing instead", + "line": 43, + "column": 19, + "severity": "warning" + }, + { + "rule": "prefer-guard-clauses", + "message": "Invert this condition and return early instead of wrapping the entire function body in an if statement", + "line": 213, + "column": 18, + "severity": "warning" + }, + { + "rule": "safe-json-parse", + "message": "Wrap JSON.parse() in a try-catch block to handle malformed input.", + "line": 72, + "column": 18, + "severity": "warning" + } + ], + "finishReason": "stop", + "parseError": null, + "generationError": null, + "usage": { + "prompt_tokens": 141, + "completion_tokens": 2357, + "total_tokens": 2498, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + } + }, + { + "prompt": { + "id": "chat-web", + "platform": "web", + "source": "refactor-bench", + "description": "Realtime chat application page with auth gate, message history, typing indicators, local draft persistence, and theme switching.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "anthropic-sonnet-4.6", + "provider": "anthropic", + "model": "claude-sonnet-4-6" + }, + "outputPath": "paper/eval/results/full-grid-2026-05-17/generations/anthropic-sonnet-4.6/chat-web/page.tsx", + "code": "\"use client\";\n\nimport { useState, useEffect, useRef, useCallback } from \"react\";\n\ntype Message = { id: string; user: string; text: string; ts: number };\ntype Theme = \"light\" | \"dark\";\n\nconst STORAGE_DRAFT = \"chat_draft\";\nconst STORAGE_USER = \"chat_user\";\nconst STORAGE_MSGS = \"chat_messages\";\n\nfunction uid() {\n return Math.random().toString(36).slice(2, 10);\n}\n\nfunction formatTime(ts: number) {\n return new Date(ts).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" });\n}\n\nexport default function ChatPage() {\n const [theme, setTheme] = useState(\"light\");\n const [username, setUsername] = useState(\"\");\n const [nameInput, setNameInput] = useState(\"\");\n const [messages, setMessages] = useState([]);\n const [draft, setDraft] = useState(\"\");\n const [typingUsers, setTypingUsers] = useState([]);\n const bottomRef = useRef(null);\n const typingTimer = useRef | null>(null);\n const isTypingRef = useRef(false);\n\n useEffect(() => {\n const saved = localStorage.getItem(STORAGE_USER);\n if (saved) setUsername(saved);\n const savedMsgs = localStorage.getItem(STORAGE_MSGS);\n if (savedMsgs) setMessages(JSON.parse(savedMsgs));\n const savedDraft = localStorage.getItem(STORAGE_DRAFT);\n if (savedDraft) setDraft(savedDraft);\n }, []);\n\n useEffect(() => {\n document.documentElement.setAttribute(\"data-theme\", theme);\n }, [theme]);\n\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [messages, typingUsers]);\n\n useEffect(() => {\n if (username) localStorage.setItem(STORAGE_DRAFT, draft);\n }, [draft, username]);\n\n const simulateIncoming = useCallback((sentText: string) => {\n const bot = \"Bot\";\n setTypingUsers((p) => [...p, bot]);\n setTimeout(() => {\n setTypingUsers((p) => p.filter((u) => u !== bot));\n const reply: Message = {\n id: uid(),\n user: bot,\n text: `Echo: ${sentText}`,\n ts: Date.now(),\n };\n setMessages((prev) => {\n const next = [...prev, reply];\n localStorage.setItem(STORAGE_MSGS, JSON.stringify(next));\n return next;\n });\n }, 1400);\n }, []);\n\n const sendMessage = useCallback(() => {\n const text = draft.trim();\n if (!text) return;\n const msg: Message = { id: uid(), user: username, text, ts: Date.now() };\n setMessages((prev) => {\n const next = [...prev, msg];\n localStorage.setItem(STORAGE_MSGS, JSON.stringify(next));\n return next;\n });\n setDraft(\"\");\n localStorage.removeItem(STORAGE_DRAFT);\n simulateIncoming(text);\n }, [draft, username, simulateIncoming]);\n\n const handleDraftChange = (val: string) => {\n setDraft(val);\n if (!isTypingRef.current) {\n isTypingRef.current = true;\n setTypingUsers((p) => (p.includes(username) ? p : [...p, username]));\n }\n if (typingTimer.current) clearTimeout(typingTimer.current);\n typingTimer.current = setTimeout(() => {\n isTypingRef.current = false;\n setTypingUsers((p) => p.filter((u) => u !== username));\n }, 1200);\n };\n\n const login = () => {\n const name = nameInput.trim();\n if (!name) return;\n setUsername(name);\n localStorage.setItem(STORAGE_USER, name);\n };\n\n const logout = () => {\n setUsername(\"\");\n localStorage.removeItem(STORAGE_USER);\n };\n\n const bg = theme === \"dark\" ? \"#1a1a2e\" : \"#f0f4f8\";\n const surface = theme === \"dark\" ? \"#16213e\" : \"#ffffff\";\n const text = theme === \"dark\" ? \"#e0e0e0\" : \"#1a1a2e\";\n const accent = \"#4f8ef7\";\n const bubbleSelf = accent;\n const bubbleOther = theme === \"dark\" ? \"#2a2a4a\" : \"#e2e8f0\";\n\n if (!username) {\n return (\n
\n
\n

Join Chat

\n setNameInput(e.target.value)}\n onKeyDown={(e) => e.key === \"Enter\" && login()}\n style={{ width: \"100%\", padding: \"10px 14px\", borderRadius: 8, border: \"1px solid #ccc\", fontSize: 15, boxSizing: \"border-box\", marginBottom: 14 }}\n />\n \n
\n
\n );\n }\n\n return (\n
\n
\n 💬 Realtime Chat\n
\n Hi, {username}\n \n \n
\n
\n\n
\n {messages.length === 0 &&

No messages yet. Say hello!

}\n {messages.map((m) => {\n const self = m.user === username;\n return (\n
\n {m.user} · {formatTime(m.ts)}\n
\n {m.text}\n
\n
\n );\n })}\n {typingUsers.filter((u) => u !== username).length > 0 && (\n
\n {typingUsers.filter((u) => u !== username).join(\", \")} {typingUsers.filter((u) => u !== username).length === 1 ? \"is\" : \"are\"} typing…\n
\n )}\n
\n
\n\n
\n handleDraftChange(e.target.value)}\n onKeyDown={(e) => e.key === \"Enter\" && !e.shiftKey && (e.preventDefault(), sendMessage())}\n placeholder=\"Type a message…\"\n style={{ flex: 1, padding: \"10px 14px\", borderRadius: 24, border: \"1px solid #ccc\", fontSize: 14, outline: \"none\", background: bg, color: text }}\n />\n \n
\n
\n );\n}", + "lintResults": [ + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 65, + "column": 8, + "severity": "warning" + }, + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 77, + "column": 6, + "severity": "warning" + }, + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 81, + "column": 4, + "severity": "warning" + }, + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 102, + "column": 4, + "severity": "warning" + }, + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 107, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 141, + "column": 56, + "severity": "warning" + }, + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 145, + "column": 32, + "severity": "warning" + }, + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 145, + "column": 45, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 119, + "column": 11, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 120, + "column": 13, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 121, + "column": 14, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 128, + "column": 12, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 130, + "column": 34, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 139, + "column": 9, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 140, + "column": 14, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 141, + "column": 14, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 142, + "column": 13, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 143, + "column": 16, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 144, + "column": 85, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 147, + "column": 35, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 153, + "column": 11, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 154, + "column": 37, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 158, + "column": 28, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 159, + "column": 20, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 160, + "column": 19, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 167, + "column": 15, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 174, + "column": 11, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 180, + "column": 10, + "severity": "warning" + }, + { + "rule": "no-inline-styles", + "message": "Avoid inline styles. Use Tailwind CSS classes instead.", + "line": 182, + "column": 63, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 33, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 35, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 37, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 49, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 87, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 91, + "column": 4, + "severity": "warning" + }, + { + "rule": "prefer-guard-clauses", + "message": "Invert this condition and return early instead of wrapping the entire function body in an if statement", + "line": 49, + "column": 4, + "severity": "warning" + }, + { + "rule": "safe-json-parse", + "message": "Wrap JSON.parse() in a try-catch block to handle malformed input.", + "line": 35, + "column": 31, + "severity": "warning" + } + ], + "finishReason": "stop", + "parseError": null, + "generationError": null, + "usage": { + "prompt_tokens": 158, + "completion_tokens": 2632, + "total_tokens": 2790 + } + }, + { + "prompt": { + "id": "chat-web", + "platform": "web", + "source": "refactor-bench", + "description": "Realtime chat application page with auth gate, message history, typing indicators, local draft persistence, and theme switching.", + "outputFile": "app/page.tsx" + }, + "model": { + "alias": "anthropic-opus-4.6", + "provider": "anthropic", + "model": "claude-opus-4-6" + }, + "outputPath": "paper/eval/results/full-grid-2026-05-17/generations/anthropic-opus-4.6/chat-web/page.tsx", + "code": "\"use client\";\n\nimport { useState, useEffect, useRef, useCallback, FormEvent } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { useAuth } from \"@/lib/auth\";\nimport { supabase } from \"@/lib/supabase\";\nimport { useTheme } from \"@/lib/theme\";\nimport type { Message, TypingEvent } from \"@/types/chat\";\n\nconst DRAFT_KEY = \"chat_draft\";\nconst TYPING_TIMEOUT = 2000;\n\nexport default function ChatPage() {\n const { user, loading: authLoading, signOut } = useAuth();\n const { theme, toggleTheme } = useTheme();\n const router = useRouter();\n\n const [messages, setMessages] = useState([]);\n const [draft, setDraft] = useState(\"\");\n const [typingUsers, setTypingUsers] = useState>(new Map());\n const [sending, setSending] = useState(false);\n\n const bottomRef = useRef(null);\n const typingTimerRef = useRef(null);\n const channelRef = useRef | null>(null);\n\n useEffect(() => {\n const saved = localStorage.getItem(DRAFT_KEY);\n if (saved) setDraft(saved);\n }, []);\n\n useEffect(() => {\n localStorage.setItem(DRAFT_KEY, draft);\n }, [draft]);\n\n useEffect(() => {\n if (!user) return;\n\n const fetchHistory = async () => {\n const { data } = await supabase\n .from(\"messages\")\n .select(\"*\")\n .order(\"created_at\", { ascending: true })\n .limit(200);\n if (data) setMessages(data as Message[]);\n };\n\n fetchHistory();\n\n const channel = supabase\n .channel(\"chat-room\")\n .on(\"postgres_changes\", { event: \"INSERT\", schema: \"public\", table: \"messages\" }, (payload) => {\n setMessages((prev) => [...prev, payload.new as Message]);\n })\n .on(\"broadcast\", { event: \"typing\" }, (payload) => {\n const evt = payload.payload as TypingEvent;\n if (evt.user_id === user.id) return;\n setTypingUsers((prev) => {\n const next = new Map(prev);\n const existing = next.get(evt.user_id);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n setTypingUsers((p) => {\n const n = new Map(p);\n n.delete(evt.user_id);\n return n;\n });\n }, TYPING_TIMEOUT);\n next.set(evt.user_id, timer);\n return next;\n });\n })\n .subscribe();\n\n channelRef.current = channel;\n\n return () => {\n supabase.removeChannel(channel);\n };\n }, [user]);\n\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [messages, typingUsers]);\n\n const broadcastTyping = useCallback(() => {\n if (!channelRef.current || !user) return;\n channelRef.current.send({\n type: \"broadcast\",\n event: \"typing\",\n payload: { user_id: user.id, display_name: user.user_metadata?.display_name ?? user.email },\n });\n }, [user]);\n\n const handleInputChange = (value: string) => {\n setDraft(value);\n if (typingTimerRef.current) clearTimeout(typingTimerRef.current);\n broadcastTyping();\n typingTimerRef.current = setTimeout(() => {}, TYPING_TIMEOUT);\n };\n\n const handleSend = async (e: FormEvent) => {\n e.preventDefault();\n const text = draft.trim();\n if (!text || !user) return;\n setSending(true);\n setDraft(\"\");\n localStorage.removeItem(DRAFT_KEY);\n\n await supabase.from(\"messages\").insert({\n content: text,\n user_id: user.id,\n display_name: user.user_metadata?.display_name ?? user.email,\n });\n\n setSending(false);\n };\n\n if (authLoading) {\n return (\n
\n

Loading…

\n
\n );\n }\n\n if (!user) {\n return (\n
\n

Welcome to Chat

\n

Sign in to start chatting

\n router.push(\"/login\")}\n className=\"rounded-lg bg-primary px-6 py-2 text-primary-foreground hover:opacity-90 transition\"\n >\n Sign In\n \n
\n );\n }\n\n const typingNames = Array.from(typingUsers.keys());\n\n return (\n
\n
\n

Chat

\n
\n \n \n {user.user_metadata?.display_name ?? user.email}\n \n \n
\n
\n\n
\n {messages.map((msg) => {\n const isOwn = msg.user_id === user.id;\n return (\n
\n \n {!isOwn &&

{msg.display_name}

}\n

{msg.content}

\n

\n {new Date(msg.created_at).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}\n

\n
\n
\n );\n })}\n {typingNames.length > 0 && (\n

\n {typingNames.length === 1 ? \"Someone is typing…\" : `${typingNames.length} people are typing…`}\n

\n )}\n
\n \n\n
\n handleInputChange(e.target.value)}\n placeholder=\"Type a message…\"\n className=\"flex-1 rounded-lg border bg-transparent px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary\"\n autoFocus\n />\n \n Send\n \n \n
\n );\n}", + "lintResults": [ + { + "rule": "browser-api-in-useeffect", + "message": "Access to 'localStorage' should be inside useEffect() or behind a typeof check for SSR compatibility", + "line": 108, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 150, + "column": 32, + "severity": "warning" + }, + { + "rule": "no-emoji-icons", + "message": "Use icons from 'lucide-react' or 'lucide-react-native' instead of emoji characters", + "line": 150, + "column": 39, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 29, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 45, + "column": 6, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 61, + "column": 10, + "severity": "warning" + }, + { + "rule": "no-silent-skip", + "message": "This if statement has no else branch. Add an else with logging to avoid silently skipping the falsy case", + "line": 97, + "column": 4, + "severity": "warning" + }, + { + "rule": "no-tailwind-animation-classes", + "message": "Avoid Tailwind animation classes (animate-*). Use