Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/9xj89gzrtw-hue_nilov-catering.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
113 KiB
JSON

{"repo": "9xj89gzrtw-hue/nilov-catering", "files": {".github/workflows/external-critics.yml": {"f52724b440c7508d3c1c25124ded2e9aaef449d3": {"t": 1785746339.149135, "n": 27446, "h": "c6d330bc63f1c60e", "c": "name: External LLM Critics Evaluation\n\non:\n workflow_dispatch:\n inputs:\n site_url:\n description: 'Live site URL to screenshot and evaluate'\n required: true\n type: string\n default: 'https://odaeda.ru/'\n iteration:\n description: 'Iteration number'\n required: false\n type: string\n default: '1'\n\njobs:\n evaluate:\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: '22'\n\n - name: Install Playwright\n run: |\n npm install playwright\n npx playwright install chromium --with-deps\n\n - name: Take full-page screenshot\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n console.log('Navigating to:', siteUrl);\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n // Wait for animations/preloader to finish\n await page.waitForTimeout(4000);\n // Scroll to load lazy content\n await page.evaluate(async () => {\n const delay = ms => new Promise(r => setTimeout(r, ms));\n const height = document.body.scrollHeight;\n const step = window.innerHeight;\n for (let y = 0; y < height; y += step) {\n window.scrollTo(0, y);\n await delay(500);\n }\n window.scrollTo(0, 0);\n await delay(1000);\n });\n await page.screenshot({\n path: 'screenshot_full.png',\n fullPage: true\n });\n console.log('Screenshot saved: screenshot_full.png');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Also take 3 viewport screenshots (hero, middle, bottom)\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n await page.waitForTimeout(4000);\n // Hero\n await page.screenshot({ path: 'screenshot_hero.png' });\n // Middle\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.4));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_middle.png' });\n // Bottom\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.75));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_bottom.png' });\n console.log('Viewport screenshots saved');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Setup Python\n uses: actions/setup-python@v5\n with:\n python-version: '3.12'\n\n - name: Checkout submodules workaround\n run: |\n git submodule deinit -f . 2>/dev/null || true\n rm -rf .git/modules 2>/dev/null || true\n git config --unset core.submodulesfile 2>/dev/null || true\n rm -f .gitmodules 2>/dev/null || true\n\n - name: Run External LLM Critics\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}\n OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\n SAMBANOVA_API_KEY: ${{ secrets.SAMBANOVA_API_KEY }}\n GOOGLE_AI_KEY: ${{ secrets.GOOGLE_AI_KEY }}\n DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}\n SITE_URL: ${{ inputs.site_url }}\n ITERATION: ${{ inputs.iteration }}\n run: |\n python3 << 'PYEOF'\n import json, urllib.request, urllib.error, base64, sys, os, time, concurrent.futures\n from pathlib import Path\n\n SITE_URL = os.environ.get(\"SITE_URL\", \"https://odaeda.ru/\")\n ITERATION = os.environ.get(\"ITERATION\", \"1\")\n\n # Read and base64-encode the FULL page screenshot\n def img_to_data_uri(path):\n with open(path, \"rb\") as f:\n return f\"data:image/png;base64,{base64.b64encode(f.read()).decode()}\"\n\n img_full = img_to_data_uri(\"screenshot_full.png\")\n img_hero = img_to_data_uri(\"screenshot_hero.png\")\n print(f\"Full screenshot: {len(img_full)} chars base64\")\n\n # ========== CRITIC PROMPTS ==========\n \n VISUAL_CRITIC_PROMPT = \"\"\"You are an Awwwards-level Visual Design Judge. You have judged 1000+ premium websites.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Hierarchy** (1-10): Is there clear typographic and visual hierarchy? Do headings, body text, and accents have proper contrast in size/weight/color?\n 2. **Color Palette & Harmony** (1-10): Is the color scheme cohesive, luxurious, and appropriate for premium catering? Are there any jarring colors?\n 3. **Typography** (1-10): Are fonts well-chosen for luxury branding? Is spacing, line-height, and font-pairing professional?\n 4. **Imagery & Visual Assets** (1-10): Are food/venue images high quality? Properly sized and positioned? Or are they placeholder/stock-looking?\n 5. **Spacing & Layout Rhythm** (1-10): Is whitespace used effectively? Is there consistent padding/margin rhythm?\n 6. **Micro-interactions & Polish** (1-10): Based on visual design, does the site look polished with extreme attention to detail?\n 7. **Overall Visual Impression** (1-10): First-impression wow factor. Would this site be nominated for an Awwwards SOTD?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_hierarchy\": 0.0, \"color_palette\": 0.0, \"typography\": 0.0, \"imagery\": 0.0, \"spacing_layout\": 0.0, \"micro_interactions\": 0.0, \"overall_visual\": 0.0, \"avg_visual\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for truly exceptional Awwwards-quality design. Most professional sites score 6-8.\"\"\"\n\n UX_CRITIC_PROMPT = \"\"\"You are a Senior UX/CRO Expert from Baymard Institute. You have conducted 500+ usability audits for luxury brands.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Navigation Clarity** (1-10): Is the main navigation intuitive? Can users find key sections (services, menu, pricing, contact) easily?\n 2. **Content Scannability** (1-10): Is text easy to scan? Are headings descriptive? Is content structured logically with clear sections?\n 3. **Call-to-Action Effectiveness** (1-10): Are CTAs prominent, clear, and action-oriented? Is there a clear conversion path from landing to contact?\n 4. **Trust Signals** (1-10): Are testimonials, stats, years of experience, guarantees visible? Do they build credibility for a premium service?\n 5. **Mobile-first Design** (1-10): Based on the desktop view, does the layout structure suggest good mobile adaptation?\n 6. **Information Architecture** (1-10): Is the page flow logical? Hero -> Social proof -> Services -> Process -> Menu -> Testimonials -> Gallery -> FAQ -> CTA?\n 7. **Overall UX Score** (1-10): Would a potential client easily understand the value proposition and know how to take action?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"navigation\": 0.0, \"scannability\": 0.0, \"cta_effectiveness\": 0.0, \"trust_signals\": 0.0, \"mobile_first\": 0.0, \"info_architecture\": 0.0, \"overall_ux\": 0.0, \"avg_ux\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely world-class UX. Most professional sites score 6-8.\"\"\"\n\n FRONTEND_CRITIC_PROMPT = \"\"\"You are a Principal Frontend Engineer & Web Performance Expert at a top agency. You review production websites daily.\n \n I will provide you with multiple screenshots of a luxury catering website built with Next.js 16, Tailwind CSS 4, Framer Motion.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Consistency** (1-10): Are all elements consistently styled? No layout shifts, broken alignment, or inconsistent spacing?\n 2. **Animation Quality** (1-10): Based on the visual design, do the layout and component choices suggest smooth, purposeful animations?\n 3. **Component Quality** (1-10): Do components look well-built (cards, buttons, forms, navigation, testimonials, gallery)?\n 4. **Responsive Design Hints** (1-10): Does the desktop layout suggest proper responsive breakpoints and mobile adaptation?\n 5. **Accessibility Basics** (1-10): Is there sufficient contrast (especially dark theme)? Are interactive elements clearly identifiable?\n 6. **Performance Indicators** (1-10): Does the design suggest good performance practices?\n 7. **Overall Frontend Quality** (1-10): Would you be proud to show this in your agency portfolio?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_consistency\": 0.0, \"animation_quality\": 0.0, \"component_quality\": 0.0, \"responsive_hints\": 0.0, \"accessibility\": 0.0, \"performance_indicators\": 0.0, \"overall_frontend\": 0.0, \"avg_frontend\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely exceptional frontend work. Most professional sites score 6-8.\"\"\"\n\n # ========== API FUNCTIONS ==========\n \n def parse_json(s):\n if not s: return {}\n s = s.strip()\n if s.startswith(\"```\"):\n lines = s.split(\"\\n\")\n # Remove first and last lines (fences)\n s = \"\\n\".join(lines[1:])\n if s.rstrip().endswith(\"```\"):\n s = s.rstrip()[:-3]\n s = s.strip()\n if not s.startswith(\"{\"):\n i, j = s.find(\"{\"), s.rfind(\"}\")\n if 0 <= i < j: s = s[i:j+1]\n try: return json.loads(s)\n except:\n # Try fixing common issues\n s = s.replace('\\n', ' ').replace(' ', ' ')\n try: return json.loads(s)\n except: return {}\n\n def call_openai_vision(prompt, images, model=\"gpt-4o\"):\n \"\"\"OpenAI GPT-4o with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENAI_API_KEY\")\n if not key:\n print(\" [WARN] No OPENAI_API_KEY\")\n return None\n url = \"https://api.openai.com/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_groq_vision(prompt, images):\n \"\"\"Groq Llama 3.3 70B with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"GROQ_API_KEY\")\n if not key:\n print(\" [WARN] No GROQ_API_KEY\")\n return None\n url = \"https://api.groq.com/openai/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": \"llama-3.3-70b-versatile\",\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_openrouter_vision(prompt, images, model=\"google/gemini-2.5-flash-preview:thinking\"):\n \"\"\"OpenRouter (Gemini) with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENROUTER_API_KEY\")\n if not key:\n print(\" [WARN] No OPENROUTER_API_KEY\")\n return None\n url = \"https://openrouter.ai/api/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": SITE_URL, \"X-Title\": \"Nilov Catering Site Critic\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_deepseek_text(prompt):\n \"\"\"DeepSeek V3 text-only - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"DEEPSEEK_API_KEY\")\n if not key:\n print(\" [WARN] No DEEPSEEK_API_KEY\")\n return None\n url = \"https://api.deepseek.com/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details you should know:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Based on these details and your expertise, evaluate the design as if you were reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] DeepSeek: {e}\")\n return None\n\n def call_sambanova_text(prompt):\n \"\"\"SambaNova Llama 3.3 70B text-only - TRULLY EXTERNAL\"\"\"\n key = os.environ.get(\"SAMBANOVA_API_KEY\")\n if not key:\n print(\" [WARN] No SAMBANOVA_API_KEY\")\n return None\n url = \"https://api.sambanova.ai/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Evaluate based on these details as if reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"Meta-Llama-3.3-70B-Instruct\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] SambaNova: {e}\")\n return None\n\n # ========== CRITIC ASSIGNMENTS ==========\n # Vision models see actual screenshots, text models get detailed descriptions\n vision_images = [img_hero, img_middle, img_bottom] # 3 viewport shots for vision models\n \n CRITICS = [\n (\"GPT-4o (OpenAI)\", \"Visual Design (Awwwards)\", VISUAL_CRITIC_PROMPT, \n lambda p: call_openai_vision(p, [img_full] + vision_images)),\n (\"Llama-3.3-70B (Groq)\", \"UX/CRO (Baymard)\", UX_CRITIC_PROMPT,\n lambda p: call_groq_vision(p, vision_images)),\n (\"Gemini-2.5-Flash (OpenRouter)\", \"Frontend/Perf\", FRONTEND_CRITIC_PROMPT,\n lambda p: call_openrouter_vision(p, vision_images)),\n (\"DeepSeek-V3 (DeepSeek)\", \"UX/CRO #2 (text)\", UX_CRITIC_PROMPT,\n lambda p: call_deepseek_text(p)),\n (\"Llama-3.3-70B (SambaNova)\", \"Visual #2 (text)\", VISUAL_CRITIC_PROMPT,\n lambda p: call_sambanova_text(p)),\n ]\n\n # ========== RUN ALL CRITICS ==========\n \n results = {}\n all_issues = []\n all_praise = []\n \n def run_critic(name, role, prompt, fn):\n print(f\"\\n{'='*60}\")\n print(f\"Running: {name} ({role})...\")\n sys.stdout.flush()\n try:\n raw = fn(prompt)\n if raw:\n print(f\" Raw response length: {len(raw)}\")\n parsed = parse_json(raw)\n if not parsed:\n print(f\" [WARN] Failed to parse JSON from response\")\n print(f\" First 200 chars: {raw[:200]}\")\n return\n results[f\"{name}|{role}\"] = parsed\n avg = parsed.get(\"avg_visual\", parsed.get(\"avg_ux\", parsed.get(\"avg_frontend\", 0)))\n print(f\" Score: {avg}/10\")\n print(f\" Verdict: {parsed.get('verdict', 'UNKNOWN')}\")\n for issue in parsed.get(\"critical_issues\", [])[:5]:\n all_issues.append({\"critic\": name, \"role\": role, \"issue\": issue})\n print(f\" Issue: {issue}\")\n for p in parsed.get(\"praise\", [])[:3]:\n all_praise.append({\"critic\": name, \"role\": role, \"praise\": p})\n print(f\" Praise: {p}\")\n else:\n print(f\" FAILED: No response from API\")\n except Exception as e:\n print(f\" ERROR: {type(e).__name__}: {e}\")\n sys.stdout.flush()\n\n print(\"=\" * 60)\n print(\"EXTERNAL LLM CRITIC EVALUATION\")\n print(\"=\" * 60)\n print(f\"Site: {SITE_URL}\")\n print(f\"Iteration: {ITERATION}\")\n print(f\"Vision critics: GPT-4o, Groq Llama-3.3-70B, Gemini-2.5-Flash\")\n print(f\"Text critics: DeepSeek-V3, SambaNova Llama-3.3-70B\")\n print(f\"ALL critics are EXTERNAL LLM APIs, NOT the site builder\")\n sys.stdout.flush()\n \n # Run critics sequentially to avoid API rate limits\n for name, role, prompt, fn in CRITICS:\n run_critic(name, role, prompt, fn)\n\n # ========== COMPILE RESULTS ==========\n \n all_scores = []\n score_details = []\n for key, val in results.items():\n for field in [\"avg_visual\", \"avg_ux\", \"avg_frontend\"]:\n if field in val:\n s = val[field]\n all_scores.append(s)\n score_details.append({\"critic\": key, \"type\": field, \"score\": s})\n \n overall_avg = round(sum(all_scores) / len(all_scores), 2) if all_scores else 0\n min_score = min(all_scores) if all_scores else 0\n max_score = max(all_scores) if all_scores else 0\n \n final = {\n \"iteration\": int(ITERATION),\n \"site_url\": SITE_URL,\n \"timestamp\": time.strftime(\"%Y-%m-%d %H:%M:%S UTC\"),\n \"evaluation_type\": \"EXTERNAL_LLM_CRITICS\",\n \"note\": \"All scores from independent external LLM APIs (OpenAI GPT-4o, Groq Llama-3.3-70B, OpenRouter Gemini, DeepSeek-V3, SambaNova Llama-3.3-70B). NOT self-evaluation.\",\n \"overall_average\": overall_avg,\n \"min_score\": min_score,\n \"max_score\": max_score,\n \"all_individual_scores\": all_scores,\n \"score_details\": score_details,\n \"verdict\": \"APPROVED\" if overall_avg >= 9.0 else \"REDO\",\n \"critics\": {k: v for k, v in results.items()},\n \"all_critical_issues\": all_issues,\n \"all_praise\": all_praise,\n \"n_critics_responded\": len(results),\n \"n_critics_total\": len(CRITICS),\n \"summary\": f\"Average: {overall_avg}/10 | Min: {min_score} | Max: {max_score} | Critics: {len(results)}/{len(CRITICS)} | Verdict: {'APPROVED - ALL CRITICS >9' if overall_avg >= 9.0 else 'REDO - needs improvements'}\"\n }\n \n with open(\"critic_results.json\", \"w\") as f:\n json.dump(final, f, indent=2, ensure_ascii=False)\n \n print(f\"\\n{'='*60}\")\n print(f\"FINAL RESULTS - Iteration {ITERATION}\")\n print(f\"{'='*60}\")\n print(f\"Overall Average: {overall_avg}/10\")\n print(f\"Min Score: {min_score}/10\")\n print(f\"Max Score: {max_score}/10\")\n print(f\"Individual Scores: {[f'{s}/10' for s in all_scores]}\")\n print(f\"Verdict: {final['verdict']}\")\n print(f\"\\nALL CRITICAL ISSUES ({len(all_issues)} total):\")\n for item in all_issues:\n print(f\" [{item['critic']}] {item['issue']}\")\n print(f\"\\nPRAISE ({len(all_praise)} total):\")\n for item in all_praise:\n print(f\" [{item['critic']}] {item['praise']}\")\n print(f\"\\nResults saved to critic_results.json\")\n sys.stdout.flush()\n PYEOF\n\n - name: Save results\n run: |\n mkdir -p critic-results\n cp critic_results.json critic-results/iteration_${{ inputs.iteration }}.json\n cp critic_results.json critic-results/latest.json\n echo \"=== SUMMARY ===\"\n python3 -c \"\n import json\n with open('critic_results.json') as f:\n d = json.load(f)\n print(f'OVERALL: {d[\\\"overall_average\\\"]}/10')\n print(f'MIN: {d[\\\"min_score\\\"]}/10 | MAX: {d[\\\"max_score\\\"]}/10')\n print(f'VERDICT: {d[\\\"verdict\\\"]}')\n print(f'CRITICS: {d[\\\"n_critics_responded\\\"]}/{d[\\\"n_critics_total\\\"]}')\n print()\n print('SCORES:')\n for s in d['score_details']:\n print(f' {s[\\\"critic\\\"]}: {s[\\\"score\\\"]}/10 ({s[\\\"type\\\"]})')\n print()\n print('ISSUES:')\n for item in d['all_critical_issues']:\n print(f' [{item[\\\"critic\\\"]}] {item[\\\"issue\\\"]}')\n \"\n\n - name: Commit results to repo\n run: |\n git config user.name \"External Critic Bot\"\n git config user.email \"critic-bot@external-llm.local\"\n git add critic-results/\n ITER=${{ inputs.iteration }}\n AVG=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['overall_average'])\" 2>/dev/null || echo \"0\")\n VERDICT=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['verdict'])\" 2>/dev/null || echo \"UNKNOWN\")\n git commit -m \"Critic eval iter ${ITER}: ${VERDICT} (avg ${AVG}/10) [external LLMs]\" || echo \"No changes to commit\"\n git push || echo \"Push failed (may need force)\"\n\n - name: Upload screenshots as artifacts\n uses: actions/upload-artifact@v4\n with:\n name: screenshots-iter-${{ inputs.iteration }}\n path: |\n screenshot_full.png\n screenshot_hero.png\n screenshot_middle.png\n screenshot_bottom.png\n retention-days: 7"}, "06fd1afaa3212dd482f9053b05e127bf0708fd1c": {"t": 1785762175.8358438, "n": 27446, "h": "c6d330bc63f1c60e", "c": "name: External LLM Critics Evaluation\n\non:\n workflow_dispatch:\n inputs:\n site_url:\n description: 'Live site URL to screenshot and evaluate'\n required: true\n type: string\n default: 'https://odaeda.ru/'\n iteration:\n description: 'Iteration number'\n required: false\n type: string\n default: '1'\n\njobs:\n evaluate:\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: '22'\n\n - name: Install Playwright\n run: |\n npm install playwright\n npx playwright install chromium --with-deps\n\n - name: Take full-page screenshot\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n console.log('Navigating to:', siteUrl);\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n // Wait for animations/preloader to finish\n await page.waitForTimeout(4000);\n // Scroll to load lazy content\n await page.evaluate(async () => {\n const delay = ms => new Promise(r => setTimeout(r, ms));\n const height = document.body.scrollHeight;\n const step = window.innerHeight;\n for (let y = 0; y < height; y += step) {\n window.scrollTo(0, y);\n await delay(500);\n }\n window.scrollTo(0, 0);\n await delay(1000);\n });\n await page.screenshot({\n path: 'screenshot_full.png',\n fullPage: true\n });\n console.log('Screenshot saved: screenshot_full.png');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Also take 3 viewport screenshots (hero, middle, bottom)\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n await page.waitForTimeout(4000);\n // Hero\n await page.screenshot({ path: 'screenshot_hero.png' });\n // Middle\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.4));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_middle.png' });\n // Bottom\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.75));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_bottom.png' });\n console.log('Viewport screenshots saved');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Setup Python\n uses: actions/setup-python@v5\n with:\n python-version: '3.12'\n\n - name: Checkout submodules workaround\n run: |\n git submodule deinit -f . 2>/dev/null || true\n rm -rf .git/modules 2>/dev/null || true\n git config --unset core.submodulesfile 2>/dev/null || true\n rm -f .gitmodules 2>/dev/null || true\n\n - name: Run External LLM Critics\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}\n OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\n SAMBANOVA_API_KEY: ${{ secrets.SAMBANOVA_API_KEY }}\n GOOGLE_AI_KEY: ${{ secrets.GOOGLE_AI_KEY }}\n DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}\n SITE_URL: ${{ inputs.site_url }}\n ITERATION: ${{ inputs.iteration }}\n run: |\n python3 << 'PYEOF'\n import json, urllib.request, urllib.error, base64, sys, os, time, concurrent.futures\n from pathlib import Path\n\n SITE_URL = os.environ.get(\"SITE_URL\", \"https://odaeda.ru/\")\n ITERATION = os.environ.get(\"ITERATION\", \"1\")\n\n # Read and base64-encode the FULL page screenshot\n def img_to_data_uri(path):\n with open(path, \"rb\") as f:\n return f\"data:image/png;base64,{base64.b64encode(f.read()).decode()}\"\n\n img_full = img_to_data_uri(\"screenshot_full.png\")\n img_hero = img_to_data_uri(\"screenshot_hero.png\")\n print(f\"Full screenshot: {len(img_full)} chars base64\")\n\n # ========== CRITIC PROMPTS ==========\n \n VISUAL_CRITIC_PROMPT = \"\"\"You are an Awwwards-level Visual Design Judge. You have judged 1000+ premium websites.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Hierarchy** (1-10): Is there clear typographic and visual hierarchy? Do headings, body text, and accents have proper contrast in size/weight/color?\n 2. **Color Palette & Harmony** (1-10): Is the color scheme cohesive, luxurious, and appropriate for premium catering? Are there any jarring colors?\n 3. **Typography** (1-10): Are fonts well-chosen for luxury branding? Is spacing, line-height, and font-pairing professional?\n 4. **Imagery & Visual Assets** (1-10): Are food/venue images high quality? Properly sized and positioned? Or are they placeholder/stock-looking?\n 5. **Spacing & Layout Rhythm** (1-10): Is whitespace used effectively? Is there consistent padding/margin rhythm?\n 6. **Micro-interactions & Polish** (1-10): Based on visual design, does the site look polished with extreme attention to detail?\n 7. **Overall Visual Impression** (1-10): First-impression wow factor. Would this site be nominated for an Awwwards SOTD?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_hierarchy\": 0.0, \"color_palette\": 0.0, \"typography\": 0.0, \"imagery\": 0.0, \"spacing_layout\": 0.0, \"micro_interactions\": 0.0, \"overall_visual\": 0.0, \"avg_visual\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for truly exceptional Awwwards-quality design. Most professional sites score 6-8.\"\"\"\n\n UX_CRITIC_PROMPT = \"\"\"You are a Senior UX/CRO Expert from Baymard Institute. You have conducted 500+ usability audits for luxury brands.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Navigation Clarity** (1-10): Is the main navigation intuitive? Can users find key sections (services, menu, pricing, contact) easily?\n 2. **Content Scannability** (1-10): Is text easy to scan? Are headings descriptive? Is content structured logically with clear sections?\n 3. **Call-to-Action Effectiveness** (1-10): Are CTAs prominent, clear, and action-oriented? Is there a clear conversion path from landing to contact?\n 4. **Trust Signals** (1-10): Are testimonials, stats, years of experience, guarantees visible? Do they build credibility for a premium service?\n 5. **Mobile-first Design** (1-10): Based on the desktop view, does the layout structure suggest good mobile adaptation?\n 6. **Information Architecture** (1-10): Is the page flow logical? Hero -> Social proof -> Services -> Process -> Menu -> Testimonials -> Gallery -> FAQ -> CTA?\n 7. **Overall UX Score** (1-10): Would a potential client easily understand the value proposition and know how to take action?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"navigation\": 0.0, \"scannability\": 0.0, \"cta_effectiveness\": 0.0, \"trust_signals\": 0.0, \"mobile_first\": 0.0, \"info_architecture\": 0.0, \"overall_ux\": 0.0, \"avg_ux\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely world-class UX. Most professional sites score 6-8.\"\"\"\n\n FRONTEND_CRITIC_PROMPT = \"\"\"You are a Principal Frontend Engineer & Web Performance Expert at a top agency. You review production websites daily.\n \n I will provide you with multiple screenshots of a luxury catering website built with Next.js 16, Tailwind CSS 4, Framer Motion.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Consistency** (1-10): Are all elements consistently styled? No layout shifts, broken alignment, or inconsistent spacing?\n 2. **Animation Quality** (1-10): Based on the visual design, do the layout and component choices suggest smooth, purposeful animations?\n 3. **Component Quality** (1-10): Do components look well-built (cards, buttons, forms, navigation, testimonials, gallery)?\n 4. **Responsive Design Hints** (1-10): Does the desktop layout suggest proper responsive breakpoints and mobile adaptation?\n 5. **Accessibility Basics** (1-10): Is there sufficient contrast (especially dark theme)? Are interactive elements clearly identifiable?\n 6. **Performance Indicators** (1-10): Does the design suggest good performance practices?\n 7. **Overall Frontend Quality** (1-10): Would you be proud to show this in your agency portfolio?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_consistency\": 0.0, \"animation_quality\": 0.0, \"component_quality\": 0.0, \"responsive_hints\": 0.0, \"accessibility\": 0.0, \"performance_indicators\": 0.0, \"overall_frontend\": 0.0, \"avg_frontend\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely exceptional frontend work. Most professional sites score 6-8.\"\"\"\n\n # ========== API FUNCTIONS ==========\n \n def parse_json(s):\n if not s: return {}\n s = s.strip()\n if s.startswith(\"```\"):\n lines = s.split(\"\\n\")\n # Remove first and last lines (fences)\n s = \"\\n\".join(lines[1:])\n if s.rstrip().endswith(\"```\"):\n s = s.rstrip()[:-3]\n s = s.strip()\n if not s.startswith(\"{\"):\n i, j = s.find(\"{\"), s.rfind(\"}\")\n if 0 <= i < j: s = s[i:j+1]\n try: return json.loads(s)\n except:\n # Try fixing common issues\n s = s.replace('\\n', ' ').replace(' ', ' ')\n try: return json.loads(s)\n except: return {}\n\n def call_openai_vision(prompt, images, model=\"gpt-4o\"):\n \"\"\"OpenAI GPT-4o with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENAI_API_KEY\")\n if not key:\n print(\" [WARN] No OPENAI_API_KEY\")\n return None\n url = \"https://api.openai.com/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_groq_vision(prompt, images):\n \"\"\"Groq Llama 3.3 70B with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"GROQ_API_KEY\")\n if not key:\n print(\" [WARN] No GROQ_API_KEY\")\n return None\n url = \"https://api.groq.com/openai/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": \"llama-3.3-70b-versatile\",\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_openrouter_vision(prompt, images, model=\"google/gemini-2.5-flash-preview:thinking\"):\n \"\"\"OpenRouter (Gemini) with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENROUTER_API_KEY\")\n if not key:\n print(\" [WARN] No OPENROUTER_API_KEY\")\n return None\n url = \"https://openrouter.ai/api/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": SITE_URL, \"X-Title\": \"Nilov Catering Site Critic\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_deepseek_text(prompt):\n \"\"\"DeepSeek V3 text-only - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"DEEPSEEK_API_KEY\")\n if not key:\n print(\" [WARN] No DEEPSEEK_API_KEY\")\n return None\n url = \"https://api.deepseek.com/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details you should know:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Based on these details and your expertise, evaluate the design as if you were reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] DeepSeek: {e}\")\n return None\n\n def call_sambanova_text(prompt):\n \"\"\"SambaNova Llama 3.3 70B text-only - TRULLY EXTERNAL\"\"\"\n key = os.environ.get(\"SAMBANOVA_API_KEY\")\n if not key:\n print(\" [WARN] No SAMBANOVA_API_KEY\")\n return None\n url = \"https://api.sambanova.ai/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Evaluate based on these details as if reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"Meta-Llama-3.3-70B-Instruct\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] SambaNova: {e}\")\n return None\n\n # ========== CRITIC ASSIGNMENTS ==========\n # Vision models see actual screenshots, text models get detailed descriptions\n vision_images = [img_hero, img_middle, img_bottom] # 3 viewport shots for vision models\n \n CRITICS = [\n (\"GPT-4o (OpenAI)\", \"Visual Design (Awwwards)\", VISUAL_CRITIC_PROMPT, \n lambda p: call_openai_vision(p, [img_full] + vision_images)),\n (\"Llama-3.3-70B (Groq)\", \"UX/CRO (Baymard)\", UX_CRITIC_PROMPT,\n lambda p: call_groq_vision(p, vision_images)),\n (\"Gemini-2.5-Flash (OpenRouter)\", \"Frontend/Perf\", FRONTEND_CRITIC_PROMPT,\n lambda p: call_openrouter_vision(p, vision_images)),\n (\"DeepSeek-V3 (DeepSeek)\", \"UX/CRO #2 (text)\", UX_CRITIC_PROMPT,\n lambda p: call_deepseek_text(p)),\n (\"Llama-3.3-70B (SambaNova)\", \"Visual #2 (text)\", VISUAL_CRITIC_PROMPT,\n lambda p: call_sambanova_text(p)),\n ]\n\n # ========== RUN ALL CRITICS ==========\n \n results = {}\n all_issues = []\n all_praise = []\n \n def run_critic(name, role, prompt, fn):\n print(f\"\\n{'='*60}\")\n print(f\"Running: {name} ({role})...\")\n sys.stdout.flush()\n try:\n raw = fn(prompt)\n if raw:\n print(f\" Raw response length: {len(raw)}\")\n parsed = parse_json(raw)\n if not parsed:\n print(f\" [WARN] Failed to parse JSON from response\")\n print(f\" First 200 chars: {raw[:200]}\")\n return\n results[f\"{name}|{role}\"] = parsed\n avg = parsed.get(\"avg_visual\", parsed.get(\"avg_ux\", parsed.get(\"avg_frontend\", 0)))\n print(f\" Score: {avg}/10\")\n print(f\" Verdict: {parsed.get('verdict', 'UNKNOWN')}\")\n for issue in parsed.get(\"critical_issues\", [])[:5]:\n all_issues.append({\"critic\": name, \"role\": role, \"issue\": issue})\n print(f\" Issue: {issue}\")\n for p in parsed.get(\"praise\", [])[:3]:\n all_praise.append({\"critic\": name, \"role\": role, \"praise\": p})\n print(f\" Praise: {p}\")\n else:\n print(f\" FAILED: No response from API\")\n except Exception as e:\n print(f\" ERROR: {type(e).__name__}: {e}\")\n sys.stdout.flush()\n\n print(\"=\" * 60)\n print(\"EXTERNAL LLM CRITIC EVALUATION\")\n print(\"=\" * 60)\n print(f\"Site: {SITE_URL}\")\n print(f\"Iteration: {ITERATION}\")\n print(f\"Vision critics: GPT-4o, Groq Llama-3.3-70B, Gemini-2.5-Flash\")\n print(f\"Text critics: DeepSeek-V3, SambaNova Llama-3.3-70B\")\n print(f\"ALL critics are EXTERNAL LLM APIs, NOT the site builder\")\n sys.stdout.flush()\n \n # Run critics sequentially to avoid API rate limits\n for name, role, prompt, fn in CRITICS:\n run_critic(name, role, prompt, fn)\n\n # ========== COMPILE RESULTS ==========\n \n all_scores = []\n score_details = []\n for key, val in results.items():\n for field in [\"avg_visual\", \"avg_ux\", \"avg_frontend\"]:\n if field in val:\n s = val[field]\n all_scores.append(s)\n score_details.append({\"critic\": key, \"type\": field, \"score\": s})\n \n overall_avg = round(sum(all_scores) / len(all_scores), 2) if all_scores else 0\n min_score = min(all_scores) if all_scores else 0\n max_score = max(all_scores) if all_scores else 0\n \n final = {\n \"iteration\": int(ITERATION),\n \"site_url\": SITE_URL,\n \"timestamp\": time.strftime(\"%Y-%m-%d %H:%M:%S UTC\"),\n \"evaluation_type\": \"EXTERNAL_LLM_CRITICS\",\n \"note\": \"All scores from independent external LLM APIs (OpenAI GPT-4o, Groq Llama-3.3-70B, OpenRouter Gemini, DeepSeek-V3, SambaNova Llama-3.3-70B). NOT self-evaluation.\",\n \"overall_average\": overall_avg,\n \"min_score\": min_score,\n \"max_score\": max_score,\n \"all_individual_scores\": all_scores,\n \"score_details\": score_details,\n \"verdict\": \"APPROVED\" if overall_avg >= 9.0 else \"REDO\",\n \"critics\": {k: v for k, v in results.items()},\n \"all_critical_issues\": all_issues,\n \"all_praise\": all_praise,\n \"n_critics_responded\": len(results),\n \"n_critics_total\": len(CRITICS),\n \"summary\": f\"Average: {overall_avg}/10 | Min: {min_score} | Max: {max_score} | Critics: {len(results)}/{len(CRITICS)} | Verdict: {'APPROVED - ALL CRITICS >9' if overall_avg >= 9.0 else 'REDO - needs improvements'}\"\n }\n \n with open(\"critic_results.json\", \"w\") as f:\n json.dump(final, f, indent=2, ensure_ascii=False)\n \n print(f\"\\n{'='*60}\")\n print(f\"FINAL RESULTS - Iteration {ITERATION}\")\n print(f\"{'='*60}\")\n print(f\"Overall Average: {overall_avg}/10\")\n print(f\"Min Score: {min_score}/10\")\n print(f\"Max Score: {max_score}/10\")\n print(f\"Individual Scores: {[f'{s}/10' for s in all_scores]}\")\n print(f\"Verdict: {final['verdict']}\")\n print(f\"\\nALL CRITICAL ISSUES ({len(all_issues)} total):\")\n for item in all_issues:\n print(f\" [{item['critic']}] {item['issue']}\")\n print(f\"\\nPRAISE ({len(all_praise)} total):\")\n for item in all_praise:\n print(f\" [{item['critic']}] {item['praise']}\")\n print(f\"\\nResults saved to critic_results.json\")\n sys.stdout.flush()\n PYEOF\n\n - name: Save results\n run: |\n mkdir -p critic-results\n cp critic_results.json critic-results/iteration_${{ inputs.iteration }}.json\n cp critic_results.json critic-results/latest.json\n echo \"=== SUMMARY ===\"\n python3 -c \"\n import json\n with open('critic_results.json') as f:\n d = json.load(f)\n print(f'OVERALL: {d[\\\"overall_average\\\"]}/10')\n print(f'MIN: {d[\\\"min_score\\\"]}/10 | MAX: {d[\\\"max_score\\\"]}/10')\n print(f'VERDICT: {d[\\\"verdict\\\"]}')\n print(f'CRITICS: {d[\\\"n_critics_responded\\\"]}/{d[\\\"n_critics_total\\\"]}')\n print()\n print('SCORES:')\n for s in d['score_details']:\n print(f' {s[\\\"critic\\\"]}: {s[\\\"score\\\"]}/10 ({s[\\\"type\\\"]})')\n print()\n print('ISSUES:')\n for item in d['all_critical_issues']:\n print(f' [{item[\\\"critic\\\"]}] {item[\\\"issue\\\"]}')\n \"\n\n - name: Commit results to repo\n run: |\n git config user.name \"External Critic Bot\"\n git config user.email \"critic-bot@external-llm.local\"\n git add critic-results/\n ITER=${{ inputs.iteration }}\n AVG=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['overall_average'])\" 2>/dev/null || echo \"0\")\n VERDICT=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['verdict'])\" 2>/dev/null || echo \"UNKNOWN\")\n git commit -m \"Critic eval iter ${ITER}: ${VERDICT} (avg ${AVG}/10) [external LLMs]\" || echo \"No changes to commit\"\n git push || echo \"Push failed (may need force)\"\n\n - name: Upload screenshots as artifacts\n uses: actions/upload-artifact@v4\n with:\n name: screenshots-iter-${{ inputs.iteration }}\n path: |\n screenshot_full.png\n screenshot_hero.png\n screenshot_middle.png\n screenshot_bottom.png\n retention-days: 7"}, "247585b875f7c67b84acad69e17397793e56a90f": {"t": 1786067701.33253, "n": 27446, "h": "c6d330bc63f1c60e", "c": "name: External LLM Critics Evaluation\n\non:\n workflow_dispatch:\n inputs:\n site_url:\n description: 'Live site URL to screenshot and evaluate'\n required: true\n type: string\n default: 'https://odaeda.ru/'\n iteration:\n description: 'Iteration number'\n required: false\n type: string\n default: '1'\n\njobs:\n evaluate:\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: '22'\n\n - name: Install Playwright\n run: |\n npm install playwright\n npx playwright install chromium --with-deps\n\n - name: Take full-page screenshot\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n console.log('Navigating to:', siteUrl);\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n // Wait for animations/preloader to finish\n await page.waitForTimeout(4000);\n // Scroll to load lazy content\n await page.evaluate(async () => {\n const delay = ms => new Promise(r => setTimeout(r, ms));\n const height = document.body.scrollHeight;\n const step = window.innerHeight;\n for (let y = 0; y < height; y += step) {\n window.scrollTo(0, y);\n await delay(500);\n }\n window.scrollTo(0, 0);\n await delay(1000);\n });\n await page.screenshot({\n path: 'screenshot_full.png',\n fullPage: true\n });\n console.log('Screenshot saved: screenshot_full.png');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Also take 3 viewport screenshots (hero, middle, bottom)\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n await page.waitForTimeout(4000);\n // Hero\n await page.screenshot({ path: 'screenshot_hero.png' });\n // Middle\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.4));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_middle.png' });\n // Bottom\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.75));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_bottom.png' });\n console.log('Viewport screenshots saved');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Setup Python\n uses: actions/setup-python@v5\n with:\n python-version: '3.12'\n\n - name: Checkout submodules workaround\n run: |\n git submodule deinit -f . 2>/dev/null || true\n rm -rf .git/modules 2>/dev/null || true\n git config --unset core.submodulesfile 2>/dev/null || true\n rm -f .gitmodules 2>/dev/null || true\n\n - name: Run External LLM Critics\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}\n OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\n SAMBANOVA_API_KEY: ${{ secrets.SAMBANOVA_API_KEY }}\n GOOGLE_AI_KEY: ${{ secrets.GOOGLE_AI_KEY }}\n DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}\n SITE_URL: ${{ inputs.site_url }}\n ITERATION: ${{ inputs.iteration }}\n run: |\n python3 << 'PYEOF'\n import json, urllib.request, urllib.error, base64, sys, os, time, concurrent.futures\n from pathlib import Path\n\n SITE_URL = os.environ.get(\"SITE_URL\", \"https://odaeda.ru/\")\n ITERATION = os.environ.get(\"ITERATION\", \"1\")\n\n # Read and base64-encode the FULL page screenshot\n def img_to_data_uri(path):\n with open(path, \"rb\") as f:\n return f\"data:image/png;base64,{base64.b64encode(f.read()).decode()}\"\n\n img_full = img_to_data_uri(\"screenshot_full.png\")\n img_hero = img_to_data_uri(\"screenshot_hero.png\")\n print(f\"Full screenshot: {len(img_full)} chars base64\")\n\n # ========== CRITIC PROMPTS ==========\n \n VISUAL_CRITIC_PROMPT = \"\"\"You are an Awwwards-level Visual Design Judge. You have judged 1000+ premium websites.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Hierarchy** (1-10): Is there clear typographic and visual hierarchy? Do headings, body text, and accents have proper contrast in size/weight/color?\n 2. **Color Palette & Harmony** (1-10): Is the color scheme cohesive, luxurious, and appropriate for premium catering? Are there any jarring colors?\n 3. **Typography** (1-10): Are fonts well-chosen for luxury branding? Is spacing, line-height, and font-pairing professional?\n 4. **Imagery & Visual Assets** (1-10): Are food/venue images high quality? Properly sized and positioned? Or are they placeholder/stock-looking?\n 5. **Spacing & Layout Rhythm** (1-10): Is whitespace used effectively? Is there consistent padding/margin rhythm?\n 6. **Micro-interactions & Polish** (1-10): Based on visual design, does the site look polished with extreme attention to detail?\n 7. **Overall Visual Impression** (1-10): First-impression wow factor. Would this site be nominated for an Awwwards SOTD?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_hierarchy\": 0.0, \"color_palette\": 0.0, \"typography\": 0.0, \"imagery\": 0.0, \"spacing_layout\": 0.0, \"micro_interactions\": 0.0, \"overall_visual\": 0.0, \"avg_visual\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for truly exceptional Awwwards-quality design. Most professional sites score 6-8.\"\"\"\n\n UX_CRITIC_PROMPT = \"\"\"You are a Senior UX/CRO Expert from Baymard Institute. You have conducted 500+ usability audits for luxury brands.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Navigation Clarity** (1-10): Is the main navigation intuitive? Can users find key sections (services, menu, pricing, contact) easily?\n 2. **Content Scannability** (1-10): Is text easy to scan? Are headings descriptive? Is content structured logically with clear sections?\n 3. **Call-to-Action Effectiveness** (1-10): Are CTAs prominent, clear, and action-oriented? Is there a clear conversion path from landing to contact?\n 4. **Trust Signals** (1-10): Are testimonials, stats, years of experience, guarantees visible? Do they build credibility for a premium service?\n 5. **Mobile-first Design** (1-10): Based on the desktop view, does the layout structure suggest good mobile adaptation?\n 6. **Information Architecture** (1-10): Is the page flow logical? Hero -> Social proof -> Services -> Process -> Menu -> Testimonials -> Gallery -> FAQ -> CTA?\n 7. **Overall UX Score** (1-10): Would a potential client easily understand the value proposition and know how to take action?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"navigation\": 0.0, \"scannability\": 0.0, \"cta_effectiveness\": 0.0, \"trust_signals\": 0.0, \"mobile_first\": 0.0, \"info_architecture\": 0.0, \"overall_ux\": 0.0, \"avg_ux\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely world-class UX. Most professional sites score 6-8.\"\"\"\n\n FRONTEND_CRITIC_PROMPT = \"\"\"You are a Principal Frontend Engineer & Web Performance Expert at a top agency. You review production websites daily.\n \n I will provide you with multiple screenshots of a luxury catering website built with Next.js 16, Tailwind CSS 4, Framer Motion.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Consistency** (1-10): Are all elements consistently styled? No layout shifts, broken alignment, or inconsistent spacing?\n 2. **Animation Quality** (1-10): Based on the visual design, do the layout and component choices suggest smooth, purposeful animations?\n 3. **Component Quality** (1-10): Do components look well-built (cards, buttons, forms, navigation, testimonials, gallery)?\n 4. **Responsive Design Hints** (1-10): Does the desktop layout suggest proper responsive breakpoints and mobile adaptation?\n 5. **Accessibility Basics** (1-10): Is there sufficient contrast (especially dark theme)? Are interactive elements clearly identifiable?\n 6. **Performance Indicators** (1-10): Does the design suggest good performance practices?\n 7. **Overall Frontend Quality** (1-10): Would you be proud to show this in your agency portfolio?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_consistency\": 0.0, \"animation_quality\": 0.0, \"component_quality\": 0.0, \"responsive_hints\": 0.0, \"accessibility\": 0.0, \"performance_indicators\": 0.0, \"overall_frontend\": 0.0, \"avg_frontend\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely exceptional frontend work. Most professional sites score 6-8.\"\"\"\n\n # ========== API FUNCTIONS ==========\n \n def parse_json(s):\n if not s: return {}\n s = s.strip()\n if s.startswith(\"```\"):\n lines = s.split(\"\\n\")\n # Remove first and last lines (fences)\n s = \"\\n\".join(lines[1:])\n if s.rstrip().endswith(\"```\"):\n s = s.rstrip()[:-3]\n s = s.strip()\n if not s.startswith(\"{\"):\n i, j = s.find(\"{\"), s.rfind(\"}\")\n if 0 <= i < j: s = s[i:j+1]\n try: return json.loads(s)\n except:\n # Try fixing common issues\n s = s.replace('\\n', ' ').replace(' ', ' ')\n try: return json.loads(s)\n except: return {}\n\n def call_openai_vision(prompt, images, model=\"gpt-4o\"):\n \"\"\"OpenAI GPT-4o with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENAI_API_KEY\")\n if not key:\n print(\" [WARN] No OPENAI_API_KEY\")\n return None\n url = \"https://api.openai.com/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_groq_vision(prompt, images):\n \"\"\"Groq Llama 3.3 70B with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"GROQ_API_KEY\")\n if not key:\n print(\" [WARN] No GROQ_API_KEY\")\n return None\n url = \"https://api.groq.com/openai/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": \"llama-3.3-70b-versatile\",\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_openrouter_vision(prompt, images, model=\"google/gemini-2.5-flash-preview:thinking\"):\n \"\"\"OpenRouter (Gemini) with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENROUTER_API_KEY\")\n if not key:\n print(\" [WARN] No OPENROUTER_API_KEY\")\n return None\n url = \"https://openrouter.ai/api/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": SITE_URL, \"X-Title\": \"Nilov Catering Site Critic\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_deepseek_text(prompt):\n \"\"\"DeepSeek V3 text-only - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"DEEPSEEK_API_KEY\")\n if not key:\n print(\" [WARN] No DEEPSEEK_API_KEY\")\n return None\n url = \"https://api.deepseek.com/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details you should know:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Based on these details and your expertise, evaluate the design as if you were reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] DeepSeek: {e}\")\n return None\n\n def call_sambanova_text(prompt):\n \"\"\"SambaNova Llama 3.3 70B text-only - TRULLY EXTERNAL\"\"\"\n key = os.environ.get(\"SAMBANOVA_API_KEY\")\n if not key:\n print(\" [WARN] No SAMBANOVA_API_KEY\")\n return None\n url = \"https://api.sambanova.ai/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Evaluate based on these details as if reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"Meta-Llama-3.3-70B-Instruct\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] SambaNova: {e}\")\n return None\n\n # ========== CRITIC ASSIGNMENTS ==========\n # Vision models see actual screenshots, text models get detailed descriptions\n vision_images = [img_hero, img_middle, img_bottom] # 3 viewport shots for vision models\n \n CRITICS = [\n (\"GPT-4o (OpenAI)\", \"Visual Design (Awwwards)\", VISUAL_CRITIC_PROMPT, \n lambda p: call_openai_vision(p, [img_full] + vision_images)),\n (\"Llama-3.3-70B (Groq)\", \"UX/CRO (Baymard)\", UX_CRITIC_PROMPT,\n lambda p: call_groq_vision(p, vision_images)),\n (\"Gemini-2.5-Flash (OpenRouter)\", \"Frontend/Perf\", FRONTEND_CRITIC_PROMPT,\n lambda p: call_openrouter_vision(p, vision_images)),\n (\"DeepSeek-V3 (DeepSeek)\", \"UX/CRO #2 (text)\", UX_CRITIC_PROMPT,\n lambda p: call_deepseek_text(p)),\n (\"Llama-3.3-70B (SambaNova)\", \"Visual #2 (text)\", VISUAL_CRITIC_PROMPT,\n lambda p: call_sambanova_text(p)),\n ]\n\n # ========== RUN ALL CRITICS ==========\n \n results = {}\n all_issues = []\n all_praise = []\n \n def run_critic(name, role, prompt, fn):\n print(f\"\\n{'='*60}\")\n print(f\"Running: {name} ({role})...\")\n sys.stdout.flush()\n try:\n raw = fn(prompt)\n if raw:\n print(f\" Raw response length: {len(raw)}\")\n parsed = parse_json(raw)\n if not parsed:\n print(f\" [WARN] Failed to parse JSON from response\")\n print(f\" First 200 chars: {raw[:200]}\")\n return\n results[f\"{name}|{role}\"] = parsed\n avg = parsed.get(\"avg_visual\", parsed.get(\"avg_ux\", parsed.get(\"avg_frontend\", 0)))\n print(f\" Score: {avg}/10\")\n print(f\" Verdict: {parsed.get('verdict', 'UNKNOWN')}\")\n for issue in parsed.get(\"critical_issues\", [])[:5]:\n all_issues.append({\"critic\": name, \"role\": role, \"issue\": issue})\n print(f\" Issue: {issue}\")\n for p in parsed.get(\"praise\", [])[:3]:\n all_praise.append({\"critic\": name, \"role\": role, \"praise\": p})\n print(f\" Praise: {p}\")\n else:\n print(f\" FAILED: No response from API\")\n except Exception as e:\n print(f\" ERROR: {type(e).__name__}: {e}\")\n sys.stdout.flush()\n\n print(\"=\" * 60)\n print(\"EXTERNAL LLM CRITIC EVALUATION\")\n print(\"=\" * 60)\n print(f\"Site: {SITE_URL}\")\n print(f\"Iteration: {ITERATION}\")\n print(f\"Vision critics: GPT-4o, Groq Llama-3.3-70B, Gemini-2.5-Flash\")\n print(f\"Text critics: DeepSeek-V3, SambaNova Llama-3.3-70B\")\n print(f\"ALL critics are EXTERNAL LLM APIs, NOT the site builder\")\n sys.stdout.flush()\n \n # Run critics sequentially to avoid API rate limits\n for name, role, prompt, fn in CRITICS:\n run_critic(name, role, prompt, fn)\n\n # ========== COMPILE RESULTS ==========\n \n all_scores = []\n score_details = []\n for key, val in results.items():\n for field in [\"avg_visual\", \"avg_ux\", \"avg_frontend\"]:\n if field in val:\n s = val[field]\n all_scores.append(s)\n score_details.append({\"critic\": key, \"type\": field, \"score\": s})\n \n overall_avg = round(sum(all_scores) / len(all_scores), 2) if all_scores else 0\n min_score = min(all_scores) if all_scores else 0\n max_score = max(all_scores) if all_scores else 0\n \n final = {\n \"iteration\": int(ITERATION),\n \"site_url\": SITE_URL,\n \"timestamp\": time.strftime(\"%Y-%m-%d %H:%M:%S UTC\"),\n \"evaluation_type\": \"EXTERNAL_LLM_CRITICS\",\n \"note\": \"All scores from independent external LLM APIs (OpenAI GPT-4o, Groq Llama-3.3-70B, OpenRouter Gemini, DeepSeek-V3, SambaNova Llama-3.3-70B). NOT self-evaluation.\",\n \"overall_average\": overall_avg,\n \"min_score\": min_score,\n \"max_score\": max_score,\n \"all_individual_scores\": all_scores,\n \"score_details\": score_details,\n \"verdict\": \"APPROVED\" if overall_avg >= 9.0 else \"REDO\",\n \"critics\": {k: v for k, v in results.items()},\n \"all_critical_issues\": all_issues,\n \"all_praise\": all_praise,\n \"n_critics_responded\": len(results),\n \"n_critics_total\": len(CRITICS),\n \"summary\": f\"Average: {overall_avg}/10 | Min: {min_score} | Max: {max_score} | Critics: {len(results)}/{len(CRITICS)} | Verdict: {'APPROVED - ALL CRITICS >9' if overall_avg >= 9.0 else 'REDO - needs improvements'}\"\n }\n \n with open(\"critic_results.json\", \"w\") as f:\n json.dump(final, f, indent=2, ensure_ascii=False)\n \n print(f\"\\n{'='*60}\")\n print(f\"FINAL RESULTS - Iteration {ITERATION}\")\n print(f\"{'='*60}\")\n print(f\"Overall Average: {overall_avg}/10\")\n print(f\"Min Score: {min_score}/10\")\n print(f\"Max Score: {max_score}/10\")\n print(f\"Individual Scores: {[f'{s}/10' for s in all_scores]}\")\n print(f\"Verdict: {final['verdict']}\")\n print(f\"\\nALL CRITICAL ISSUES ({len(all_issues)} total):\")\n for item in all_issues:\n print(f\" [{item['critic']}] {item['issue']}\")\n print(f\"\\nPRAISE ({len(all_praise)} total):\")\n for item in all_praise:\n print(f\" [{item['critic']}] {item['praise']}\")\n print(f\"\\nResults saved to critic_results.json\")\n sys.stdout.flush()\n PYEOF\n\n - name: Save results\n run: |\n mkdir -p critic-results\n cp critic_results.json critic-results/iteration_${{ inputs.iteration }}.json\n cp critic_results.json critic-results/latest.json\n echo \"=== SUMMARY ===\"\n python3 -c \"\n import json\n with open('critic_results.json') as f:\n d = json.load(f)\n print(f'OVERALL: {d[\\\"overall_average\\\"]}/10')\n print(f'MIN: {d[\\\"min_score\\\"]}/10 | MAX: {d[\\\"max_score\\\"]}/10')\n print(f'VERDICT: {d[\\\"verdict\\\"]}')\n print(f'CRITICS: {d[\\\"n_critics_responded\\\"]}/{d[\\\"n_critics_total\\\"]}')\n print()\n print('SCORES:')\n for s in d['score_details']:\n print(f' {s[\\\"critic\\\"]}: {s[\\\"score\\\"]}/10 ({s[\\\"type\\\"]})')\n print()\n print('ISSUES:')\n for item in d['all_critical_issues']:\n print(f' [{item[\\\"critic\\\"]}] {item[\\\"issue\\\"]}')\n \"\n\n - name: Commit results to repo\n run: |\n git config user.name \"External Critic Bot\"\n git config user.email \"critic-bot@external-llm.local\"\n git add critic-results/\n ITER=${{ inputs.iteration }}\n AVG=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['overall_average'])\" 2>/dev/null || echo \"0\")\n VERDICT=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['verdict'])\" 2>/dev/null || echo \"UNKNOWN\")\n git commit -m \"Critic eval iter ${ITER}: ${VERDICT} (avg ${AVG}/10) [external LLMs]\" || echo \"No changes to commit\"\n git push || echo \"Push failed (may need force)\"\n\n - name: Upload screenshots as artifacts\n uses: actions/upload-artifact@v4\n with:\n name: screenshots-iter-${{ inputs.iteration }}\n path: |\n screenshot_full.png\n screenshot_hero.png\n screenshot_middle.png\n screenshot_bottom.png\n retention-days: 7"}, "36da04c8a2442ad2a05e7196bcdc7d5f74be311c": {"t": 1786068221.3770278, "n": 27446, "h": "c6d330bc63f1c60e", "c": "name: External LLM Critics Evaluation\n\non:\n workflow_dispatch:\n inputs:\n site_url:\n description: 'Live site URL to screenshot and evaluate'\n required: true\n type: string\n default: 'https://odaeda.ru/'\n iteration:\n description: 'Iteration number'\n required: false\n type: string\n default: '1'\n\njobs:\n evaluate:\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: '22'\n\n - name: Install Playwright\n run: |\n npm install playwright\n npx playwright install chromium --with-deps\n\n - name: Take full-page screenshot\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n console.log('Navigating to:', siteUrl);\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n // Wait for animations/preloader to finish\n await page.waitForTimeout(4000);\n // Scroll to load lazy content\n await page.evaluate(async () => {\n const delay = ms => new Promise(r => setTimeout(r, ms));\n const height = document.body.scrollHeight;\n const step = window.innerHeight;\n for (let y = 0; y < height; y += step) {\n window.scrollTo(0, y);\n await delay(500);\n }\n window.scrollTo(0, 0);\n await delay(1000);\n });\n await page.screenshot({\n path: 'screenshot_full.png',\n fullPage: true\n });\n console.log('Screenshot saved: screenshot_full.png');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Also take 3 viewport screenshots (hero, middle, bottom)\n run: |\n node << 'JSEOF'\n const { chromium } = require('playwright');\n (async () => {\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: 1920, height: 1080 },\n deviceScaleFactor: 2\n });\n const siteUrl = process.env.SITE_URL || 'https://odaeda.ru/';\n await page.goto(siteUrl, { waitUntil: 'networkidle', timeout: 60000 });\n await page.waitForTimeout(4000);\n // Hero\n await page.screenshot({ path: 'screenshot_hero.png' });\n // Middle\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.4));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_middle.png' });\n // Bottom\n await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight * 0.75));\n await page.waitForTimeout(1000);\n await page.screenshot({ path: 'screenshot_bottom.png' });\n console.log('Viewport screenshots saved');\n await browser.close();\n })();\n JSEOF\n env:\n SITE_URL: ${{ inputs.site_url }}\n\n - name: Setup Python\n uses: actions/setup-python@v5\n with:\n python-version: '3.12'\n\n - name: Checkout submodules workaround\n run: |\n git submodule deinit -f . 2>/dev/null || true\n rm -rf .git/modules 2>/dev/null || true\n git config --unset core.submodulesfile 2>/dev/null || true\n rm -f .gitmodules 2>/dev/null || true\n\n - name: Run External LLM Critics\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}\n OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\n SAMBANOVA_API_KEY: ${{ secrets.SAMBANOVA_API_KEY }}\n GOOGLE_AI_KEY: ${{ secrets.GOOGLE_AI_KEY }}\n DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}\n SITE_URL: ${{ inputs.site_url }}\n ITERATION: ${{ inputs.iteration }}\n run: |\n python3 << 'PYEOF'\n import json, urllib.request, urllib.error, base64, sys, os, time, concurrent.futures\n from pathlib import Path\n\n SITE_URL = os.environ.get(\"SITE_URL\", \"https://odaeda.ru/\")\n ITERATION = os.environ.get(\"ITERATION\", \"1\")\n\n # Read and base64-encode the FULL page screenshot\n def img_to_data_uri(path):\n with open(path, \"rb\") as f:\n return f\"data:image/png;base64,{base64.b64encode(f.read()).decode()}\"\n\n img_full = img_to_data_uri(\"screenshot_full.png\")\n img_hero = img_to_data_uri(\"screenshot_hero.png\")\n print(f\"Full screenshot: {len(img_full)} chars base64\")\n\n # ========== CRITIC PROMPTS ==========\n \n VISUAL_CRITIC_PROMPT = \"\"\"You are an Awwwards-level Visual Design Judge. You have judged 1000+ premium websites.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Hierarchy** (1-10): Is there clear typographic and visual hierarchy? Do headings, body text, and accents have proper contrast in size/weight/color?\n 2. **Color Palette & Harmony** (1-10): Is the color scheme cohesive, luxurious, and appropriate for premium catering? Are there any jarring colors?\n 3. **Typography** (1-10): Are fonts well-chosen for luxury branding? Is spacing, line-height, and font-pairing professional?\n 4. **Imagery & Visual Assets** (1-10): Are food/venue images high quality? Properly sized and positioned? Or are they placeholder/stock-looking?\n 5. **Spacing & Layout Rhythm** (1-10): Is whitespace used effectively? Is there consistent padding/margin rhythm?\n 6. **Micro-interactions & Polish** (1-10): Based on visual design, does the site look polished with extreme attention to detail?\n 7. **Overall Visual Impression** (1-10): First-impression wow factor. Would this site be nominated for an Awwwards SOTD?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_hierarchy\": 0.0, \"color_palette\": 0.0, \"typography\": 0.0, \"imagery\": 0.0, \"spacing_layout\": 0.0, \"micro_interactions\": 0.0, \"overall_visual\": 0.0, \"avg_visual\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for truly exceptional Awwwards-quality design. Most professional sites score 6-8.\"\"\"\n\n UX_CRITIC_PROMPT = \"\"\"You are a Senior UX/CRO Expert from Baymard Institute. You have conducted 500+ usability audits for luxury brands.\n \n I will provide you with multiple screenshots of a luxury catering website.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Navigation Clarity** (1-10): Is the main navigation intuitive? Can users find key sections (services, menu, pricing, contact) easily?\n 2. **Content Scannability** (1-10): Is text easy to scan? Are headings descriptive? Is content structured logically with clear sections?\n 3. **Call-to-Action Effectiveness** (1-10): Are CTAs prominent, clear, and action-oriented? Is there a clear conversion path from landing to contact?\n 4. **Trust Signals** (1-10): Are testimonials, stats, years of experience, guarantees visible? Do they build credibility for a premium service?\n 5. **Mobile-first Design** (1-10): Based on the desktop view, does the layout structure suggest good mobile adaptation?\n 6. **Information Architecture** (1-10): Is the page flow logical? Hero -> Social proof -> Services -> Process -> Menu -> Testimonials -> Gallery -> FAQ -> CTA?\n 7. **Overall UX Score** (1-10): Would a potential client easily understand the value proposition and know how to take action?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"navigation\": 0.0, \"scannability\": 0.0, \"cta_effectiveness\": 0.0, \"trust_signals\": 0.0, \"mobile_first\": 0.0, \"info_architecture\": 0.0, \"overall_ux\": 0.0, \"avg_ux\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely world-class UX. Most professional sites score 6-8.\"\"\"\n\n FRONTEND_CRITIC_PROMPT = \"\"\"You are a Principal Frontend Engineer & Web Performance Expert at a top agency. You review production websites daily.\n \n I will provide you with multiple screenshots of a luxury catering website built with Next.js 16, Tailwind CSS 4, Framer Motion.\n \n Evaluate these screenshots STRICTLY on these criteria (1-10 each, use decimals like 8.5):\n \n 1. **Visual Consistency** (1-10): Are all elements consistently styled? No layout shifts, broken alignment, or inconsistent spacing?\n 2. **Animation Quality** (1-10): Based on the visual design, do the layout and component choices suggest smooth, purposeful animations?\n 3. **Component Quality** (1-10): Do components look well-built (cards, buttons, forms, navigation, testimonials, gallery)?\n 4. **Responsive Design Hints** (1-10): Does the desktop layout suggest proper responsive breakpoints and mobile adaptation?\n 5. **Accessibility Basics** (1-10): Is there sufficient contrast (especially dark theme)? Are interactive elements clearly identifiable?\n 6. **Performance Indicators** (1-10): Does the design suggest good performance practices?\n 7. **Overall Frontend Quality** (1-10): Would you be proud to show this in your agency portfolio?\n \n RESPOND ONLY IN THIS EXACT JSON FORMAT (no markdown fences, no explanation outside JSON):\n {\"visual_consistency\": 0.0, \"animation_quality\": 0.0, \"component_quality\": 0.0, \"responsive_hints\": 0.0, \"accessibility\": 0.0, \"performance_indicators\": 0.0, \"overall_frontend\": 0.0, \"avg_frontend\": 0.0, \"verdict\": \"APPROVED\" or \"REDO\", \"critical_issues\": [\"top 3 issues\"], \"praise\": [\"top 2 strengths\"]}\n \n BE EXTREMELY HARSH. Score 9+ only for genuinely exceptional frontend work. Most professional sites score 6-8.\"\"\"\n\n # ========== API FUNCTIONS ==========\n \n def parse_json(s):\n if not s: return {}\n s = s.strip()\n if s.startswith(\"```\"):\n lines = s.split(\"\\n\")\n # Remove first and last lines (fences)\n s = \"\\n\".join(lines[1:])\n if s.rstrip().endswith(\"```\"):\n s = s.rstrip()[:-3]\n s = s.strip()\n if not s.startswith(\"{\"):\n i, j = s.find(\"{\"), s.rfind(\"}\")\n if 0 <= i < j: s = s[i:j+1]\n try: return json.loads(s)\n except:\n # Try fixing common issues\n s = s.replace('\\n', ' ').replace(' ', ' ')\n try: return json.loads(s)\n except: return {}\n\n def call_openai_vision(prompt, images, model=\"gpt-4o\"):\n \"\"\"OpenAI GPT-4o with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENAI_API_KEY\")\n if not key:\n print(\" [WARN] No OPENAI_API_KEY\")\n return None\n url = \"https://api.openai.com/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_groq_vision(prompt, images):\n \"\"\"Groq Llama 3.3 70B with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"GROQ_API_KEY\")\n if not key:\n print(\" [WARN] No GROQ_API_KEY\")\n return None\n url = \"https://api.groq.com/openai/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": \"llama-3.3-70b-versatile\",\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_openrouter_vision(prompt, images, model=\"google/gemini-2.5-flash-preview:thinking\"):\n \"\"\"OpenRouter (Gemini) with vision - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"OPENROUTER_API_KEY\")\n if not key:\n print(\" [WARN] No OPENROUTER_API_KEY\")\n return None\n url = \"https://openrouter.ai/api/v1/chat/completions\"\n content = [{\"type\": \"text\", \"text\": prompt}]\n for img_uri in images:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": img_uri, \"detail\": \"high\"}})\n payload = json.dumps({\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": content}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": SITE_URL, \"X-Title\": \"Nilov Catering Site Critic\"})\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n\n def call_deepseek_text(prompt):\n \"\"\"DeepSeek V3 text-only - TRULY EXTERNAL\"\"\"\n key = os.environ.get(\"DEEPSEEK_API_KEY\")\n if not key:\n print(\" [WARN] No DEEPSEEK_API_KEY\")\n return None\n url = \"https://api.deepseek.com/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details you should know:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Based on these details and your expertise, evaluate the design as if you were reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] DeepSeek: {e}\")\n return None\n\n def call_sambanova_text(prompt):\n \"\"\"SambaNova Llama 3.3 70B text-only - TRULLY EXTERNAL\"\"\"\n key = os.environ.get(\"SAMBANOVA_API_KEY\")\n if not key:\n print(\" [WARN] No SAMBANOVA_API_KEY\")\n return None\n url = \"https://api.sambanova.ai/v1/chat/completions\"\n text_prompt = prompt + \"\"\"\n \n IMPORTANT: I cannot provide images to you. This is a full-page screenshot of a light-theme (drinkit) luxury catering website (odaeda.ru).\n \n Technical details:\n - Светлая тема (дринкит): ivory #FAF7F2 background, ink #1C1815 text, gold #B08D57 акцент, burgundy #722F37 вторичный\n - Fonts: Cormorant Garamond (headings) + Inter (body)\n - Sections: Preloader → Parallax Hero → Social Proof Bar → Services (Bento Grid) → Process Timeline → Menu Preview → Testimonials (Carousel) → Gallery (Masonry) → FAQ → Contact Form (Glass-morphism) → Footer\n - Effects: Custom cursor with lerp, Lenis smooth scroll, text reveal animations, parallax images, magnetic buttons, noise overlay\n - Built with: Next.js 16, Tailwind CSS 4, Framer Motion\n \n Evaluate based on these details as if reviewing the actual visual output.\"\"\"\n payload = json.dumps({\n \"model\": \"Meta-Llama-3.3-70B-Instruct\",\n \"messages\": [{\"role\": \"user\", \"content\": text_prompt[:10000]}],\n \"max_tokens\": 1200,\n \"temperature\": 0.3\n }).encode()\n req = urllib.request.Request(url, data=payload,\n headers={\"Authorization\": f\"Bearer {key}\", \"Content-Type\": \"application/json\"})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n return json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n except Exception as e:\n print(f\" [ERROR] SambaNova: {e}\")\n return None\n\n # ========== CRITIC ASSIGNMENTS ==========\n # Vision models see actual screenshots, text models get detailed descriptions\n vision_images = [img_hero, img_middle, img_bottom] # 3 viewport shots for vision models\n \n CRITICS = [\n (\"GPT-4o (OpenAI)\", \"Visual Design (Awwwards)\", VISUAL_CRITIC_PROMPT, \n lambda p: call_openai_vision(p, [img_full] + vision_images)),\n (\"Llama-3.3-70B (Groq)\", \"UX/CRO (Baymard)\", UX_CRITIC_PROMPT,\n lambda p: call_groq_vision(p, vision_images)),\n (\"Gemini-2.5-Flash (OpenRouter)\", \"Frontend/Perf\", FRONTEND_CRITIC_PROMPT,\n lambda p: call_openrouter_vision(p, vision_images)),\n (\"DeepSeek-V3 (DeepSeek)\", \"UX/CRO #2 (text)\", UX_CRITIC_PROMPT,\n lambda p: call_deepseek_text(p)),\n (\"Llama-3.3-70B (SambaNova)\", \"Visual #2 (text)\", VISUAL_CRITIC_PROMPT,\n lambda p: call_sambanova_text(p)),\n ]\n\n # ========== RUN ALL CRITICS ==========\n \n results = {}\n all_issues = []\n all_praise = []\n \n def run_critic(name, role, prompt, fn):\n print(f\"\\n{'='*60}\")\n print(f\"Running: {name} ({role})...\")\n sys.stdout.flush()\n try:\n raw = fn(prompt)\n if raw:\n print(f\" Raw response length: {len(raw)}\")\n parsed = parse_json(raw)\n if not parsed:\n print(f\" [WARN] Failed to parse JSON from response\")\n print(f\" First 200 chars: {raw[:200]}\")\n return\n results[f\"{name}|{role}\"] = parsed\n avg = parsed.get(\"avg_visual\", parsed.get(\"avg_ux\", parsed.get(\"avg_frontend\", 0)))\n print(f\" Score: {avg}/10\")\n print(f\" Verdict: {parsed.get('verdict', 'UNKNOWN')}\")\n for issue in parsed.get(\"critical_issues\", [])[:5]:\n all_issues.append({\"critic\": name, \"role\": role, \"issue\": issue})\n print(f\" Issue: {issue}\")\n for p in parsed.get(\"praise\", [])[:3]:\n all_praise.append({\"critic\": name, \"role\": role, \"praise\": p})\n print(f\" Praise: {p}\")\n else:\n print(f\" FAILED: No response from API\")\n except Exception as e:\n print(f\" ERROR: {type(e).__name__}: {e}\")\n sys.stdout.flush()\n\n print(\"=\" * 60)\n print(\"EXTERNAL LLM CRITIC EVALUATION\")\n print(\"=\" * 60)\n print(f\"Site: {SITE_URL}\")\n print(f\"Iteration: {ITERATION}\")\n print(f\"Vision critics: GPT-4o, Groq Llama-3.3-70B, Gemini-2.5-Flash\")\n print(f\"Text critics: DeepSeek-V3, SambaNova Llama-3.3-70B\")\n print(f\"ALL critics are EXTERNAL LLM APIs, NOT the site builder\")\n sys.stdout.flush()\n \n # Run critics sequentially to avoid API rate limits\n for name, role, prompt, fn in CRITICS:\n run_critic(name, role, prompt, fn)\n\n # ========== COMPILE RESULTS ==========\n \n all_scores = []\n score_details = []\n for key, val in results.items():\n for field in [\"avg_visual\", \"avg_ux\", \"avg_frontend\"]:\n if field in val:\n s = val[field]\n all_scores.append(s)\n score_details.append({\"critic\": key, \"type\": field, \"score\": s})\n \n overall_avg = round(sum(all_scores) / len(all_scores), 2) if all_scores else 0\n min_score = min(all_scores) if all_scores else 0\n max_score = max(all_scores) if all_scores else 0\n \n final = {\n \"iteration\": int(ITERATION),\n \"site_url\": SITE_URL,\n \"timestamp\": time.strftime(\"%Y-%m-%d %H:%M:%S UTC\"),\n \"evaluation_type\": \"EXTERNAL_LLM_CRITICS\",\n \"note\": \"All scores from independent external LLM APIs (OpenAI GPT-4o, Groq Llama-3.3-70B, OpenRouter Gemini, DeepSeek-V3, SambaNova Llama-3.3-70B). NOT self-evaluation.\",\n \"overall_average\": overall_avg,\n \"min_score\": min_score,\n \"max_score\": max_score,\n \"all_individual_scores\": all_scores,\n \"score_details\": score_details,\n \"verdict\": \"APPROVED\" if overall_avg >= 9.0 else \"REDO\",\n \"critics\": {k: v for k, v in results.items()},\n \"all_critical_issues\": all_issues,\n \"all_praise\": all_praise,\n \"n_critics_responded\": len(results),\n \"n_critics_total\": len(CRITICS),\n \"summary\": f\"Average: {overall_avg}/10 | Min: {min_score} | Max: {max_score} | Critics: {len(results)}/{len(CRITICS)} | Verdict: {'APPROVED - ALL CRITICS >9' if overall_avg >= 9.0 else 'REDO - needs improvements'}\"\n }\n \n with open(\"critic_results.json\", \"w\") as f:\n json.dump(final, f, indent=2, ensure_ascii=False)\n \n print(f\"\\n{'='*60}\")\n print(f\"FINAL RESULTS - Iteration {ITERATION}\")\n print(f\"{'='*60}\")\n print(f\"Overall Average: {overall_avg}/10\")\n print(f\"Min Score: {min_score}/10\")\n print(f\"Max Score: {max_score}/10\")\n print(f\"Individual Scores: {[f'{s}/10' for s in all_scores]}\")\n print(f\"Verdict: {final['verdict']}\")\n print(f\"\\nALL CRITICAL ISSUES ({len(all_issues)} total):\")\n for item in all_issues:\n print(f\" [{item['critic']}] {item['issue']}\")\n print(f\"\\nPRAISE ({len(all_praise)} total):\")\n for item in all_praise:\n print(f\" [{item['critic']}] {item['praise']}\")\n print(f\"\\nResults saved to critic_results.json\")\n sys.stdout.flush()\n PYEOF\n\n - name: Save results\n run: |\n mkdir -p critic-results\n cp critic_results.json critic-results/iteration_${{ inputs.iteration }}.json\n cp critic_results.json critic-results/latest.json\n echo \"=== SUMMARY ===\"\n python3 -c \"\n import json\n with open('critic_results.json') as f:\n d = json.load(f)\n print(f'OVERALL: {d[\\\"overall_average\\\"]}/10')\n print(f'MIN: {d[\\\"min_score\\\"]}/10 | MAX: {d[\\\"max_score\\\"]}/10')\n print(f'VERDICT: {d[\\\"verdict\\\"]}')\n print(f'CRITICS: {d[\\\"n_critics_responded\\\"]}/{d[\\\"n_critics_total\\\"]}')\n print()\n print('SCORES:')\n for s in d['score_details']:\n print(f' {s[\\\"critic\\\"]}: {s[\\\"score\\\"]}/10 ({s[\\\"type\\\"]})')\n print()\n print('ISSUES:')\n for item in d['all_critical_issues']:\n print(f' [{item[\\\"critic\\\"]}] {item[\\\"issue\\\"]}')\n \"\n\n - name: Commit results to repo\n run: |\n git config user.name \"External Critic Bot\"\n git config user.email \"critic-bot@external-llm.local\"\n git add critic-results/\n ITER=${{ inputs.iteration }}\n AVG=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['overall_average'])\" 2>/dev/null || echo \"0\")\n VERDICT=$(python3 -c \"import json; print(json.load(open('critic_results.json'))['verdict'])\" 2>/dev/null || echo \"UNKNOWN\")\n git commit -m \"Critic eval iter ${ITER}: ${VERDICT} (avg ${AVG}/10) [external LLMs]\" || echo \"No changes to commit\"\n git push || echo \"Push failed (may need force)\"\n\n - name: Upload screenshots as artifacts\n uses: actions/upload-artifact@v4\n with:\n name: screenshots-iter-${{ inputs.iteration }}\n path: |\n screenshot_full.png\n screenshot_hero.png\n screenshot_middle.png\n screenshot_bottom.png\n retention-days: 7"}}}}