File: backend/config.py

# Useful for debugging purposes when you don't want to waste GPT4-Vision credits
# Setting to True will stream a mock response instead of calling the OpenAI API
# TODO: Should only be set to true when value is 'True', not any abitrary truthy value
import os

NUM_VARIANTS = 2

# LLM-related
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", None)
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", None)
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", None)

# Image generation (optional)
REPLICATE_API_KEY = os.environ.get("REPLICATE_API_KEY", None)

# Debugging-related

SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False))
IS_DEBUG_ENABLED = bool(os.environ.get("IS_DEBUG_ENABLED", False))
DEBUG_DIR = os.environ.get("DEBUG_DIR", "")

# Set to True when running in production (on the hosted version)
# Used as a feature flag to enable or disable certain features
IS_PROD = os.environ.get("IS_PROD", False)



File: backend/test_llm.py

import unittest
from llm import convert_frontend_str_to_llm, Llm


class TestConvertFrontendStrToLlm(unittest.TestCase):
    def test_convert_valid_strings(self):
        self.assertEqual(
            convert_frontend_str_to_llm("gpt_4_vision"),
            Llm.GPT_4_VISION,
            "Should convert 'gpt_4_vision' to Llm.GPT_4_VISION",
        )
        self.assertEqual(
            convert_frontend_str_to_llm("claude_3_sonnet"),
            Llm.CLAUDE_3_SONNET,
            "Should convert 'claude_3_sonnet' to Llm.CLAUDE_3_SONNET",
        )
        self.assertEqual(
            convert_frontend_str_to_llm("claude-3-opus-20240229"),
            Llm.CLAUDE_3_OPUS,
            "Should convert 'claude-3-opus-20240229' to Llm.CLAUDE_3_OPUS",
        )
        self.assertEqual(
            convert_frontend_str_to_llm("gpt-4-turbo-2024-04-09"),
            Llm.GPT_4_TURBO_2024_04_09,
            "Should convert 'gpt-4-turbo-2024-04-09' to Llm.GPT_4_TURBO_2024_04_09",
        )
        self.assertEqual(
            convert_frontend_str_to_llm("gpt-4o-2024-05-13"),
            Llm.GPT_4O_2024_05_13,
            "Should convert 'gpt-4o-2024-05-13' to Llm.GPT_4O_2024_05_13",
        )

    def test_convert_invalid_string_raises_exception(self):
        with self.assertRaises(ValueError):
            convert_frontend_str_to_llm("invalid_string")
        with self.assertRaises(ValueError):
            convert_frontend_str_to_llm("another_invalid_string")


if __name__ == "__main__":
    unittest.main()



File: backend/mock_llm.py

import asyncio
from typing import Awaitable, Callable

from custom_types import InputMode


STREAM_CHUNK_SIZE = 20


async def mock_completion(
    process_chunk: Callable[[str, int], Awaitable[None]], input_mode: InputMode
) -> str:
    code_to_return = (
        TALLY_FORM_VIDEO_PROMPT_MOCK
        if input_mode == "video"
        else NO_IMAGES_NYTIMES_MOCK_CODE
    )

    for i in range(0, len(code_to_return), STREAM_CHUNK_SIZE):
        await process_chunk(code_to_return[i : i + STREAM_CHUNK_SIZE], 0)
        await asyncio.sleep(0.01)

    if input_mode == "video":
        # Extract the last <html></html> block from code_to_return
        # because we can have multiple passes
        start = code_to_return.rfind("<html")
        end = code_to_return.rfind("</html>") + len("</html>")
        if start != -1 and end != -1:
            code_to_return = code_to_return[start:end]
        else:
            code_to_return = "Error: HTML block not found."

    return code_to_return


APPLE_MOCK_CODE = """<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Product Showcase</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Roboto', sans-serif;
        }
    </style>
</head>
<body class="bg-black text-white">
    <nav class="py-6">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center">
            <div class="flex items-center">
                <img src="https://placehold.co/24x24" alt="Company Logo" class="mr-8">
                <a href="#" class="text-white text-sm font-medium mr-4">Store</a>
                <a href="#" class="text-white text-sm font-medium mr-4">Mac</a>
                <a href="#" class="text-white text-sm font-medium mr-4">iPad</a>
                <a href="#" class="text-white text-sm font-medium mr-4">iPhone</a>
                <a href="#" class="text-white text-sm font-medium mr-4">Watch</a>
                <a href="#" class="text-white text-sm font-medium mr-4">Vision</a>
                <a href="#" class="text-white text-sm font-medium mr-4">AirPods</a>
                <a href="#" class="text-white text-sm font-medium mr-4">TV & Home</a>
                <a href="#" class="text-white text-sm font-medium mr-4">Entertainment</a>
                <a href="#" class="text-white text-sm font-medium mr-4">Accessories</a>
                <a href="#" class="text-white text-sm font-medium">Support</a>
            </div>
            <div class="flex items-center">
                <a href="#" class="text-white text-sm font-medium mr-4"><i class="fas fa-search"></i></a>
                <a href="#" class="text-white text-sm font-medium"><i class="fas fa-shopping-bag"></i></a>
            </div>
        </div>
    </nav>

    <main class="mt-8">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="text-center">
                <img src="https://placehold.co/100x100" alt="Brand Logo" class="mx-auto mb-4">
                <h1 class="text-5xl font-bold mb-4">WATCH SERIES 9</h1>
                <p class="text-2xl font-medium mb-8">Smarter. Brighter. Mightier.</p>
                <div class="flex justify-center space-x-4">
                    <a href="#" class="text-blue-600 text-sm font-medium">Learn more ></a>
                    <a href="#" class="text-blue-600 text-sm font-medium">Buy ></a>
                </div>
            </div>
            <div class="flex justify-center mt-12">
                <img src="https://placehold.co/500x300" alt="Product image of a smartwatch with a pink band and a circular interface displaying various health metrics." class="mr-8">
                <img src="https://placehold.co/500x300" alt="Product image of a smartwatch with a blue band and a square interface showing a classic analog clock face." class="ml-8">
            </div>
        </div>
    </main>
</body>
</html>"""

NYTIMES_MOCK_CODE = """
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>The New York Times - News</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;400;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
    <style>
        body {
            font-family: 'Libre Franklin', sans-serif;
        }
    </style>
</head>
<body class="bg-gray-100">
    <div class="container mx-auto px-4">
        <header class="border-b border-gray-300 py-4">
            <div class="flex justify-between items-center">
                <div class="flex items-center space-x-4">
                    <button class="text-gray-700"><i class="fas fa-bars"></i></button>
                    <button class="text-gray-700"><i class="fas fa-search"></i></button>
                    <div class="text-xs uppercase tracking-widest">Tuesday, November 14, 2023<br>Today's Paper</div>
                </div>
                <div>
                    <img src="https://placehold.co/200x50?text=The+New+York+Times+Logo" alt="The New York Times Logo" class="h-8">
                </div>
                <div class="flex items-center space-x-4">
                    <button class="bg-black text-white px-4 py-1 text-xs uppercase tracking-widest">Give the times</button>
                    <div class="text-xs">Account</div>
                </div>
            </div>
            <nav class="flex justify-between items-center py-4">
                <div class="flex space-x-4">
                    <a href="#" class="text-xs uppercase tracking-widest text-gray-700">U.S.</a>
                    <!-- Add other navigation links as needed -->
                </div>
                <div class="flex space-x-4">
                    <a href="#" class="text-xs uppercase tracking-widest text-gray-700">Cooking</a>
                    <!-- Add other navigation links as needed -->
                </div>
            </nav>
        </header>
        <main>
            <section class="py-6">
                <div class="grid grid-cols-3 gap-4">
                    <div class="col-span-2">
                        <article class="mb-4">
                            <h2 class="text-xl font-bold mb-2">Israeli Military Raids Gaza’s Largest Hospital</h2>
                            <p class="text-gray-700 mb-2">Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.</p>
                            <a href="#" class="text-blue-600 text-sm">See more updates <i class="fas fa-external-link-alt"></i></a>
                        </article>
                        <!-- Repeat for each news item -->
                    </div>
                    <div class="col-span-1">
                        <article class="mb-4">
                            <img src="https://placehold.co/300x200?text=News+Image" alt="Flares and plumes of smoke over the northern Gaza skyline on Tuesday." class="mb-2">
                            <h2 class="text-xl font-bold mb-2">From Elvis to Elopements, the Evolution of the Las Vegas Wedding</h2>
                            <p class="text-gray-700 mb-2">The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.</p>
                            <a href="#" class="text-blue-600 text-sm">8 MIN READ</a>
                        </article>
                        <!-- Repeat for each news item -->
                    </div>
                </div>
            </section>
        </main>
    </div>
</body>
</html>
"""

NO_IMAGES_NYTIMES_MOCK_CODE = """
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>The New York Times - News</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;400;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
    <style>
        body {
            font-family: 'Libre Franklin', sans-serif;
        }
    </style>
</head>
<body class="bg-gray-100">
    <div class="container mx-auto px-4">
        <header class="border-b border-gray-300 py-4">
            <div class="flex justify-between items-center">
                <div class="flex items-center space-x-4">
                    <button class="text-gray-700"><i class="fas fa-bars"></i></button>
                    <button class="text-gray-700"><i class="fas fa-search"></i></button>
                    <div class="text-xs uppercase tracking-widest">Tuesday, November 14, 2023<br>Today's Paper</div>
                </div>
                <div class="flex items-center space-x-4">
                    <button class="bg-black text-white px-4 py-1 text-xs uppercase tracking-widest">Give the times</button>
                    <div class="text-xs">Account</div>
                </div>
            </div>
            <nav class="flex justify-between items-center py-4">
                <div class="flex space-x-4">
                    <a href="#" class="text-xs uppercase tracking-widest text-gray-700">U.S.</a>
                    <!-- Add other navigation links as needed -->
                </div>
                <div class="flex space-x-4">
                    <a href="#" class="text-xs uppercase tracking-widest text-gray-700">Cooking</a>
                    <!-- Add other navigation links as needed -->
                </div>
            </nav>
        </header>
        <main>
            <section class="py-6">
                <div class="grid grid-cols-3 gap-4">
                    <div class="col-span-2">
                        <article class="mb-4">
                            <h2 class="text-xl font-bold mb-2">Israeli Military Raids Gaza’s Largest Hospital</h2>
                            <p class="text-gray-700 mb-2">Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.</p>
                            <a href="#" class="text-blue-600 text-sm">See more updates <i class="fas fa-external-link-alt"></i></a>
                        </article>
                        <!-- Repeat for each news item -->
                    </div>
                    <div class="col-span-1">
                        <article class="mb-4">
                            <h2 class="text-xl font-bold mb-2">From Elvis to Elopements, the Evolution of the Las Vegas Wedding</h2>
                            <p class="text-gray-700 mb-2">The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.</p>
                            <a href="#" class="text-blue-600 text-sm">8 MIN READ</a>
                        </article>
                        <!-- Repeat for each news item -->
                    </div>
                </div>
            </section>
        </main>
    </div>
</body>
</html>
"""

MORTGAGE_CALCULATOR_VIDEO_PROMPT_MOCK = """
<thinking>
The user flow in the video seems to be:
1. The calculator starts with some default values for loan amount, loan term, interest rate, etc. 
2. The user toggles the "Include taxes & fees" checkbox which shows an explanation tooltip.
3. The user selects different loan terms from the dropdown, which updates the monthly payment amount.
4. The user enters a custom loan amount.
5. The user selects a different loan term (30-yr fixed FHA).
6. The user enters additional details like home price, down payment, state, credit score, property tax, home insurance, and HOA fees.
7. The calculator updates the total monthly payment breakdown.

To build this:
- Use a container div for the whole calculator
- Have sections for Monthly Payment, Purchase Budget, loan details, additional costs
- Use input fields, dropdowns, and checkboxes for user input
- Update values dynamically using JavaScript when inputs change
- Show/hide explanation tooltip when checkbox is toggled
- Update monthly payment whenever loan amount, interest rate or term is changed
- Allow selecting loan term from a dropdown 
- Update total monthly payment breakdown as additional costs are entered
- Style everything to match the screenshots using Tailwind utility classes
</thinking>

<html>
<head>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
</head>
<body>
<div class="max-w-lg mx-auto bg-white shadow-lg rounded-lg p-6 font-sans">
  <div class="text-2xl font-semibold mb-6">Mortgage Calculator</div>
  
  <div class="flex justify-between text-sm uppercase font-semibold text-gray-500 border-b mb-4">
    <div class="pb-2 border-b-2 border-blue-500">Monthly payment</div>
    <div class="pb-2">Purchase budget</div>
  </div>

  <div class="flex items-center mb-4">
    <input type="checkbox" class="mr-2 taxes-toggle" id="taxesToggle">
    <label for="taxesToggle" class="text-sm">Include taxes & fees</label>
    <i class="fas fa-info-circle ml-1 text-gray-400 cursor-pointer" id="taxesInfo"></i>
    <div class="hidden bg-gray-100 text-xs p-2 ml-2 rounded" id="taxesTooltip">Your total monthly payment is more than just your mortgage. It can include property taxes, homeowners insurance, and HOA fees, among other things.</div>
  </div>

  <div class="text-3xl font-semibold mb-4">$<span id="monthlyPayment">1,696</span></div>

  <div class="mb-4">
    <div class="text-sm font-semibold mb-2">Loan amount</div>
    <input type="text" value="240,000" class="w-full border rounded px-2 py-1 text-lg" id="loanAmount">
  </div>

  <div class="mb-4">
    <div class="text-sm font-semibold mb-2">Loan term</div>
    <select class="w-full border rounded px-2 py-1 text-lg" id="loanTerm">
      <option>30-yr fixed</option>
      <option>30-yr fixed FHA</option>
      <option>30-yr fixed VA</option>
      <option>30-yr fixed USDA</option>
      <option>20-yr fixed</option>
      <option>15-yr fixed</option>
      <option>10 / 6 ARM</option>
      <option>7 / 6 ARM</option>
      <option>5 / 6 ARM</option>
      <option>3 / 6 ARM</option>
    </select>
  </div>

  <div class="flex justify-between mb-4">
    <div>
      <div class="text-sm font-semibold mb-2">Interest</div>
      <div class="text-lg">7.61 %</div>
    </div>
    <i class="fas fa-info-circle text-gray-400 cursor-pointer self-center"></i>
  </div>

  <div class="border-t pt-4">
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Home price</div>
      <input type="text" value="300,000" class="w-full border rounded px-2 py-1 text-lg" id="homePrice">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Down payment</div>
      <div class="flex">
        <input type="text" value="60,000" class="w-full border rounded-l px-2 py-1 text-lg" id="downPayment">
        <div class="bg-gray-200 rounded-r px-4 py-1 text-lg flex items-center">20 %</div>
      </div>
    </div>
  </div>

  <div class="border-t pt-4">
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">State</div>
      <select class="w-full border rounded px-2 py-1 text-lg" id="state">
        <option>New York</option>
      </select>
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Credit score</div>
      <select class="w-full border rounded px-2 py-1 text-lg" id="creditScore">
        <option>700 - 719</option>
      </select>
    </div>
  </div>

  <div class="border-t pt-4">
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Property tax (yearly)</div>
      <input type="text" value="3,750" class="w-full border rounded px-2 py-1 text-lg" id="propertyTax">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Home insurance (yearly)</div>
      <input type="text" value="1,050" class="w-full border rounded px-2 py-1 text-lg" id="homeInsurance">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Private mortgage insurance (monthly)</div>
      <input type="text" value="0" class="w-full border rounded px-2 py-1 text-lg" id="mortgageInsurance">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Homeowners association (monthly)</div>
      <input type="text" value="0" class="w-full border rounded px-2 py-1 text-lg" id="hoaFees">
    </div>
  </div>

  <div class="flex justify-between pt-4">
    <div class="text-sm">
      <div class="font-semibold">Total monthly payment</div>
      <div class="text-3xl font-semibold">$<span id="totalPayment">2,036</span></div>
    </div>
    <div class="text-right">
      <div class="text-sm">Loan</div>
      <div class="text-lg">$<span id="loanPayment">1,635</span></div>
      <div class="text-sm">Taxes & fees</div>
      <div>$<span id="taxesPayment">401</span></div>
    </div>
  </div>

  <div class="text-xs text-gray-500 mt-4">
    <div>Disclaimer • Feedback</div>
  </div>
</div>

<script>
$(document).ready(function() {
  var loanAmount = 240000;
  var interestRate = 7.61;
  var loanTerm = 30;
  var homePrice = 300000;
  var downPayment = 60000;
  var propertyTax = 3750;
  var homeInsurance = 1050;
  var mortgageInsurance = 0;
  var hoaFees = 0;

  function updateCalculations() {
    var principal = loanAmount;
    var monthlyInterest = interestRate / 100 / 12;
    var numPayments = loanTerm * 12;
    var monthlyPayment = principal * monthlyInterest / (1 - (Math.pow(1/(1 + monthlyInterest), numPayments)));
    var totalPayment = monthlyPayment;
    var taxesPayment = (propertyTax + homeInsurance) / 12;

    if ($('#taxesToggle').is(':checked')) {
      totalPayment += taxesPayment + mortgageInsurance + hoaFees;
    }

    $('#monthlyPayment').text(Math.round(monthlyPayment).toLocaleString());
    $('#loanPayment').text(Math.round(monthlyPayment).toLocaleString());
    $('#taxesPayment').text(Math.round(taxesPayment + mortgageInsurance + hoaFees).toLocaleString());
    $('#totalPayment').text(Math.round(totalPayment).toLocaleString());
  }

  $('#taxesInfo').hover(function() {
    $('#taxesTooltip').removeClass('hidden');
  }, function() {
    $('#taxesTooltip').addClass('hidden');
  });

  $('#loanTerm').change(function() {
    loanTerm = parseInt($(this).val().split('-')[0]);
    updateCalculations();
  });

  $('#loanAmount').change(function() {
    loanAmount = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#homePrice').change(function() {
    homePrice = parseInt($(this).val().replace(/,/g, ''));
    loanAmount = homePrice - downPayment;
    $('#loanAmount').val(loanAmount.toLocaleString());
    updateCalculations();
  });

  $('#downPayment').change(function() {
    downPayment = parseInt($(this).val().replace(/,/g, ''));
    loanAmount = homePrice - downPayment;
    $('#loanAmount').val(loanAmount.toLocaleString());
    updateCalculations();
  });

  $('#propertyTax').change(function() {
    propertyTax = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#homeInsurance').change(function() {
    homeInsurance = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#mortgageInsurance').change(function() {
    mortgageInsurance = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#hoaFees').change(function() {
    hoaFees = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  updateCalculations();
});
</script>
</body>
</html>

<thinking>
More things to consider:
- Add validation to input fields to prevent non-numeric values
- Add a reset button to clear all inputs and reset to default values
</thinking>

<html>
<head>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
</head>
<body>
<div class="max-w-lg mx-auto bg-black text-white shadow-lg rounded-lg p-6 font-sans">
  <div class="text-2xl font-semibold mb-6">Mortgage Calculator</div>
  
  <div class="flex justify-between text-sm uppercase font-semibold text-gray-500 border-b mb-4">
    <div class="pb-2 border-b-2 border-blue-500">Monthly payment</div>
    <div class="pb-2">Purchase budget</div>
  </div>

  <div class="flex items-center mb-4">
    <input type="checkbox" class="mr-2 taxes-toggle" id="taxesToggle">
    <label for="taxesToggle" class="text-sm">Include taxes & fees</label>
    <i class="fas fa-info-circle ml-1 text-gray-400 cursor-pointer" id="taxesInfo"></i>
    <div class="hidden bg-gray-100 text-xs p-2 ml-2 rounded" id="taxesTooltip">Your total monthly payment is more than just your mortgage. It can include property taxes, homeowners insurance, and HOA fees, among other things.</div>
  </div>

  <div class="text-3xl font-semibold mb-4">$<span id="monthlyPayment">1,696</span></div>

  <div class="mb-4">
    <div class="text-sm font-semibold mb-2">Loan amount</div>
    <input type="text" value="240,000" class="w-full border rounded px-2 py-1 text-lg" id="loanAmount">
  </div>

  <div class="mb-4">
    <div class="text-sm font-semibold mb-2">Loan term</div>
    <select class="w-full border rounded px-2 py-1 text-lg" id="loanTerm">
      <option>30-yr fixed</option>
      <option>30-yr fixed FHA</option>
      <option>30-yr fixed VA</option>
      <option>30-yr fixed USDA</option>
      <option>20-yr fixed</option>
      <option>15-yr fixed</option>
      <option>10 / 6 ARM</option>
      <option>7 / 6 ARM</option>
      <option>5 / 6 ARM</option>
      <option>3 / 6 ARM</option>
    </select>
  </div>

  <div class="flex justify-between mb-4">
    <div>
      <div class="text-sm font-semibold mb-2">Interest</div>
      <div class="text-lg">7.61 %</div>
    </div>
    <i class="fas fa-info-circle text-gray-400 cursor-pointer self-center"></i>
  </div>

  <div class="border-t pt-4">
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Home price</div>
      <input type="text" value="300,000" class="w-full border rounded px-2 py-1 text-lg" id="homePrice">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Down payment</div>
      <div class="flex">
        <input type="text" value="60,000" class="w-full border rounded-l px-2 py-1 text-lg" id="downPayment">
        <div class="bg-gray-200 rounded-r px-4 py-1 text-lg flex items-center">20 %</div>
      </div>
    </div>
  </div>

  <div class="border-t pt-4">
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">State</div>
      <select class="w-full border rounded px-2 py-1 text-lg" id="state">
        <option>New York</option>
      </select>
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Credit score</div>
      <select class="w-full border rounded px-2 py-1 text-lg" id="creditScore">
        <option>700 - 719</option>
      </select>
    </div>
  </div>

  <div class="border-t pt-4">
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Property tax (yearly)</div>
      <input type="text" value="3,750" class="w-full border rounded px-2 py-1 text-lg" id="propertyTax">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Home insurance (yearly)</div>
      <input type="text" value="1,050" class="w-full border rounded px-2 py-1 text-lg" id="homeInsurance">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Private mortgage insurance (monthly)</div>
      <input type="text" value="0" class="w-full border rounded px-2 py-1 text-lg" id="mortgageInsurance">
    </div>
    <div class="mb-4">
      <div class="text-sm font-semibold mb-2">Homeowners association (monthly)</div>
      <input type="text" value="0" class="w-full border rounded px-2 py-1 text-lg" id="hoaFees">
    </div>
  </div>

  <div class="flex justify-between pt-4">
    <div class="text-sm">
      <div class="font-semibold">Total monthly payment</div>
      <div class="text-3xl font-semibold">$<span id="totalPayment">2,036</span></div>
    </div>
    <div class="text-right">
      <div class="text-sm">Loan</div>
      <div class="text-lg">$<span id="loanPayment">1,635</span></div>
      <div class="text-sm">Taxes & fees</div>
      <div>$<span id="taxesPayment">401</span></div>
    </div>
  </div>

  <div class="text-xs text-gray-500 mt-4">
    <div>Disclaimer • Feedback</div>
  </div>
</div>

<script>
$(document).ready(function() {
  var loanAmount = 240000;
  var interestRate = 7.61;
  var loanTerm = 30;
  var homePrice = 300000;
  var downPayment = 60000;
  var propertyTax = 3750;
  var homeInsurance = 1050;
  var mortgageInsurance = 0;
  var hoaFees = 0;

  function updateCalculations() {
    var principal = loanAmount;
    var monthlyInterest = interestRate / 100 / 12;
    var numPayments = loanTerm * 12;
    var monthlyPayment = principal * monthlyInterest / (1 - (Math.pow(1/(1 + monthlyInterest), numPayments)));
    var totalPayment = monthlyPayment;
    var taxesPayment = (propertyTax + homeInsurance) / 12;

    if ($('#taxesToggle').is(':checked')) {
      totalPayment += taxesPayment + mortgageInsurance + hoaFees;
    }

    $('#monthlyPayment').text(Math.round(monthlyPayment).toLocaleString());
    $('#loanPayment').text(Math.round(monthlyPayment).toLocaleString());
    $('#taxesPayment').text(Math.round(taxesPayment + mortgageInsurance + hoaFees).toLocaleString());
    $('#totalPayment').text(Math.round(totalPayment).toLocaleString());
  }

  $('#taxesInfo').hover(function() {
    $('#taxesTooltip').removeClass('hidden');
  }, function() {
    $('#taxesTooltip').addClass('hidden');
  });

  $('#loanTerm').change(function() {
    loanTerm = parseInt($(this).val().split('-')[0]);
    updateCalculations();
  });

  $('#loanAmount').change(function() {
    loanAmount = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#homePrice').change(function() {
    homePrice = parseInt($(this).val().replace(/,/g, ''));
    loanAmount = homePrice - downPayment;
    $('#loanAmount').val(loanAmount.toLocaleString());
    updateCalculations();
  });

  $('#downPayment').change(function() {
    downPayment = parseInt($(this).val().replace(/,/g, ''));
    loanAmount = homePrice - downPayment;
    $('#loanAmount').val(loanAmount.toLocaleString());
    updateCalculations();
  });

  $('#propertyTax').change(function() {
    propertyTax = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#homeInsurance').change(function() {
    homeInsurance = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#mortgageInsurance').change(function() {
    mortgageInsurance = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  $('#hoaFees').change(function() {
    hoaFees = parseInt($(this).val().replace(/,/g, ''));
    updateCalculations();
  });

  updateCalculations();
});
</script>
</body>
</html>

"""

GOOGLE_FORM_VIDEO_PROMPT_MOCK = """

<thinking>User flow:
1. User starts on the Google search page and types in "times" in the search bar
2. As the user types, Google provides autocomplete suggestions related to "times"
3. User selects the "times" suggestion from the autocomplete dropdown
4. The search results page for "times" loads, showing various results related to The New York Times newspaper
5. User clicks the "Generate" button under "Get an AI-powered overview for this search?"
6. An AI-generated overview about The New York Times loads on the right side of the search results

Code structure:
- HTML structure with header, search bar, autocomplete dropdown, search button
- Search results area to display search results
- Sidebar area to display the AI-generated overview
- Use Tailwind CSS utility classes for styling
- Use jQuery to handle user interactions:
  - Typing in search bar to show/filter autocomplete suggestions
  - Selecting autocomplete suggestion to populate search bar
  - Clicking search button to display search results
  - Clicking "Generate" button to display AI overview
- Hardcode search results and AI overview content for demo purposes
</thinking>

<html>
<head>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body>

<div class="flex flex-col h-screen">
  <div class="flex items-center gap-4 px-5 py-3">
    <img class="w-24" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png" alt="Google logo">
    <div class="flex-grow relative">
      <input type="text" id="search" class="w-full px-4 py-2 text-lg border border-gray-200 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Search Google or type a URL">
      <div id="autocomplete" class="absolute w-full bg-white border border-gray-100 rounded-md shadow-lg z-10 hidden">
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">times</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">ts-ebml</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">tiktok ceo</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">two videos</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Taskrabbit</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">translate</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Target</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Taylor Swift</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Travis Kelce</div>
        <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Temu</div>
      </div>
    </div>
    <button id="search-btn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md ml-2">
      <i class="fas fa-search"></i>
    </button>
    <img class="w-8 h-8 rounded-full ml-4" src="https://via.placeholder.com/150" alt="Profile picture of the user">
  </div>
  
  <div class="flex-grow overflow-y-auto">
    <div id="search-results" class="p-8 hidden">
      <div class="text-sm text-gray-600 mb-4">Results for New York, NY 10022 - Choose area</div>
      
      <div class="bg-blue-50 p-4 mb-4">
        <button id="overview-btn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md">Get an AI-powered overview for this search?</button>
      </div>
      
      <div class="mb-4">
        <div class="text-xl text-blue-800 mb-1">
          <a href="https://www.nytimes.com">The New York Times</a>
        </div>
        <div class="text-sm text-gray-800 mb-1">https://www.nytimes.com</div>
        <div class="text-sm text-gray-600">
          The New York Times - Breaking News, US News, World News ...
        </div>
        <div class="text-sm text-gray-600">  
          Live news, investigations, opinion, photos and video by the journalists of The New York Times from more than 150 countries around the world.
        </div>
      </div>

      <div class="mb-4">
        <div class="text-sm text-gray-600 mb-1">Opinion | The House Vote to Force TikTok's Sale Is a Mistake - The ...</div>
        <div class="text-sm text-gray-600">Why Are Lawmakers Trying to Ban TikTok Instead of Doing What ...</div>
      </div>

      <div class="mb-4">  
        <div class="text-sm text-gray-600 mb-1">The Crossword</div>
        <div class="text-sm text-gray-600">Play the Daily New York Times Crossword puzzle edited by Will ...</div>
      </div>

      <div class="mb-4">
        <div class="text-sm text-gray-600 mb-1">Today's Paper</div>
        <div class="text-sm text-gray-600">Today's Paper · The Front Page · Highlights · Lawyer, Author ...</div>
      </div>

      <div class="mb-4">
        <div class="text-sm text-gray-600 mb-1">Word game Wordle</div>
        <div class="text-sm text-gray-600">Get 6 chances to guess a 5-letter word.Get 6 chances to guess a ...</div>
      </div>

      <div>
        <div class="text-sm text-gray-600">Connections</div>
        <div class="text-sm text-gray-600">A look at the links between The Times and the world it covers.</div>
      </div>
    </div>
    
    <div id="overview" class="p-8 hidden">
      <h2 class="text-2xl font-bold mb-4">The New York Times</h2>
      <div class="mb-4">
        <div class="font-bold">Newspaper</div>
      </div>
      <div class="mb-4">  
        <div class="font-bold">Owner:</div>
        <div>The New York Times Company</div>
      </div>
      <div class="mb-4">
        <div class="font-bold">Publisher:</div>  
        <div>The New York Times Company</div>
      </div>
      <div class="mb-4">
        <div class="font-bold">Founders:</div>
        <div>George Jones, Henry Jarvis Raymond</div>  
      </div>
      <div class="mb-4">
        <div class="font-bold">Circulation:</div>
        <div>10,360,000 news subscribers (as of February 2024)</div>
      </div>
      <div class="mb-4">  
        <div class="font-bold">Editor-in-chief:</div>
        <div>Joseph Kahn</div>
      </div>
      <div class="mb-4">
        <div class="font-bold">Format:</div>
        <div>Broadsheet</div>
      </div>
      <div>  
        <div class="font-bold">Founded:</div> 
        <div>September 18, 1851; 172 years ago</div>
      </div>
    </div>
  </div>
</div>

<script>
$(document).ready(function() {
  let selectedText = "";
  
  $("#search").on("input", function() {
    const searchText = $(this).val().trim();
    
    if (searchText !== "") {
      $("#autocomplete").removeClass("hidden");
      $("#autocomplete div").each(function() {
        if ($(this).text().toLowerCase().startsWith(searchText.toLowerCase())) {
          $(this).removeClass("hidden");
        } else {
          $(this).addClass("hidden");
        }
      });
    } else {
      $("#autocomplete").addClass("hidden");
    }
  });
  
  $("#autocomplete div").click(function() {
    selectedText = $(this).text();
    $("#search").val(selectedText);
    $("#autocomplete").addClass("hidden");
  });
  
  $("#search-btn").click(function() {
    if (selectedText !== "") {
      $("#search-results").removeClass("hidden");
      $("#overview").addClass("hidden");
    }
  });
  
  $("#overview-btn").click(function() {
    $("#overview").removeClass("hidden");
  });
});
</script>

</body>
</html>

<thinking>
To improve the app further and make it fully functional like the original video:

1. Add more visual styling to match the original, including:
   - Google logo and favicon
   - User profile icon
   - Search bar styling and hover effects
   - Search button icon and styling
   - Fonts and text sizes
   - Spacing and layout adjustments

2. Implement the search functionality:
   - Perform a search when the user presses Enter or clicks the search button
   - Show a loading state while searching
   - Display the search results dynamically based on the query
   - Handle cases when no results are found

3. Enhance the autocomplete functionality:
   - Fetch autocomplete suggestions from a data source based on the user's input
   - Highlight the text match in the suggestions
   - Allow navigating the suggestions with arrow keys
   - Update the search input when a suggestion is selected

4. Improve the AI-powered overview:
   - Fetch the overview content dynamically based on the search query
   - Display a loading state while generating the overview
   - Enhance the styling of the overview to match the original

5. Add additional UI elements:
   - Header navigation tabs (About, Store, Gmail, Images)
   - Footer links (Advertising, Business, How Search works, Privacy, Terms)
   - Pagination for search results
   - Feedback link

6. Optimize performance:
   - Lazy-load images and content
   - Implement caching mechanisms
   - Minify and compress assets

7. Ensure responsiveness:
   - Make the layout responsive and mobile-friendly
   - Adjust font sizes, padding, and margins for different screen sizes
   - Hide/collapse certain elements on smaller screens

8. Enhance accessibility:
   - Add appropriate ARIA attributes
   - Ensure keyboard navigation works properly
   - Provide alternative text for images

9. Implement error handling:
   - Handle network errors gracefully
   - Display user-friendly error messages
   - Provide options to retry or refresh

10. Add smooth animations and transitions:
    - Fade in/out effects for loading states
    - Smooth sliding animations for autocomplete suggestions
    - Subtle hover and click animations for interactive elements
</thinking>

<html>
<head>
  <title>Google Search</title>
  <link rel="icon" href="https://www.google.com/favicon.ico" type="image/x-icon">
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body class="font-roboto">

<div class="flex flex-col min-h-screen">
  <header class="flex items-center justify-end space-x-4 px-5 py-2 text-sm text-gray-700">
    <a href="#" class="hover:underline">About</a>
    <a href="#" class="hover:underline">Store</a>
    <a href="#" class="hover:underline">Gmail</a>
    <a href="#" class="hover:underline">Images</a>
    <img class="w-8 h-8 rounded-full" src="https://via.placeholder.com/150" alt="Profile picture of the user">
  </header>
  
  <div class="flex items-center justify-center flex-grow">
    <div class="flex flex-col items-center w-full max-w-[584px]">
      <img class="w-[272px] mb-8" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt="Google logo">
      <div class="relative w-full mb-3">
        <input type="text" id="search" class="w-full px-5 py-3 text-lg border border-gray-200 rounded-full hover:shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Search Google or type a URL" autocomplete="off">
        <div id="autocomplete" class="absolute w-full bg-white border border-gray-100 rounded-md shadow-lg z-10 hidden">
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">times</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">ts-ebml</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">tiktok ceo</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">two videos</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Taskrabbit</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">translate</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Target</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Taylor Swift</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Travis Kelce</div>
          <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer">Temu</div>
        </div>
      </div>
      <div class="flex space-x-2">
        <button id="search-btn" class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-4 py-2 rounded">Google Search</button>
        <button class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-4 py-2 rounded">I'm Feeling Lucky</button>
      </div>
    </div>
  </div>
  
  <div id="search-results" class="flex-grow p-8 overflow-y-auto hidden">
    <div class="text-sm text-gray-600 mb-4">Results for New York, NY 10022 - Choose area</div>
    
    <div class="bg-blue-50 p-4 mb-4">
      <button id="overview-btn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Get an AI-powered overview for this search</button>
    </div>
    
    <div id="results-list">
      <div class="mb-4">
        <div class="text-xl text-blue-800 mb-1">
          <a href="https://www.nytimes.com">The New York Times</a>
        </div>
        <div class="text-sm text-gray-800 mb-1">https://www.nytimes.com</div>
        <div class="text-sm text-gray-600">
          The New York Times - Breaking News, US News, World News ...
        </div>
        <div class="text-sm text-gray-600">  
          Live news, investigations, opinion, photos and video by the journalists of The New York Times from more than 150 countries around the world.
        </div>
      </div>

      <div class="mb-4">
        <div class="text-sm text-gray-600 mb-1">Opinion | The House Vote to Force TikTok's Sale Is a Mistake - The ...</div>
        <div class="text-sm text-gray-600">Why Are Lawmakers Trying to Ban TikTok Instead of Doing What ...</div>
      </div>

      <div class="mb-4">  
        <div class="text-sm text-gray-600 mb-1">The Crossword</div>
        <div class="text-sm text-gray-600">Play the Daily New York Times Crossword puzzle edited by Will ...</div>
      </div>

      <div class="mb-4">
        <div class="text-sm text-gray-600 mb-1">Today's Paper</div>
        <div class="text-sm text-gray-600">Today's Paper · The Front Page · Highlights · Lawyer, Author ...</div>
      </div>

      <div class="mb-4">
        <div class="text-sm text-gray-600 mb-1">Word game Wordle</div>
        <div class="text-sm text-gray-600">Get 6 chances to guess a 5-letter word.Get 6 chances to guess a ...</div>
      </div>

      <div>
        <div class="text-sm text-gray-600">Connections</div>
        <div class="text-sm text-gray-600">A look at the links between The Times and the world it covers.</div>
      </div>
    </div>
    
    <div id="pagination" class="flex justify-center mt-8">
      <button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded mr-2">Previous</button>
      <button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Next</button>
    </div>
  </div>
  
  <div id="overview" class="flex-grow p-8 overflow-y-auto hidden">
    <h2 class="text-2xl font-medium mb-4">The New York Times</h2>
    <div class="mb-4">
      <div class="font-medium">Newspaper</div>
    </div>
    <div class="mb-4">  
      <div class="font-medium">Owner:</div>
      <div>The New York Times Company</div>
    </div>
    <div class="mb-4">
      <div class="font-medium">Publisher:</div>  
      <div>The New York Times Company</div>
    </div>
    <div class="mb-4">
      <div class="font-medium">Founders:</div>
      <div>George Jones, Henry Jarvis Raymond</div>  
    </div>
    <div class="mb-4">
      <div class="font-medium">Circulation:</div>
      <div>10,360,000 news subscribers (as of February 2024)</div>
    </div>
    <div class="mb-4">  
      <div class="font-medium">Editor-in-chief:</div>
      <div>Joseph Kahn</div>
    </div>
    <div class="mb-4">
      <div class="font-medium">Format:</div>
      <div>Broadsheet</div>
    </div>
    <div>  
      <div class="font-medium">Founded:</div> 
      <div>September 18, 1851; 172 years ago</div>
    </div>
  </div>
  
  <footer class="bg-gray-100 text-sm text-gray-600 px-8 py-3">
    <div class="flex justify-between">
      <div class="flex space-x-6">
        <a href="#" class="hover:underline">Advertising</a>
        <a href="#" class="hover:underline">Business</a>
        <a href="#" class="hover:underline">How Search works</a>
      </div>
      <div class="flex space-x-6">
        <a href="#" class="hover:underline">Privacy</a>
        <a href="#" class="hover:underline">Terms</a>
        <a href="#" class="hover:underline">Settings</a>
      </div>
    </div>
  </footer>
</div>

<script>
$(document).ready(function() {
  let selectedText = "";
  
  $("#search").on("input", function() {
    const searchText = $(this).val().trim();
    
    if (searchText !== "") {
      $("#autocomplete").removeClass("hidden");
      $("#autocomplete div").each(function() {
        if ($(this).text().toLowerCase().includes(searchText.toLowerCase())) {
          $(this).removeClass("hidden");
          $(this).html($(this).text().replace(new RegExp(searchText, "gi"), "<strong>$&</strong>"));
        } else {
          $(this).addClass("hidden");
        }
      });
    } else {
      $("#autocomplete").addClass("hidden");
    }
  });
  
  $("#autocomplete div").click(function() {
    selectedText = $(this).text();
    $("#search").val(selectedText);
    $("#autocomplete").addClass("hidden");
  });
  
  $("#search").keydown(function(e) {
    if (e.which === 13) {
      performSearch();
    }
  });
  
  $("#search-btn").click(performSearch);
  
  $("#overview-btn").click(function() {
    $("#overview").removeClass("hidden");
    $("#search-results").addClass("hidden");
  });
  
  function performSearch() {
    const query = $("#search").val().trim();
    if (query !== "") {
      $("#search-results").removeClass("hidden");
      $("#overview").addClass("hidden");
      
      // Simulating search results for demo purposes
      $("#results-list").empty();
      for (let i = 0; i < 6; i++) {
        $("#results-list").append(`
          <div class="mb-4">
            <div class="text-xl text-blue-800 mb-1">
              <a href="#">Search Result ${i + 1}</a>
            </div>
            <div class="text-sm text-gray-800 mb-1">https://example.com/result${i + 1}</div>
            <div class="text-sm text-gray-600">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nulla sit amet aliquam lacinia, nisl nisl aliquam nisl, nec aliquam nisl nisl sit amet nisl.
            </div>
          </div>
        `);
      }
    }
  }
});
</script>

</body>
</html>
"""

TALLY_FORM_VIDEO_PROMPT_MOCK = """
<thinking>
The user flow in the video seems to be:
1. The quiz starts by asking "What is the capital of Bulgaria?" with three multiple choice options. The user selects "Sofia".
2. The next question asks "What is the most famous dish in Belgium?" with two image-based options. The user selects "Steak with fries".
3. The next question asks "What is the name of this actor?" with an image of an older man with gray hair and beard. The user types in "Clooney" and submits the answer.
4. The quiz ends showing the user's score of 10/30 and a "Bummer, try again!" message with an arrow to restart.

To build this as a functional web app:
1. Create the overall page structure with a title, question area, answer options area, and next/submit buttons.
2. Use jQuery to show/hide different questions and update the answer options based on the current question. 
3. Store the questions, answer options, and correct answers in a Javascript array.
4. Keep track of the user's selections and compare them to the correct answers to calculate the score.
5. After the last question, show the final score and restart option.
6. Style everything to match the screenshots using Tailwind utility classes.
</thinking>

<html>
<!DOCTYPE html>
<html>
<head>
  <title>Online Trivia Quiz</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body class="bg-white text-gray-800 p-6">
  <h1 class="text-2xl font-bold mb-4">Online Trivia Quiz</h1>
  
  <div id="question-container" class="mb-6">
    <h2 id="question" class="text-xl mb-4"></h2>
    <div id="options" class="space-y-4"></div>
  </div>
  
  <div id="actor-container" class="mb-6 hidden">
    <img id="actor-image" src="" alt="" class="mb-4 w-2/3 mx-auto rounded-lg">
    <input type="text" id="actor-answer" class="border rounded w-full py-2 px-3 text-gray-700 mb-3">
  </div>

  <div id="result-container" class="text-center hidden">
    <h2 class="text-xl mb-2">Bummer, try again!</h2>
    <p id="score" class="text-lg mb-4"></p>
    <button id="restart-btn" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
      <i class="fas fa-redo mr-2"></i> Restart
    </button>
  </div>

  <button id="next-btn" class="bg-black hover:bg-gray-800 text-white font-bold py-2 px-4 rounded">
    Next <i class="fas fa-arrow-right ml-2"></i>
  </button>
  
  <button id="submit-btn" class="bg-black hover:bg-gray-800 text-white font-bold py-2 px-4 rounded hidden">
    Submit <i class="fas fa-arrow-right ml-2"></i>
  </button>

  <script>
    const quizData = [
      {
        question: "What is the capital of Bulgaria?",
        options: ["Sofia", "Plovdiv", "Varna"],
        answer: "Sofia"
      },
      {
        question: "What is the most famous dish in Belgium?", 
        options: [
          {
            image: "https://via.placeholder.com/300x200?text=Spaghetti",
            alt: "A bowl of spaghetti pasta with tomato sauce and parmesan cheese on top",
            text: "Spaghetti"
          },
          {
            image: "https://via.placeholder.com/300x200?text=Steak+with+fries",
            alt: "A plate with grilled steak, french fries, and greens",
            text: "Steak with fries"
          }
        ],
        answer: "Steak with fries"
      },
      {
        question: "What is the name of this actor?",
        image: "https://via.placeholder.com/400x300?text=Older+man+with+gray+hair+and+beard",
        alt: "Headshot of an older man with gray hair and a gray beard wearing a black suit and bowtie",
        answer: "Clooney"
      }
    ];

    let currentQuestion = 0;
    let score = 0;

    function startQuiz() {
      currentQuestion = 0;
      score = 0;
      $("#question-container").show();
      $("#actor-container").hide();
      $("#result-container").hide();
      $("#next-btn").show();
      $("#submit-btn").hide();
      showQuestion();
    }

    function showQuestion() {
      const questionData = quizData[currentQuestion];
      $("#question").text(questionData.question);
      
      if (questionData.options) {
        let optionsHtml = "";
        for (let option of questionData.options) {
          if (typeof option === "string") {
            optionsHtml += `
              <div>
                <label>
                  <input type="radio" name="answer" value="${option}">
                  ${option}
                </label>
              </div>
            `;
          } else {
            optionsHtml += `
              <div>
                <label>
                  <input type="radio" name="answer" value="${option.text}">
                  <img src="${option.image}" alt="${option.alt}" class="w-full mb-2">
                  ${option.text}
                </label>
              </div>
            `;
          }
        }
        $("#options").html(optionsHtml);
        $("#actor-container").hide();
      } else {
        $("#options").empty();
        $("#actor-image").attr("src", questionData.image).attr("alt", questionData.alt);
        $("#actor-answer").val("");
        $("#actor-container").show();
        $("#next-btn").hide();
        $("#submit-btn").show();
      }
    }

    function checkAnswer() {
      const questionData = quizData[currentQuestion];
      
      let userAnswer;
      if (questionData.options) {
        userAnswer = $("input[name='answer']:checked").val();
      } else {
        userAnswer = $("#actor-answer").val().trim();
      }

      if (userAnswer === questionData.answer) {
        score++;
      }

      currentQuestion++;
      
      if (currentQuestion < quizData.length) {
        showQuestion();
      } else {
        showResult();
      }
    }

    function showResult() {
      $("#question-container").hide();
      $("#actor-container").hide();
      $("#score").text(`Your score is: ${score} / ${quizData.length}`);
      $("#result-container").show();
    }

    $("#next-btn").click(checkAnswer);
    $("#submit-btn").click(checkAnswer);
    $("#restart-btn").click(startQuiz);

    startQuiz();
  </script>
</body>
</html>
</html>

This implements the online trivia quiz app as shown in the video. It uses jQuery to handle the interactions and Tailwind CSS for styling. The questions, options, and answers are defined in the quizData array. The app keeps track of the user's current question and score, allows selecting or entering answers, and displays the final score at the end with the option to restart.

<thinking>
To further improve the app to match the original video:

1. Adjust the styling to more closely match the font, colors, spacing, and layout in the screenshots. 
2. Add a cursor pointer when hovering over answer options.
3. Improve the styling of the selected answer option.
4. Make the "Next" button change to an arrow icon on hover.
5. Add a "Back" button to go to the previous question.
6. Show the current question number and total (e.g. "Question 1 of 3").
7. Animate the transitions between questions.
8. Improve the responsive layout for different screen sizes.
9. Preload the images to avoid flickering.
10. Refactor the code to be more modular and maintainable.
</thinking>

<html>
<!DOCTYPE html>
<html>
<head>
  <title>Online Trivia Quiz</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
    body {
      font-family: 'Roboto', sans-serif;
    }
    .option {
      cursor: pointer;
    }
    .option.selected {
      background-color: #EFF6FF;
      border-color: #2563EB;
    }
    #next-btn:hover i {
      transform: translateX(5px);
    }
  </style>
</head>
<body class="bg-white text-gray-800 p-6">
  <h1 class="text-3xl font-bold mb-8 text-center">Online Trivia Quiz</h1>
  
  <div class="max-w-xl mx-auto">
    <div id="question-container" class="mb-8">
      <div class="flex justify-between mb-4">
        <button id="back-btn" class="text-blue-600 hover:text-blue-800 font-medium py-2 px-4 rounded">
          <i class="fas fa-arrow-left mr-2"></i> Back
        </button>
        <span id="question-number" class="text-lg font-medium"></span>
      </div>
      <h2 id="question" class="text-2xl font-medium mb-6"></h2>
      <div id="options" class="space-y-4"></div>
    </div>
    
    <div id="actor-container" class="mb-8 hidden">
      <img id="actor-image" src="" alt="" class="mb-6 mx-auto rounded-lg">
      <input type="text" id="actor-answer" class="border border-gray-400 rounded w-full py-2 px-4 text-lg mb-4 focus:outline-none focus:ring-2 focus:ring-blue-600">
    </div>

    <div id="result-container" class="text-center hidden">
      <h2 class="text-2xl font-medium mb-2">Bummer, try again!</h2>
      <p id="score" class="text-xl mb-6"></p>
      <button id="restart-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded">
        <i class="fas fa-redo mr-2"></i> Restart
      </button>
    </div>

    <button id="next-btn" class="bg-black hover:bg-gray-800 text-white font-medium py-2 px-4 rounded flex items-center ml-auto">
      Next <i class="fas fa-arrow-right ml-2"></i>
    </button>
    
    <button id="submit-btn" class="bg-black hover:bg-gray-800 text-white font-medium py-2 px-4 rounded flex items-center ml-auto hidden">
      Submit <i class="fas fa-arrow-right ml-2"></i>
    </button>
  </div>

  <script>
    const quizData = [
      {
        question: "What is the capital of Bulgaria?",
        options: ["Sofia", "Plovdiv", "Varna"],
        answer: "Sofia"
      },
      {
        question: "What is the most famous dish in Belgium?", 
        options: [
          {
            image: "https://sdxl-trainings.s3.amazonaws.com/DALL%C2%B7E+2024-03-08+12.29.30+-+A+bowl+of+spaghetti+pasta+with+tomato+sauce+and+parmesan+cheese+on+top%2C+set+on+a+rustic+wooden+table.+The+light+is+soft+and+warm%2C+enhancing+the+vibran.webp",
            alt: "A bowl of spaghetti pasta with tomato sauce and parmesan cheese on top",
            text: "Spaghetti"
          },
          {
            image: "https://sdxl-trainings.s3.amazonaws.com/DALL%C2%B7E+2024-03-08+12.33.19+-+A+plate+with+grilled+steak%2C+french+fries%2C+and+greens%2C+presented+elegantly.+The+steak+is+perfectly+cooked+to+medium+rare%2C+showing+a+rich%2C+pink+center+a.webp",
            alt: "A plate with grilled steak, french fries, and greens",
            text: "Steak with fries"
          }
        ],
        answer: "Steak with fries"
      },
      {
        question: "What is the name of this actor?",
        image: "https://sdxl-trainings.s3.amazonaws.com/DALL%C2%B7E+2024-03-08+12.34.00+-+Headshot+of+an+older+man+with+distinguished+gray+hair+and+a+neatly+trimmed+gray+beard%2C+conveying+a+sense+of+wisdom+and+experience.+He+is+wearing+a+cri.webp",
        alt: "Headshot of an older man with gray hair and a gray beard wearing a black suit and bowtie",
        answer: "Clooney"
      }
    ];

    let currentQuestion = 0;
    let score = 0;

    function startQuiz() {
      currentQuestion = 0;
      score = 0;
      $("#question-container").show();
      $("#actor-container").hide();
      $("#result-container").hide();
      $("#next-btn").show();
      $("#submit-btn").hide();
      showQuestion();
    }

    function showQuestion() {
      const questionData = quizData[currentQuestion];
      $("#question").text(questionData.question);
      $("#question-number").text(`Question ${currentQuestion + 1} of ${quizData.length}`);
      
      if (questionData.options) {
        let optionsHtml = "";
        for (let option of questionData.options) {
          if (typeof option === "string") {
            optionsHtml += `
              <div class="option p-4 border border-gray-300 rounded flex items-center">
                <input type="radio" name="answer" id="${option}" value="${option}" class="mr-4">
                <label for="${option}" class="text-lg font-medium flex-grow cursor-pointer">${option}</label>
              </div>
            `;
          } else {
            optionsHtml += `
              <div class="option p-4 border border-gray-300 rounded flex items-center">
                <input type="radio" name="answer" id="${option.text}" value="${option.text}" class="mr-4">
                <label for="${option.text}" class="flex-grow cursor-pointer">
                  <img src="${option.image}" alt="${option.alt}" class="w-full mb-2 rounded">
                  <span class="text-lg font-medium">${option.text}</span>
                </label>
              </div>
            `;
          }
        }
        $("#options").html(optionsHtml);
        $("#actor-container").hide();
      } else {
        $("#options").empty();
        $("#actor-image").attr("src", questionData.image).attr("alt", questionData.alt);
        $("#actor-answer").val("");
        $("#actor-container").show();
        $("#next-btn").hide();
        $("#submit-btn").show();
      }
      
      $("#back-btn").toggle(currentQuestion > 0);
    }

    function checkAnswer() {
      const questionData = quizData[currentQuestion];
      
      let userAnswer;
      if (questionData.options) {
        userAnswer = $("input[name='answer']:checked").val();
      } else {
        userAnswer = $("#actor-answer").val().trim();
      }

      if (userAnswer === questionData.answer) {
        score++;
      }

      currentQuestion++;
      
      if (currentQuestion < quizData.length) {
        $("#question-container").fadeOut(200, function() {
          showQuestion();
          $(this).fadeIn(200);
        });
      } else {
        $("#question-container").fadeOut(200, function() {
          showResult();
          $("#result-container").fadeIn(200);
        });
      }
    }

    function previousQuestion() {
      if (currentQuestion > 0) {
        currentQuestion--;
        $("#question-container").fadeOut(200, function() {
          showQuestion();
          $(this).fadeIn(200);
        });
      }
    }

    function showResult() {
      $("#score").text(`Your score is: ${score} / ${quizData.length}`);
    }

    $("#next-btn").click(checkAnswer);
    $("#submit-btn").click(checkAnswer);
    $("#back-btn").click(previousQuestion);
    $("#restart-btn").click(function() {
      $("#result-container").fadeOut(200, function() {
        startQuiz();
        $("#question-container").fadeIn(200);
      });
    });

    $("body").on("change", "input[name='answer']", function() {
      $(".option").removeClass("selected");
      $(this).closest(".option").addClass("selected");
    });

    $(document).ready(function() {
      // Preload images
      let images = [];
      for (let question of quizData) {
        if (question.image) {
          images.push(question.image);
        } else if (question.options) {
          for (let option of question.options) {
            if (option.image) {
              images.push(option.image);
            }
          }
        }
      }
      preloadImages(images, startQuiz);
    });

    function preloadImages(urls, callback) {
      let count = 0;
      for (let url of urls) {
        $("<img>").attr("src", url).on("load", function() {
          count++;
          if (count === urls.length) {
            callback();
          }
        });
      }
    }
  </script>
</body>
</html>
</html>

This updated version makes several improvements to match the original app more closely:

1. Styling is refined to better match the font, colors, spacing, and layout of the screenshots.
2. Cursor changes to a pointer when hovering over answer options.
3. Selected answer option is highlighted.
4. "Next" button shows an animated arrow on hover.
5. "Back" button added to go to the previous question.
6. Current question number and total are displayed.
7. Fade transitions are added between questions.
8. Layout is made responsive for different screen sizes.
9. Images are preloaded to avoid flickering.
10. Code is refactored to be more modular and maintainable.

The app should now be fully functional and closely resemble the look and behavior of the original video. Let me know if you would like any further adjustments!
"""



File: backend/llm.py

import copy
from enum import Enum
from typing import Any, Awaitable, Callable, List, cast
from anthropic import AsyncAnthropic
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
from config import IS_DEBUG_ENABLED
from debug.DebugFileWriter import DebugFileWriter
from image_processing.utils import process_image

from utils import pprint_prompt


# Actual model versions that are passed to the LLMs and stored in our logs
class Llm(Enum):
    GPT_4_VISION = "gpt-4-vision-preview"
    GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
    GPT_4O_2024_05_13 = "gpt-4o-2024-05-13"
    CLAUDE_3_SONNET = "claude-3-sonnet-20240229"
    CLAUDE_3_OPUS = "claude-3-opus-20240229"
    CLAUDE_3_HAIKU = "claude-3-haiku-20240307"
    CLAUDE_3_5_SONNET_2024_06_20 = "claude-3-5-sonnet-20240620"


# Will throw errors if you send a garbage string
def convert_frontend_str_to_llm(frontend_str: str) -> Llm:
    if frontend_str == "gpt_4_vision":
        return Llm.GPT_4_VISION
    elif frontend_str == "claude_3_sonnet":
        return Llm.CLAUDE_3_SONNET
    else:
        return Llm(frontend_str)


async def stream_openai_response(
    messages: List[ChatCompletionMessageParam],
    api_key: str,
    base_url: str | None,
    callback: Callable[[str], Awaitable[None]],
    model: Llm,
) -> str:
    client = AsyncOpenAI(api_key=api_key, base_url=base_url)

    # Base parameters
    params = {
        "model": model.value,
        "messages": messages,
        "stream": True,
        "timeout": 600,
        "temperature": 0.0,
    }

    # Add 'max_tokens' only if the model is a GPT4 vision or Turbo model
    if (
        model == Llm.GPT_4_VISION
        or model == Llm.GPT_4_TURBO_2024_04_09
        or model == Llm.GPT_4O_2024_05_13
    ):
        params["max_tokens"] = 4096

    stream = await client.chat.completions.create(**params)  # type: ignore
    full_response = ""
    async for chunk in stream:  # type: ignore
        assert isinstance(chunk, ChatCompletionChunk)
        if (
            chunk.choices
            and len(chunk.choices) > 0
            and chunk.choices[0].delta
            and chunk.choices[0].delta.content
        ):
            content = chunk.choices[0].delta.content or ""
            full_response += content
            await callback(content)

    await client.close()

    return full_response


# TODO: Have a seperate function that translates OpenAI messages to Claude messages
async def stream_claude_response(
    messages: List[ChatCompletionMessageParam],
    api_key: str,
    callback: Callable[[str], Awaitable[None]],
    model: Llm,
) -> str:

    client = AsyncAnthropic(api_key=api_key)

    # Base parameters
    max_tokens = 8192
    temperature = 0.0

    # Translate OpenAI messages to Claude messages

    # Deep copy messages to avoid modifying the original list
    cloned_messages = copy.deepcopy(messages)

    system_prompt = cast(str, cloned_messages[0].get("content"))
    claude_messages = [dict(message) for message in cloned_messages[1:]]
    for message in claude_messages:
        if not isinstance(message["content"], list):
            continue

        for content in message["content"]:  # type: ignore
            if content["type"] == "image_url":
                content["type"] = "image"

                # Extract base64 data and media type from data URL
                # Example base64 data URL: data:image/png;base64,iVBOR...
                image_data_url = cast(str, content["image_url"]["url"])

                # Process image and split media type and data
                # so it works with Claude (under 5mb in base64 encoding)
                (media_type, base64_data) = process_image(image_data_url)

                # Remove OpenAI parameter
                del content["image_url"]

                content["source"] = {
                    "type": "base64",
                    "media_type": media_type,
                    "data": base64_data,
                }

    # Stream Claude response
    async with client.messages.stream(
        model=model.value,
        max_tokens=max_tokens,
        temperature=temperature,
        system=system_prompt,
        messages=claude_messages,  # type: ignore
        extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"},
    ) as stream:
        async for text in stream.text_stream:
            await callback(text)

    # Return final message
    response = await stream.get_final_message()

    # Close the Anthropic client
    await client.close()

    return response.content[0].text


async def stream_claude_response_native(
    system_prompt: str,
    messages: list[Any],
    api_key: str,
    callback: Callable[[str], Awaitable[None]],
    include_thinking: bool = False,
    model: Llm = Llm.CLAUDE_3_OPUS,
) -> str:

    client = AsyncAnthropic(api_key=api_key)

    # Base model parameters
    max_tokens = 4096
    temperature = 0.0

    # Multi-pass flow
    current_pass_num = 1
    max_passes = 2

    prefix = "<thinking>"
    response = None

    # For debugging
    full_stream = ""
    debug_file_writer = DebugFileWriter()

    while current_pass_num <= max_passes:
        current_pass_num += 1

        # Set up message depending on whether we have a <thinking> prefix
        messages_to_send = (
            messages + [{"role": "assistant", "content": prefix}]
            if include_thinking
            else messages
        )

        pprint_prompt(messages_to_send)

        async with client.messages.stream(
            model=model.value,
            max_tokens=max_tokens,
            temperature=temperature,
            system=system_prompt,
            messages=messages_to_send,  # type: ignore
        ) as stream:
            async for text in stream.text_stream:
                print(text, end="", flush=True)
                full_stream += text
                await callback(text)

        response = await stream.get_final_message()
        response_text = response.content[0].text

        # Write each pass's code to .html file and thinking to .txt file
        if IS_DEBUG_ENABLED:
            debug_file_writer.write_to_file(
                f"pass_{current_pass_num - 1}.html",
                debug_file_writer.extract_html_content(response_text),
            )
            debug_file_writer.write_to_file(
                f"thinking_pass_{current_pass_num - 1}.txt",
                response_text.split("</thinking>")[0],
            )

        # Set up messages array for next pass
        messages += [
            {"role": "assistant", "content": str(prefix) + response.content[0].text},
            {
                "role": "user",
                "content": "You've done a good job with a first draft. Improve this further based on the original instructions so that the app is fully functional and looks like the original video of the app we're trying to replicate.",
            },
        ]

        print(
            f"Token usage: Input Tokens: {response.usage.input_tokens}, Output Tokens: {response.usage.output_tokens}"
        )

    # Close the Anthropic client
    await client.close()

    if IS_DEBUG_ENABLED:
        debug_file_writer.write_to_file("full_stream.txt", full_stream)

    if not response:
        raise Exception("No HTML response found in AI response")
    else:
        return response.content[0].text



File: backend/start.py

import uvicorn

if __name__ == "__main__":
    uvicorn.run("main:app", port=7001, reload=True)



File: backend/run_evals.py

# Load environment variables first
from dotenv import load_dotenv

from llm import Llm

load_dotenv()

import os
from typing import Any, Coroutine
import asyncio

from evals.config import EVALS_DIR
from evals.core import generate_code_for_image
from evals.utils import image_to_data_url

STACK = "html_tailwind"
# MODEL = Llm.CLAUDE_3_5_SONNET_2024_06_20
N = 2  # Number of outputs to generate


async def main():
    INPUT_DIR = EVALS_DIR + "/inputs"
    OUTPUT_DIR = EVALS_DIR + "/outputs"

    # Get all the files in the directory (only grab pngs)
    evals = [f for f in os.listdir(INPUT_DIR) if f.endswith(".png")]

    tasks: list[Coroutine[Any, Any, str]] = []
    for filename in evals:
        filepath = os.path.join(INPUT_DIR, filename)
        data_url = await image_to_data_url(filepath)
        for n in range(N):  # Generate N tasks for each input
            if n == 0:
                task = generate_code_for_image(
                    image_url=data_url,
                    stack=STACK,
                    model=Llm.CLAUDE_3_5_SONNET_2024_06_20,
                )
            else:
                task = generate_code_for_image(
                    image_url=data_url, stack=STACK, model=Llm.GPT_4O_2024_05_13
                )
            tasks.append(task)

    print(f"Generating {len(tasks)} codes")

    results = await asyncio.gather(*tasks)

    os.makedirs(OUTPUT_DIR, exist_ok=True)

    for i, content in enumerate(results):
        # Calculate index for filename and output number
        eval_index = i // N
        output_number = i % N
        filename = evals[eval_index]
        # File name is derived from the original filename in evals with an added output number
        output_filename = f"{os.path.splitext(filename)[0]}_{output_number}.html"
        output_filepath = os.path.join(OUTPUT_DIR, output_filename)
        with open(output_filepath, "w") as file:
            file.write(content)


# async def text_main():
#     OUTPUT_DIR = EVALS_DIR + "/outputs"

#     GENERAL_TEXT_V1 = [
#         "Login form",
#         "Simple notification",
#         "button",
#         "saas dashboard",
#         "landing page for barber shop",
#     ]

#     tasks: list[Coroutine[Any, Any, str]] = []
#     for prompt in GENERAL_TEXT_V1:
#         for n in range(N):  # Generate N tasks for each input
#             if n == 0:
#                 task = generate_code_for_text(
#                     text=prompt,
#                     stack=STACK,
#                     model=Llm.CLAUDE_3_5_SONNET_2024_06_20,
#                 )
#             else:
#                 task = generate_code_for_text(
#                     text=prompt, stack=STACK, model=Llm.GPT_4O_2024_05_13
#                 )
#             tasks.append(task)

#     print(f"Generating {len(tasks)} codes")

#     results = await asyncio.gather(*tasks)

#     os.makedirs(OUTPUT_DIR, exist_ok=True)

#     for i, content in enumerate(results):
#         # Calculate index for filename and output number
#         eval_index = i // N
#         output_number = i % N
#         filename = GENERAL_TEXT_V1[eval_index]
#         # File name is derived from the original filename in evals with an added output number
#         output_filename = f"{os.path.splitext(filename)[0]}_{output_number}.html"
#         output_filepath = os.path.join(OUTPUT_DIR, output_filename)
#         with open(output_filepath, "w") as file:
#             file.write(content)


asyncio.run(main())



File: backend/utils.py

import copy
import json
from typing import List
from openai.types.chat import ChatCompletionMessageParam


def pprint_prompt(prompt_messages: List[ChatCompletionMessageParam]):
    print(json.dumps(truncate_data_strings(prompt_messages), indent=4))


def truncate_data_strings(data: List[ChatCompletionMessageParam]):  # type: ignore
    # Deep clone the data to avoid modifying the original object
    cloned_data = copy.deepcopy(data)

    if isinstance(cloned_data, dict):
        for key, value in cloned_data.items():  # type: ignore
            # Recursively call the function if the value is a dictionary or a list
            if isinstance(value, (dict, list)):
                cloned_data[key] = truncate_data_strings(value)  # type: ignore
            # Truncate the string if it it's long and add ellipsis and length
            elif isinstance(value, str):
                cloned_data[key] = value[:40]  # type: ignore
                if len(value) > 40:
                    cloned_data[key] += "..." + f" ({len(value)} chars)"  # type: ignore

    elif isinstance(cloned_data, list):  # type: ignore
        # Process each item in the list
        cloned_data = [truncate_data_strings(item) for item in cloned_data]  # type: ignore

    return cloned_data  # type: ignore



File: backend/custom_types.py

from typing import Literal


InputMode = Literal[
    "image",
    "video",
]



File: backend/main.py

# Load environment variables first
from dotenv import load_dotenv

load_dotenv()


from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routes import screenshot, generate_code, home, evals

app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)

# Configure CORS settings
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Add routes
app.include_router(generate_code.router)
app.include_router(screenshot.router)
app.include_router(home.router)
app.include_router(evals.router)



File: backend/run_image_generation_evals.py

import asyncio
import os
from typing import List, Optional, Literal
from dotenv import load_dotenv
import aiohttp
from image_generation.core import process_tasks

EVALS = [
    "Romantic Background",
    "Company logo: A stylized green sprout emerging from a circle",
    "Placeholder image of a PDF cover with abstract design",
    "A complex bubble diagram showing various interconnected features and aspects of FestivalPro, with a large central bubble surrounded by smaller bubbles of different colors representing different categories and functionalities",
    "A vibrant, abstract visualization of the RhythmRise experience ecosystem, featuring interconnected neon elements representing music, technology, and human connection",
    "Banner with text 'LiblibAI学院 课程入口'",
    "Profile picture of Pierre-Louis Labonne",
    "Two hands holding iPhone 14 models with colorful displays",
    "Portrait of a woman with long dark hair smiling at the camera",
    "Threadless logo on a gradient background from light pink to coral",
    "Jordan Schlansky Shows Conan His Favorite Nose Hair Trimmer",
    "Team Coco",
    "Intro to Large Language Models",
    "Andrej Karpathy",
    "He built a $200 million toy company",
    "CNBC International",
    "What will happen in year three of the war?",
    "Channel",
    "This is it",
    "How ASML Dominates Chip Machines",
]

# Load environment variables
load_dotenv()

# Get API keys from environment variables
OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
REPLICATE_API_TOKEN: Optional[str] = os.getenv("REPLICATE_API_TOKEN")

# Directory to save generated images
OUTPUT_DIR: str = "generated_images"


async def generate_and_save_images(
    prompts: List[str],
    model: Literal["dalle3", "sdxl-lightning"],
    api_key: Optional[str],
) -> None:
    # Ensure the output directory exists
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    if api_key is None:
        raise ValueError(f"API key for {model} is not set in the environment variables")

    # Generate images
    results: List[Optional[str]] = await process_tasks(
        prompts, api_key, None, model=model
    )

    # Save images to disk
    async with aiohttp.ClientSession() as session:
        for i, image_url in enumerate(results):
            if image_url:
                # Get the image data
                async with session.get(image_url) as response:
                    image_data: bytes = await response.read()

                # Save the image with a filename based on the input eval
                prefix = "replicate_" if model == "sdxl-lightning" else "dalle3_"
                filename: str = (
                    f"{prefix}{prompts[i][:50].replace(' ', '_').replace(':', '')}.png"
                )
                filepath: str = os.path.join(OUTPUT_DIR, filename)
                with open(filepath, "wb") as f:
                    f.write(image_data)
                print(f"Saved {model} image: {filepath}")
            else:
                print(f"Failed to generate {model} image for prompt: {prompts[i]}")


async def main() -> None:
    # await generate_and_save_images(EVALS, "dalle3", OPENAI_API_KEY)
    await generate_and_save_images(EVALS, "sdxl-lightning", REPLICATE_API_TOKEN)


if __name__ == "__main__":
    asyncio.run(main())



File: backend/video_to_app.py

# Load environment variables first

from dotenv import load_dotenv

load_dotenv()


import base64
import mimetypes
import time
import subprocess
import os
import asyncio
from datetime import datetime
from prompts.claude_prompts import VIDEO_PROMPT
from utils import pprint_prompt
from config import ANTHROPIC_API_KEY
from video.utils import extract_tag_content, assemble_claude_prompt_video
from llm import (
    Llm,
    stream_claude_response_native,
)

STACK = "html_tailwind"

VIDEO_DIR = "./video_evals/videos"
SCREENSHOTS_DIR = "./video_evals/screenshots"
OUTPUTS_DIR = "./video_evals/outputs"


async def main():
    video_filename = "shortest.mov"
    is_followup = False

    if not ANTHROPIC_API_KEY:
        raise ValueError("ANTHROPIC_API_KEY is not set")

    # Get previous HTML
    previous_html = ""
    if is_followup:
        previous_html_file = max(
            [
                os.path.join(OUTPUTS_DIR, f)
                for f in os.listdir(OUTPUTS_DIR)
                if f.endswith(".html")
            ],
            key=os.path.getctime,
        )
        with open(previous_html_file, "r") as file:
            previous_html = file.read()

    video_file = os.path.join(VIDEO_DIR, video_filename)
    mime_type = mimetypes.guess_type(video_file)[0]
    with open(video_file, "rb") as file:
        video_content = file.read()
    video_data_url = (
        f"data:{mime_type};base64,{base64.b64encode(video_content).decode('utf-8')}"
    )

    prompt_messages = await assemble_claude_prompt_video(video_data_url)

    # Tell the model to continue
    # {"role": "assistant", "content": SECOND_MESSAGE},
    # {"role": "user", "content": "continue"},

    if is_followup:
        prompt_messages += [
            {"role": "assistant", "content": previous_html},
            {
                "role": "user",
                "content": "You've done a good job with a first draft. Improve this further based on the original instructions so that the app is fully functional like in the original video.",
            },
        ]  # type: ignore

    async def process_chunk(content: str):
        print(content, end="", flush=True)

    response_prefix = "<thinking>"

    pprint_prompt(prompt_messages)  # type: ignore

    start_time = time.time()

    completion = await stream_claude_response_native(
        system_prompt=VIDEO_PROMPT,
        messages=prompt_messages,
        api_key=ANTHROPIC_API_KEY,
        callback=lambda x: process_chunk(x),
        model=Llm.CLAUDE_3_OPUS,
        include_thinking=True,
    )

    end_time = time.time()

    # Prepend the response prefix to the completion
    completion = response_prefix + completion

    # Extract the outputs
    html_content = extract_tag_content("html", completion)
    thinking = extract_tag_content("thinking", completion)

    print(thinking)
    print(f"Operation took {end_time - start_time} seconds")

    os.makedirs(OUTPUTS_DIR, exist_ok=True)

    # Generate a unique filename based on the current time
    timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    filename = f"video_test_output_{timestamp}.html"
    output_path = os.path.join(OUTPUTS_DIR, filename)

    # Write the HTML content to the file
    with open(output_path, "w") as file:
        file.write(html_content)

    print(f"Output file path: {output_path}")

    # Show a notification
    subprocess.run(["osascript", "-e", 'display notification "Coding Complete"'])


asyncio.run(main())



File: backend/video/utils.py

# Extract HTML content from the completion string
import base64
import io
import mimetypes
import os
import tempfile
import uuid
from typing import Any, Union, cast
from moviepy.editor import VideoFileClip  # type: ignore
from PIL import Image
import math


DEBUG = True
TARGET_NUM_SCREENSHOTS = (
    20  # Should be max that Claude supports (20) - reduce to save tokens on testing
)


async def assemble_claude_prompt_video(video_data_url: str) -> list[Any]:
    images = split_video_into_screenshots(video_data_url)

    # Save images to tmp if we're debugging
    if DEBUG:
        save_images_to_tmp(images)

    # Validate number of images
    print(f"Number of frames extracted from video: {len(images)}")
    if len(images) > 20:
        print(f"Too many screenshots: {len(images)}")
        raise ValueError("Too many screenshots extracted from video")

    # Convert images to the message format for Claude
    content_messages: list[dict[str, Union[dict[str, str], str]]] = []
    for image in images:

        # Convert Image to buffer
        buffered = io.BytesIO()
        image.save(buffered, format="JPEG")

        # Encode bytes as base64
        base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
        media_type = "image/jpeg"

        content_messages.append(
            {
                "type": "image",
                "source": {
                    "type": "base64",
                    "media_type": media_type,
                    "data": base64_data,
                },
            }
        )

    return [
        {
            "role": "user",
            "content": content_messages,
        },
    ]


# Returns a list of images/frame (RGB format)
def split_video_into_screenshots(video_data_url: str) -> list[Image.Image]:
    target_num_screenshots = TARGET_NUM_SCREENSHOTS

    # Decode the base64 URL to get the video bytes
    video_encoded_data = video_data_url.split(",")[1]
    video_bytes = base64.b64decode(video_encoded_data)

    mime_type = video_data_url.split(";")[0].split(":")[1]
    suffix = mimetypes.guess_extension(mime_type)

    with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as temp_video_file:
        print(temp_video_file.name)
        temp_video_file.write(video_bytes)
        temp_video_file.flush()
        clip = VideoFileClip(temp_video_file.name)
        images: list[Image.Image] = []
        total_frames = cast(int, clip.reader.nframes)  # type: ignore

        # Calculate frame skip interval by dividing total frames by the target number of screenshots
        # Ensuring a minimum skip of 1 frame
        frame_skip = max(1, math.ceil(total_frames / target_num_screenshots))

        # Iterate over each frame in the clip
        for i, frame in enumerate(clip.iter_frames()):
            # Save every nth frame
            if i % frame_skip == 0:
                frame_image = Image.fromarray(frame)  # type: ignore
                images.append(frame_image)
                # Ensure that we don't capture more than the desired number of frames
                if len(images) >= target_num_screenshots:
                    break

        # Close the video file to release resources
        clip.close()

        return images


# Save a list of PIL images to a random temporary directory
def save_images_to_tmp(images: list[Image.Image]):

    # Create a unique temporary directory
    unique_dir_name = f"screenshots_{uuid.uuid4()}"
    tmp_screenshots_dir = os.path.join(tempfile.gettempdir(), unique_dir_name)
    os.makedirs(tmp_screenshots_dir, exist_ok=True)

    for idx, image in enumerate(images):
        # Generate a unique image filename using index
        image_filename = f"screenshot_{idx}.jpg"
        tmp_filepath = os.path.join(tmp_screenshots_dir, image_filename)
        image.save(tmp_filepath, format="JPEG")

    print("Saved to " + tmp_screenshots_dir)


def extract_tag_content(tag: str, text: str) -> str:
    """
    Extracts content for a given tag from the provided text.

    :param tag: The tag to search for.
    :param text: The text to search within.
    :return: The content found within the tag, if any.
    """
    tag_start = f"<{tag}>"
    tag_end = f"</{tag}>"
    start_idx = text.find(tag_start)
    end_idx = text.find(tag_end, start_idx)
    if start_idx != -1 and end_idx != -1:
        return text[start_idx : end_idx + len(tag_end)]
    return ""



File: backend/evals/config.py

EVALS_DIR = "./evals_data"



File: backend/evals/__init__.py




File: backend/evals/core.py

from config import ANTHROPIC_API_KEY, OPENAI_API_KEY
from llm import Llm, stream_claude_response, stream_openai_response
from prompts import assemble_prompt
from prompts.types import Stack
from openai.types.chat import ChatCompletionMessageParam


async def generate_code_for_image(image_url: str, stack: Stack, model: Llm) -> str:
    prompt_messages = assemble_prompt(image_url, stack)
    return await generate_code_core(prompt_messages, model)


async def generate_code_core(
    prompt_messages: list[ChatCompletionMessageParam], model: Llm
) -> str:

    async def process_chunk(_: str):
        pass

    if model == Llm.CLAUDE_3_SONNET or model == Llm.CLAUDE_3_5_SONNET_2024_06_20:
        if not ANTHROPIC_API_KEY:
            raise Exception("Anthropic API key not found")

        completion = await stream_claude_response(
            prompt_messages,
            api_key=ANTHROPIC_API_KEY,
            callback=lambda x: process_chunk(x),
            model=model,
        )
    else:
        if not OPENAI_API_KEY:
            raise Exception("OpenAI API key not found")

        completion = await stream_openai_response(
            prompt_messages,
            api_key=OPENAI_API_KEY,
            base_url=None,
            callback=lambda x: process_chunk(x),
            model=model,
        )

    return completion



File: backend/evals/utils.py

import base64


async def image_to_data_url(filepath: str):
    with open(filepath, "rb") as image_file:
        encoded_string = base64.b64encode(image_file.read()).decode()
    return f"data:image/png;base64,{encoded_string}"



File: backend/image_generation/replicate.py

import asyncio
import httpx


async def call_replicate(
    replicate_model_version: str, input: dict[str, str | int], api_token: str
) -> str:
    url = "https://api.replicate.com/v1/predictions"
    headers = {
        "Authorization": f"Bearer {api_token}",
        "Content-Type": "application/json",
    }

    data = {"version": replicate_model_version, "input": input}

    async with httpx.AsyncClient() as client:
        try:
            response = await client.post(url, headers=headers, json=data)
            response.raise_for_status()
            response_json = response.json()

            # Extract the id from the response
            prediction_id = response_json.get("id")
            if not prediction_id:
                raise ValueError("Prediction ID not found in initial response.")

            # Polling every 1 second until the status is succeeded or error
            num_polls = 0
            max_polls = 100
            while num_polls < max_polls:
                num_polls += 1

                await asyncio.sleep(0.2)

                # Check the status
                status_check_url = f"{url}/{prediction_id}"
                status_response = await client.get(status_check_url, headers=headers)
                status_response.raise_for_status()
                status_response_json = status_response.json()
                status = status_response_json.get("status")

                # If status is succeeded or if there's an error, break out of the loop
                if status == "succeeded":
                    return status_response_json["output"][0]
                elif status == "error":
                    raise ValueError(
                        f"Inference errored out: {status_response_json.get('error', 'Unknown error')}"
                    )
                elif status == "failed":
                    raise ValueError("Inference failed")

            # If we've reached here, it means we've exceeded the max number of polls
            raise TimeoutError("Inference timed out")

        except httpx.HTTPStatusError as e:
            raise ValueError(f"HTTP error occurred: {e}")
        except httpx.RequestError as e:
            raise ValueError(f"An error occurred while requesting: {e}")
        except asyncio.TimeoutError:
            raise TimeoutError("Request timed out")
        except Exception as e:
            raise ValueError(f"An unexpected error occurred: {e}")



File: backend/image_generation/__init__.py




File: backend/image_generation/core.py

import asyncio
import re
from typing import Dict, List, Literal, Union
from openai import AsyncOpenAI
from bs4 import BeautifulSoup

from image_generation.replicate import call_replicate


async def process_tasks(
    prompts: List[str],
    api_key: str,
    base_url: str | None,
    model: Literal["dalle3", "sdxl-lightning"],
):
    import time

    start_time = time.time()
    if model == "dalle3":
        tasks = [generate_image_dalle(prompt, api_key, base_url) for prompt in prompts]
    else:
        tasks = [generate_image_replicate(prompt, api_key) for prompt in prompts]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    end_time = time.time()
    generation_time = end_time - start_time
    print(f"Image generation time: {generation_time:.2f} seconds")

    processed_results: List[Union[str, None]] = []
    for result in results:
        if isinstance(result, BaseException):
            print(f"An exception occurred: {result}")
            processed_results.append(None)
        else:
            processed_results.append(result)

    return processed_results


async def generate_image_dalle(
    prompt: str, api_key: str, base_url: str | None
) -> Union[str, None]:
    client = AsyncOpenAI(api_key=api_key, base_url=base_url)
    res = await client.images.generate(
        model="dall-e-3",
        quality="standard",
        style="natural",
        n=1,
        size="1024x1024",
        prompt=prompt,
    )
    await client.close()
    return res.data[0].url


async def generate_image_replicate(prompt: str, api_key: str) -> str:

    # We use SDXL Lightning
    return await call_replicate(
        "5f24084160c9089501c1b3545d9be3c27883ae2239b6f412990e82d4a6210f8f",
        {
            "width": 1024,
            "height": 1024,
            "prompt": prompt,
            "scheduler": "K_EULER",
            "num_outputs": 1,
            "guidance_scale": 0,
            "negative_prompt": "worst quality, low quality",
            "num_inference_steps": 4,
        },
        api_key,
    )


def extract_dimensions(url: str):
    # Regular expression to match numbers in the format '300x200'
    matches = re.findall(r"(\d+)x(\d+)", url)

    if matches:
        width, height = matches[0]  # Extract the first match
        width = int(width)
        height = int(height)
        return (width, height)
    else:
        return (100, 100)


def create_alt_url_mapping(code: str) -> Dict[str, str]:
    soup = BeautifulSoup(code, "html.parser")
    images = soup.find_all("img")

    mapping: Dict[str, str] = {}

    for image in images:
        if not image["src"].startswith("https://placehold.co"):
            mapping[image["alt"]] = image["src"]

    return mapping


async def generate_images(
    code: str,
    api_key: str,
    base_url: Union[str, None],
    image_cache: Dict[str, str],
    model: Literal["dalle3", "sdxl-lightning"] = "dalle3",
) -> str:
    # Find all images
    soup = BeautifulSoup(code, "html.parser")
    images = soup.find_all("img")

    # Extract alt texts as image prompts
    alts: List[str | None] = []
    for img in images:
        # Only include URL if the image starts with https://placehold.co
        # and it's not already in the image_cache
        if (
            img["src"].startswith("https://placehold.co")
            and image_cache.get(img.get("alt")) is None
        ):
            alts.append(img.get("alt", None))

    # Exclude images with no alt text
    filtered_alts: List[str] = [alt for alt in alts if alt is not None]

    # Remove duplicates
    prompts = list(set(filtered_alts))

    # Return early if there are no images to replace
    if len(prompts) == 0:
        return code

    # Generate images
    results = await process_tasks(prompts, api_key, base_url, model)

    # Create a dict mapping alt text to image URL
    mapped_image_urls = dict(zip(prompts, results))

    # Merge with image_cache
    mapped_image_urls = {**mapped_image_urls, **image_cache}

    # Replace old image URLs with the generated URLs
    for img in images:
        # Skip images that don't start with https://placehold.co (leave them alone)
        if not img["src"].startswith("https://placehold.co"):
            continue

        new_url = mapped_image_urls[img.get("alt")]

        if new_url:
            # Set width and height attributes
            width, height = extract_dimensions(img["src"])
            img["width"] = width
            img["height"] = height
            # Replace img['src'] with the mapped image URL
            img["src"] = new_url
        else:
            print("Image generation failed for alt text:" + img.get("alt"))

    # Return the modified HTML
    # (need to prettify it because BeautifulSoup messes up the formatting)
    return soup.prettify()



File: backend/codegen/test_utils.py

import unittest
from codegen.utils import extract_html_content


class TestUtils(unittest.TestCase):

    def test_extract_html_content_with_html_tags(self):
        text = "<html><body><p>Hello, World!</p></body></html>"
        expected = "<html><body><p>Hello, World!</p></body></html>"
        result = extract_html_content(text)
        self.assertEqual(result, expected)

    def test_extract_html_content_without_html_tags(self):
        text = "No HTML content here."
        expected = "No HTML content here."
        result = extract_html_content(text)
        self.assertEqual(result, expected)

    def test_extract_html_content_with_partial_html_tags(self):
        text = "<html><body><p>Hello, World!</p></body>"
        expected = "<html><body><p>Hello, World!</p></body>"
        result = extract_html_content(text)
        self.assertEqual(result, expected)

    def test_extract_html_content_with_multiple_html_tags(self):
        text = "<html><body><p>First</p></body></html> Some text <html><body><p>Second</p></body></html>"
        expected = "<html><body><p>First</p></body></html>"
        result = extract_html_content(text)
        self.assertEqual(result, expected)

    ## The following are tests based on actual LLM outputs

    def test_extract_html_content_some_explanation_before(self):
        text = """Got it! You want the song list to be displayed horizontally. I'll update the code to ensure that the song list is displayed in a horizontal layout.

        Here's the updated code:

        <html lang="en"><head></head><body class="bg-black text-white"></body></html>"""
        expected = '<html lang="en"><head></head><body class="bg-black text-white"></body></html>'
        result = extract_html_content(text)
        self.assertEqual(result, expected)

    def test_markdown_tags(self):
        text = "```html<head></head>```"
        expected = "```html<head></head>```"
        result = extract_html_content(text)
        self.assertEqual(result, expected)

    def test_doctype_text(self):
        text = '<!DOCTYPE html><html lang="en"><head></head><body></body></html>'
        expected = '<html lang="en"><head></head><body></body></html>'
        result = extract_html_content(text)
        self.assertEqual(result, expected)


if __name__ == "__main__":
    unittest.main()



File: backend/codegen/__init__.py




File: backend/codegen/utils.py

import re


def extract_html_content(text: str):
    # Use regex to find content within <html> tags and include the tags themselves
    match = re.search(r"(<html.*?>.*?</html>)", text, re.DOTALL)
    if match:
        return match.group(1)
    else:
        # Otherwise, we just send the previous HTML over
        print(
            "[HTML Extraction] No <html> tags found in the generated content: " + text
        )
        return text



File: backend/fs_logging/__init__.py




File: backend/fs_logging/core.py

from datetime import datetime
import json
import os
from openai.types.chat import ChatCompletionMessageParam


def write_logs(prompt_messages: list[ChatCompletionMessageParam], completion: str):
    # Get the logs path from environment, default to the current working directory
    logs_path = os.environ.get("LOGS_PATH", os.getcwd())

    # Create run_logs directory if it doesn't exist within the specified logs path
    logs_directory = os.path.join(logs_path, "run_logs")
    if not os.path.exists(logs_directory):
        os.makedirs(logs_directory)

    print("Writing to logs directory:", logs_directory)

    # Generate a unique filename using the current timestamp within the logs directory
    filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json")

    # Write the messages dict into a new file for each run
    with open(filename, "w") as f:
        f.write(json.dumps({"prompt": prompt_messages, "completion": completion}))



File: backend/prompts/test_prompts.py

from llm import Llm
from prompts import assemble_imported_code_prompt, assemble_prompt

TAILWIND_SYSTEM_PROMPT = """
You are an expert Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Tailwind, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer
You take screenshots of a reference web page from the user, and then build single page apps 
using CSS, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Bootstrap, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using React and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include React so that it can run on a standalone page:
    <script src="https://unpkg.com/react/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IONIC_TAILWIND_SYSTEM_PROMPT = """
You are an expert Ionic/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Ionic and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include Ionic so that it can run on a standalone page:
    <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
    <script type="module">
        import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
    </script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

VUE_TAILWIND_SYSTEM_PROMPT = """
You are an expert Vue/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Vue and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- Use Vue using the global build like so:

<div id="app">{{ message }}</div>
<script>
  const { createApp, ref } = Vue
  createApp({
    setup() {
      const message = ref('Hello vue!')
      return {
        message
      }
    }
  }).mount('#app')
</script>

In terms of libraries,

- Use these script to include Vue so that it can run on a standalone page:
  <script src="https://registry.npmmirror.com/vue/3.3.11/files/dist/vue.global.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
The return result must only include the code.
"""

SVG_SYSTEM_PROMPT = """
You are an expert at building SVGs.
You take screenshots of a reference web page from the user, and then build a SVG that looks exactly like the screenshot.

- Make sure the SVG looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- You can use Google Fonts

Return only the full code in <svg></svg> tags.
Do not include markdown "```" or "```svg" at the start or end.
"""

IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT = """
You are an expert Tailwind developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include React so that it can run on a standalone page:
    <script src="https://unpkg.com/react/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT = """
You are an expert Ionic/Tailwind developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include Ionic so that it can run on a standalone page:
    <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
    <script type="module">
        import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
    </script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""


IMPORTED_CODE_VUE_TAILWIND_PROMPT = """
You are an expert Vue/Tailwind developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include Vue so that it can run on a standalone page:
  <script src="https://registry.npmmirror.com/vue/3.3.11/files/dist/vue.global.js"></script>
- Use Vue using the global build like so:
    <div id="app">{{ message }}</div>
    <script>
    const { createApp, ref } = Vue
    createApp({
        setup() {
        const message = ref('Hello vue!')
        return {
            message
        }
        }
    }).mount('#app')
    </script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
The return result must only include the code."""

IMPORTED_CODE_SVG_SYSTEM_PROMPT = """
You are an expert at building SVGs.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- You can use Google Fonts

Return only the full code in <svg></svg> tags.
Do not include markdown "```" or "```svg" at the start or end.
"""

USER_PROMPT = """
Generate code for a web page that looks exactly like this.
"""

SVG_USER_PROMPT = """
Generate code for a SVG that looks exactly like this.
"""


def test_prompts():
    tailwind_prompt = assemble_prompt(
        "image_data_url", "html_tailwind", "result_image_data_url"
    )
    assert tailwind_prompt[0].get("content") == TAILWIND_SYSTEM_PROMPT
    assert tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT  # type: ignore

    html_css_prompt = assemble_prompt(
        "image_data_url", "html_css", "result_image_data_url"
    )
    assert html_css_prompt[0].get("content") == HTML_CSS_SYSTEM_PROMPT
    assert html_css_prompt[1]["content"][2]["text"] == USER_PROMPT  # type: ignore

    react_tailwind_prompt = assemble_prompt(
        "image_data_url", "react_tailwind", "result_image_data_url"
    )
    assert react_tailwind_prompt[0].get("content") == REACT_TAILWIND_SYSTEM_PROMPT
    assert react_tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT  # type: ignore

    bootstrap_prompt = assemble_prompt(
        "image_data_url", "bootstrap", "result_image_data_url"
    )
    assert bootstrap_prompt[0].get("content") == BOOTSTRAP_SYSTEM_PROMPT
    assert bootstrap_prompt[1]["content"][2]["text"] == USER_PROMPT  # type: ignore

    ionic_tailwind = assemble_prompt(
        "image_data_url", "ionic_tailwind", "result_image_data_url"
    )
    assert ionic_tailwind[0].get("content") == IONIC_TAILWIND_SYSTEM_PROMPT
    assert ionic_tailwind[1]["content"][2]["text"] == USER_PROMPT  # type: ignore

    vue_tailwind = assemble_prompt(
        "image_data_url", "vue_tailwind", "result_image_data_url"
    )
    assert vue_tailwind[0].get("content") == VUE_TAILWIND_SYSTEM_PROMPT
    assert vue_tailwind[1]["content"][2]["text"] == USER_PROMPT  # type: ignore

    svg_prompt = assemble_prompt("image_data_url", "svg", "result_image_data_url")
    assert svg_prompt[0].get("content") == SVG_SYSTEM_PROMPT
    assert svg_prompt[1]["content"][2]["text"] == SVG_USER_PROMPT  # type: ignore


def test_imported_code_prompts():
    code = "Sample code"

    tailwind_prompt = assemble_imported_code_prompt(code, "html_tailwind")
    expected_tailwind_prompt = [
        {
            "role": "system",
            "content": IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT
            + "\n Here is the code of the app: "
            + code,
        }
    ]
    assert tailwind_prompt == expected_tailwind_prompt

    html_css_prompt = assemble_imported_code_prompt(code, "html_css")
    expected_html_css_prompt = [
        {
            "role": "system",
            "content": IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT
            + "\n Here is the code of the app: "
            + code,
        }
    ]
    assert html_css_prompt == expected_html_css_prompt

    react_tailwind_prompt = assemble_imported_code_prompt(code, "react_tailwind")
    expected_react_tailwind_prompt = [
        {
            "role": "system",
            "content": IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT
            + "\n Here is the code of the app: "
            + code,
        }
    ]
    assert react_tailwind_prompt == expected_react_tailwind_prompt

    bootstrap_prompt = assemble_imported_code_prompt(code, "bootstrap")
    expected_bootstrap_prompt = [
        {
            "role": "system",
            "content": IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT
            + "\n Here is the code of the app: "
            + code,
        }
    ]
    assert bootstrap_prompt == expected_bootstrap_prompt

    ionic_tailwind = assemble_imported_code_prompt(code, "ionic_tailwind")
    expected_ionic_tailwind = [
        {
            "role": "system",
            "content": IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT
            + "\n Here is the code of the app: "
            + code,
        }
    ]
    assert ionic_tailwind == expected_ionic_tailwind

    vue_tailwind = assemble_imported_code_prompt(code, "vue_tailwind")
    expected_vue_tailwind = [
        {
            "role": "system",
            "content": IMPORTED_CODE_VUE_TAILWIND_PROMPT
            + "\n Here is the code of the app: "
            + code,
        }
    ]
    assert vue_tailwind == expected_vue_tailwind

    svg = assemble_imported_code_prompt(code, "svg")
    expected_svg = [
        {
            "role": "system",
            "content": IMPORTED_CODE_SVG_SYSTEM_PROMPT
            + "\n Here is the code of the SVG: "
            + code,
        }
    ]
    assert svg == expected_svg



File: backend/prompts/__init__.py

from typing import Union
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam

from custom_types import InputMode
from image_generation.core import create_alt_url_mapping
from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS
from prompts.screenshot_system_prompts import SYSTEM_PROMPTS
from prompts.types import Stack
from video.utils import assemble_claude_prompt_video


USER_PROMPT = """
Generate code for a web page that looks exactly like this.
"""

SVG_USER_PROMPT = """
Generate code for a SVG that looks exactly like this.
"""


async def create_prompt(
    params: dict[str, str], stack: Stack, input_mode: InputMode
) -> tuple[list[ChatCompletionMessageParam], dict[str, str]]:

    image_cache: dict[str, str] = {}

    # If this generation started off with imported code, we need to assemble the prompt differently
    if params.get("isImportedFromCode"):
        original_imported_code = params["history"][0]
        prompt_messages = assemble_imported_code_prompt(original_imported_code, stack)
        for index, text in enumerate(params["history"][1:]):
            if index % 2 == 0:
                message: ChatCompletionMessageParam = {
                    "role": "user",
                    "content": text,
                }
            else:
                message: ChatCompletionMessageParam = {
                    "role": "assistant",
                    "content": text,
                }
            prompt_messages.append(message)
    else:
        # Assemble the prompt for non-imported code
        if params.get("resultImage"):
            prompt_messages = assemble_prompt(
                params["image"], stack, params["resultImage"]
            )
        else:
            prompt_messages = assemble_prompt(params["image"], stack)

        if params["generationType"] == "update":
            # Transform the history tree into message format
            # TODO: Move this to frontend
            for index, text in enumerate(params["history"]):
                if index % 2 == 0:
                    message: ChatCompletionMessageParam = {
                        "role": "assistant",
                        "content": text,
                    }
                else:
                    message: ChatCompletionMessageParam = {
                        "role": "user",
                        "content": text,
                    }
                prompt_messages.append(message)

            image_cache = create_alt_url_mapping(params["history"][-2])

    if input_mode == "video":
        video_data_url = params["image"]
        prompt_messages = await assemble_claude_prompt_video(video_data_url)

    return prompt_messages, image_cache


def assemble_imported_code_prompt(
    code: str, stack: Stack
) -> list[ChatCompletionMessageParam]:
    system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack]

    user_content = (
        "Here is the code of the app: " + code
        if stack != "svg"
        else "Here is the code of the SVG: " + code
    )

    return [
        {
            "role": "system",
            "content": system_content + "\n " + user_content,
        }
    ]
    # TODO: Use result_image_data_url


def assemble_prompt(
    image_data_url: str,
    stack: Stack,
    result_image_data_url: Union[str, None] = None,
) -> list[ChatCompletionMessageParam]:
    system_content = SYSTEM_PROMPTS[stack]
    user_prompt = USER_PROMPT if stack != "svg" else SVG_USER_PROMPT

    user_content: list[ChatCompletionContentPartParam] = [
        {
            "type": "image_url",
            "image_url": {"url": image_data_url, "detail": "high"},
        },
        {
            "type": "text",
            "text": user_prompt,
        },
    ]

    # Include the result image if it exists
    if result_image_data_url:
        user_content.insert(
            1,
            {
                "type": "image_url",
                "image_url": {"url": result_image_data_url, "detail": "high"},
            },
        )
    return [
        {
            "role": "system",
            "content": system_content,
        },
        {
            "role": "user",
            "content": user_content,
        },
    ]



File: backend/prompts/types.py

from typing import Literal, TypedDict


class SystemPrompts(TypedDict):
    html_css: str
    html_tailwind: str
    react_tailwind: str
    bootstrap: str
    ionic_tailwind: str
    vue_tailwind: str
    svg: str


Stack = Literal[
    "html_css",
    "html_tailwind",
    "react_tailwind",
    "bootstrap",
    "ionic_tailwind",
    "vue_tailwind",
    "svg",
]



File: backend/prompts/screenshot_system_prompts.py

from prompts.types import SystemPrompts


HTML_TAILWIND_SYSTEM_PROMPT = """
You are an expert Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Tailwind, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer
You take screenshots of a reference web page from the user, and then build single page apps 
using CSS, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Bootstrap, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using React and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include React so that it can run on a standalone page:
    <script src="https://unpkg.com/react/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IONIC_TAILWIND_SYSTEM_PROMPT = """
You are an expert Ionic/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Ionic and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include Ionic so that it can run on a standalone page:
    <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
    <script type="module">
        import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
    </script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

VUE_TAILWIND_SYSTEM_PROMPT = """
You are an expert Vue/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps 
using Vue and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- Use Vue using the global build like so:

<div id="app">{{ message }}</div>
<script>
  const { createApp, ref } = Vue
  createApp({
    setup() {
      const message = ref('Hello vue!')
      return {
        message
      }
    }
  }).mount('#app')
</script>

In terms of libraries,

- Use these script to include Vue so that it can run on a standalone page:
  <script src="https://registry.npmmirror.com/vue/3.3.11/files/dist/vue.global.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
The return result must only include the code.
"""


SVG_SYSTEM_PROMPT = """
You are an expert at building SVGs.
You take screenshots of a reference web page from the user, and then build a SVG that looks exactly like the screenshot.

- Make sure the SVG looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- You can use Google Fonts

Return only the full code in <svg></svg> tags.
Do not include markdown "```" or "```svg" at the start or end.
"""


SYSTEM_PROMPTS = SystemPrompts(
    html_css=HTML_CSS_SYSTEM_PROMPT,
    html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT,
    react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT,
    bootstrap=BOOTSTRAP_SYSTEM_PROMPT,
    ionic_tailwind=IONIC_TAILWIND_SYSTEM_PROMPT,
    vue_tailwind=VUE_TAILWIND_SYSTEM_PROMPT,
    svg=SVG_SYSTEM_PROMPT,
)



File: backend/prompts/imported_code_prompts.py

from prompts.types import SystemPrompts


IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT = """
You are an expert Tailwind developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include React so that it can run on a standalone page:
    <script src="https://unpkg.com/react/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT = """
You are an expert Ionic/Tailwind developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include Ionic so that it can run on a standalone page:
    <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
    <script type="module">
        import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
    </script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

IMPORTED_CODE_VUE_TAILWIND_SYSTEM_PROMPT = """
You are an expert Vue/Tailwind developer.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include Vue so that it can run on a standalone page:
  <script src="https://registry.npmmirror.com/vue/3.3.11/files/dist/vue.global.js"></script>
- Use Vue using the global build like so:
    <div id="app">{{ message }}</div>
    <script>
    const { createApp, ref } = Vue
    createApp({
        setup() {
        const message = ref('Hello vue!')
        return {
            message
        }
        }
    }).mount('#app')
    </script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
The return result must only include the code."""

IMPORTED_CODE_SVG_SYSTEM_PROMPT = """
You are an expert at building SVGs.

- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- You can use Google Fonts

Return only the full code in <svg></svg> tags.
Do not include markdown "```" or "```svg" at the start or end.
"""

IMPORTED_CODE_SYSTEM_PROMPTS = SystemPrompts(
    html_tailwind=IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT,
    html_css=IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT,
    react_tailwind=IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT,
    bootstrap=IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT,
    ionic_tailwind=IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT,
    vue_tailwind=IMPORTED_CODE_VUE_TAILWIND_SYSTEM_PROMPT,
    svg=IMPORTED_CODE_SVG_SYSTEM_PROMPT,
)



File: backend/prompts/claude_prompts.py

# Not used yet
# References:
# https://github.com/hundredblocks/transcription_demo
# https://docs.anthropic.com/claude/docs/prompt-engineering
# https://github.com/anthropics/anthropic-cookbook/blob/main/multimodal/best_practices_for_vision.ipynb

VIDEO_PROMPT = """
You are an expert at building single page, funtional apps using HTML, Jquery and Tailwind CSS.
You also have perfect vision and pay great attention to detail.

You will be given screenshots in order at consistent intervals from a video of a user interacting with a web app. You need to re-create the same app exactly such that the same user interactions will produce the same results in the app you build.

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- If some fuctionality requires a backend call, just mock the data instead.
- MAKE THE APP FUNCTIONAL using Javascript. Allow the user to interact with the app and get the same behavior as the video.

In terms of libraries,

- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
- Use jQuery: <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

Before generating the code for the app, think step-by-step: first, about the user flow depicated in the video and then about you how would you build it and how you would structure the code. Do the thinking within <thinking></thinking> tags. Then, provide your code within <html></html> tags.
"""

VIDEO_PROMPT_ALPINE_JS = """
You are an expert at building single page, funtional apps using HTML, Alpine.js and Tailwind CSS.
You also have perfect vision and pay great attention to detail.

You will be given screenshots in order at consistent intervals from a video of a user interacting with a web app. You need to re-create the same app exactly such that the same user interactions will produce the same results in the app you build.

- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
- If some fuctionality requires a backend call, just mock the data instead.
- MAKE THE APP FUNCTIONAL using Javascript. Allow the user to interact with the app and get the same behavior as the video.

In terms of libraries,

- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
- Use Alpine.js: <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

Before generating the code for the app, think step-by-step: first, about the user flow depicated in the video and then about you how would you build it and how you would structure the code. Do the thinking within <thinking></thinking> tags. Then, provide your code within <html></html> tags.
"""


HTML_TAILWIND_CLAUDE_SYSTEM_PROMPT = """
You have perfect vision and pay great attention to detail which makes you an expert at building single page apps using Tailwind, HTML and JS.
You take screenshots of a reference web page from the user, and then build single page apps 
using Tailwind, HTML and JS.
You might also be given a screenshot (The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Do not leave out smaller UI elements. Make sure to include every single thing in the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- In particular, pay attention to background color and overall color scheme.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Make sure to always get the layout right (if things are arranged in a row in the screenshot, they should be in a row in the app as well)
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""

#

REACT_TAILWIND_CLAUDE_SYSTEM_PROMPT = """
You have perfect vision and pay great attention to detail which makes you an expert at building single page apps using React/Tailwind.
You take screenshots of a reference web page from the user, and then build single page apps 
using React and Tailwind CSS.
You might also be given a screenshot (The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).

- Make sure the app looks exactly like the screenshot.
- Do not leave out smaller UI elements. Make sure to include every single thing in the screenshot.
- Pay close attention to background color, text color, font size, font family, 
padding, margin, border, etc. Match the colors and sizes exactly.
- In particular, pay attention to background color and overall color scheme.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Make sure to always get the layout right (if things are arranged in a row in the screenshot, they should be in a row in the app as well)
- CREATE REUSABLE COMPONENTS FOR REPEATING ELEMENTS. For example, if there are 15 similar items in the screenshot, your code should include a reusable component that generates these items. and use loops to instantiate these components as needed.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.

In terms of libraries,

- Use these script to include React so that it can run on a standalone page:
    <script src="https://unpkg.com/react/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>

Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""



File: backend/routes/generate_code.py

import asyncio
from dataclasses import dataclass
from fastapi import APIRouter, WebSocket
import openai
from codegen.utils import extract_html_content
from config import (
    ANTHROPIC_API_KEY,
    IS_PROD,
    NUM_VARIANTS,
    OPENAI_API_KEY,
    OPENAI_BASE_URL,
    REPLICATE_API_KEY,
    SHOULD_MOCK_AI_RESPONSE,
)
from custom_types import InputMode
from llm import (
    Llm,
    convert_frontend_str_to_llm,
    stream_claude_response,
    stream_claude_response_native,
    stream_openai_response,
)
from fs_logging.core import write_logs
from mock_llm import mock_completion
from typing import Any, Callable, Coroutine, Dict, List, Literal, cast, get_args
from image_generation.core import generate_images
from prompts import create_prompt
from prompts.claude_prompts import VIDEO_PROMPT
from prompts.types import Stack

# from utils import pprint_prompt
from ws.constants import APP_ERROR_WEB_SOCKET_CODE  # type: ignore


router = APIRouter()


# Auto-upgrade usage of older models
def auto_upgrade_model(code_generation_model: Llm) -> Llm:
    if code_generation_model in {Llm.GPT_4_VISION, Llm.GPT_4_TURBO_2024_04_09}:
        print(
            f"Initial deprecated model: {code_generation_model}. Auto-updating code generation model to GPT-4O-2024-05-13"
        )
        return Llm.GPT_4O_2024_05_13
    elif code_generation_model == Llm.CLAUDE_3_SONNET:
        print(
            f"Initial deprecated model: {code_generation_model}. Auto-updating code generation model to CLAUDE-3.5-SONNET-2024-06-20"
        )
        return Llm.CLAUDE_3_5_SONNET_2024_06_20
    return code_generation_model


# Generate images, if needed
async def perform_image_generation(
    completion: str,
    should_generate_images: bool,
    openai_api_key: str | None,
    openai_base_url: str | None,
    image_cache: dict[str, str],
):
    replicate_api_key = REPLICATE_API_KEY
    if not should_generate_images:
        return completion

    if replicate_api_key:
        image_generation_model = "sdxl-lightning"
        api_key = replicate_api_key
    else:
        if not openai_api_key:
            print(
                "No OpenAI API key and Replicate key found. Skipping image generation."
            )
            return completion
        image_generation_model = "dalle3"
        api_key = openai_api_key

    print("Generating images with model: ", image_generation_model)

    return await generate_images(
        completion,
        api_key=api_key,
        base_url=openai_base_url,
        image_cache=image_cache,
        model=image_generation_model,
    )


@dataclass
class ExtractedParams:
    stack: Stack
    input_mode: InputMode
    code_generation_model: Llm
    should_generate_images: bool
    openai_api_key: str | None
    anthropic_api_key: str | None
    openai_base_url: str | None


async def extract_params(
    params: Dict[str, str], throw_error: Callable[[str], Coroutine[Any, Any, None]]
) -> ExtractedParams:
    # Read the code config settings (stack) from the request.
    generated_code_config = params.get("generatedCodeConfig", "")
    if generated_code_config not in get_args(Stack):
        await throw_error(f"Invalid generated code config: {generated_code_config}")
        raise ValueError(f"Invalid generated code config: {generated_code_config}")
    validated_stack = cast(Stack, generated_code_config)

    # Validate the input mode
    input_mode = params.get("inputMode")
    if input_mode not in get_args(InputMode):
        await throw_error(f"Invalid input mode: {input_mode}")
        raise ValueError(f"Invalid input mode: {input_mode}")
    validated_input_mode = cast(InputMode, input_mode)

    # Read the model from the request. Fall back to default if not provided.
    code_generation_model_str = params.get(
        "codeGenerationModel", Llm.GPT_4O_2024_05_13.value
    )
    try:
        code_generation_model = convert_frontend_str_to_llm(code_generation_model_str)
    except ValueError:
        await throw_error(f"Invalid model: {code_generation_model_str}")
        raise ValueError(f"Invalid model: {code_generation_model_str}")

    openai_api_key = get_from_settings_dialog_or_env(
        params, "openAiApiKey", OPENAI_API_KEY
    )

    # If neither is provided, we throw an error later only if Claude is used.
    anthropic_api_key = get_from_settings_dialog_or_env(
        params, "anthropicApiKey", ANTHROPIC_API_KEY
    )

    # Base URL for OpenAI API
    openai_base_url: str | None = None
    # Disable user-specified OpenAI Base URL in prod
    if not IS_PROD:
        openai_base_url = get_from_settings_dialog_or_env(
            params, "openAiBaseURL", OPENAI_BASE_URL
        )
    if not openai_base_url:
        print("Using official OpenAI URL")

    # Get the image generation flag from the request. Fall back to True if not provided.
    should_generate_images = bool(params.get("isImageGenerationEnabled", True))

    return ExtractedParams(
        stack=validated_stack,
        input_mode=validated_input_mode,
        code_generation_model=code_generation_model,
        should_generate_images=should_generate_images,
        openai_api_key=openai_api_key,
        anthropic_api_key=anthropic_api_key,
        openai_base_url=openai_base_url,
    )


def get_from_settings_dialog_or_env(
    params: dict[str, str], key: str, env_var: str | None
) -> str | None:
    value = params.get(key)
    if value:
        print(f"Using {key} from client-side settings dialog")
        return value

    if env_var:
        print(f"Using {key} from environment variable")
        return env_var

    return None


@router.websocket("/generate-code")
async def stream_code(websocket: WebSocket):
    await websocket.accept()
    print("Incoming websocket connection...")

    ## Communication protocol setup
    async def throw_error(
        message: str,
    ):
        print(message)
        await websocket.send_json({"type": "error", "value": message})
        await websocket.close(APP_ERROR_WEB_SOCKET_CODE)

    async def send_message(
        type: Literal["chunk", "status", "setCode", "error"],
        value: str,
        variantIndex: int,
    ):
        # Print for debugging on the backend
        if type == "error":
            print(f"Error (variant {variantIndex}): {value}")
        elif type == "status":
            print(f"Status (variant {variantIndex}): {value}")

        await websocket.send_json(
            {"type": type, "value": value, "variantIndex": variantIndex}
        )

    ## Parameter extract and validation

    # TODO: Are the values always strings?
    params: dict[str, str] = await websocket.receive_json()
    print("Received params")

    extracted_params = await extract_params(params, throw_error)
    stack = extracted_params.stack
    input_mode = extracted_params.input_mode
    code_generation_model = extracted_params.code_generation_model
    openai_api_key = extracted_params.openai_api_key
    openai_base_url = extracted_params.openai_base_url
    anthropic_api_key = extracted_params.anthropic_api_key
    should_generate_images = extracted_params.should_generate_images

    # Auto-upgrade usage of older models
    code_generation_model = auto_upgrade_model(code_generation_model)

    print(
        f"Generating {stack} code in {input_mode} mode using {code_generation_model}..."
    )

    for i in range(NUM_VARIANTS):
        await send_message("status", "Generating code...", i)

    ### Prompt creation

    # Image cache for updates so that we don't have to regenerate images
    image_cache: Dict[str, str] = {}

    try:
        prompt_messages, image_cache = await create_prompt(params, stack, input_mode)
    except:
        await throw_error(
            "Error assembling prompt. Contact support at support@picoapps.xyz"
        )
        raise

    # pprint_prompt(prompt_messages)  # type: ignore

    ### Code generation

    async def process_chunk(content: str, variantIndex: int):
        await send_message("chunk", content, variantIndex)

    if SHOULD_MOCK_AI_RESPONSE:
        completions = [await mock_completion(process_chunk, input_mode=input_mode)]
    else:
        try:
            if input_mode == "video":
                if not anthropic_api_key:
                    await throw_error(
                        "Video only works with Anthropic models. No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog"
                    )
                    raise Exception("No Anthropic key")

                completions = [
                    await stream_claude_response_native(
                        system_prompt=VIDEO_PROMPT,
                        messages=prompt_messages,  # type: ignore
                        api_key=anthropic_api_key,
                        callback=lambda x: process_chunk(x, 0),
                        model=Llm.CLAUDE_3_OPUS,
                        include_thinking=True,
                    )
                ]
            else:

                # Depending on the presence and absence of various keys,
                # we decide which models to run
                variant_models = []
                if openai_api_key and anthropic_api_key:
                    variant_models = ["openai", "anthropic"]
                elif openai_api_key:
                    variant_models = ["openai", "openai"]
                elif anthropic_api_key:
                    variant_models = ["anthropic", "anthropic"]
                else:
                    await throw_error(
                        "No OpenAI or Anthropic API key found. Please add the environment variable OPENAI_API_KEY or ANTHROPIC_API_KEY to backend/.env or in the settings dialog. If you add it to .env, make sure to restart the backend server."
                    )
                    raise Exception("No OpenAI or Anthropic key")

                tasks: List[Coroutine[Any, Any, str]] = []
                for index, model in enumerate(variant_models):
                    if model == "openai":
                        if openai_api_key is None:
                            await throw_error("OpenAI API key is missing.")
                            raise Exception("OpenAI API key is missing.")

                        tasks.append(
                            stream_openai_response(
                                prompt_messages,
                                api_key=openai_api_key,
                                base_url=openai_base_url,
                                callback=lambda x, i=index: process_chunk(x, i),
                                model=Llm.GPT_4O_2024_05_13,
                            )
                        )
                    elif model == "anthropic":
                        if anthropic_api_key is None:
                            await throw_error("Anthropic API key is missing.")
                            raise Exception("Anthropic API key is missing.")

                        tasks.append(
                            stream_claude_response(
                                prompt_messages,
                                api_key=anthropic_api_key,
                                callback=lambda x, i=index: process_chunk(x, i),
                                model=Llm.CLAUDE_3_5_SONNET_2024_06_20,
                            )
                        )

                completions = await asyncio.gather(*tasks)
                print("Models used for generation: ", variant_models)

        except openai.AuthenticationError as e:
            print("[GENERATE_CODE] Authentication failed", e)
            error_message = (
                "Incorrect OpenAI key. Please make sure your OpenAI API key is correct, or create a new OpenAI API key on your OpenAI dashboard."
                + (
                    " Alternatively, you can purchase code generation credits directly on this website."
                    if IS_PROD
                    else ""
                )
            )
            return await throw_error(error_message)
        except openai.NotFoundError as e:
            print("[GENERATE_CODE] Model not found", e)
            error_message = (
                e.message
                + ". Please make sure you have followed the instructions correctly to obtain an OpenAI key with GPT vision access: https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
                + (
                    " Alternatively, you can purchase code generation credits directly on this website."
                    if IS_PROD
                    else ""
                )
            )
            return await throw_error(error_message)
        except openai.RateLimitError as e:
            print("[GENERATE_CODE] Rate limit exceeded", e)
            error_message = (
                "OpenAI error - 'You exceeded your current quota, please check your plan and billing details.'"
                + (
                    " Alternatively, you can purchase code generation credits directly on this website."
                    if IS_PROD
                    else ""
                )
            )
            return await throw_error(error_message)

    ## Post-processing

    # Strip the completion of everything except the HTML content
    completions = [extract_html_content(completion) for completion in completions]

    # Write the messages dict into a log so that we can debug later
    write_logs(prompt_messages, completions[0])

    ## Image Generation

    for index, _ in enumerate(completions):
        await send_message("status", "Generating images...", index)

    image_generation_tasks = [
        perform_image_generation(
            completion,
            should_generate_images,
            openai_api_key,
            openai_base_url,
            image_cache,
        )
        for completion in completions
    ]

    updated_completions = await asyncio.gather(*image_generation_tasks)

    for index, updated_html in enumerate(updated_completions):
        await send_message("setCode", updated_html, index)
        await send_message("status", "Code generation complete.", index)

    await websocket.close()



File: backend/routes/home.py

from fastapi import APIRouter
from fastapi.responses import HTMLResponse


router = APIRouter()


@router.get("/")
async def get_status():
    return HTMLResponse(
        content="<h3>Your backend is running correctly. Please open the front-end URL (default is http://localhost:5173) to use screenshot-to-code.</h3>"
    )



File: backend/routes/evals.py

import os
from fastapi import APIRouter
from pydantic import BaseModel
from evals.utils import image_to_data_url
from evals.config import EVALS_DIR


router = APIRouter()

# Update this if the number of outputs generated per input changes
N = 1


class Eval(BaseModel):
    input: str
    outputs: list[str]


@router.get("/evals")
async def get_evals():
    # Get all evals from EVALS_DIR
    input_dir = EVALS_DIR + "/inputs"
    output_dir = EVALS_DIR + "/outputs"

    evals: list[Eval] = []
    for file in os.listdir(input_dir):
        if file.endswith(".png"):
            input_file_path = os.path.join(input_dir, file)
            input_file = await image_to_data_url(input_file_path)

            # Construct the corresponding output file names
            output_file_names = [
                file.replace(".png", f"_{i}.html") for i in range(0, N)
            ]  # Assuming 3 outputs for each input

            output_files_data: list[str] = []
            for output_file_name in output_file_names:
                output_file_path = os.path.join(output_dir, output_file_name)
                # Check if the output file exists
                if os.path.exists(output_file_path):
                    with open(output_file_path, "r") as f:
                        output_files_data.append(f.read())
                else:
                    output_files_data.append(
                        "<html><h1>Output file not found.</h1></html>"
                    )

            evals.append(
                Eval(
                    input=input_file,
                    outputs=output_files_data,
                )
            )

    return evals



File: backend/routes/screenshot.py

import base64
from fastapi import APIRouter
from pydantic import BaseModel
import httpx

router = APIRouter()


def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str:
    base64_image = base64.b64encode(image_bytes).decode("utf-8")
    return f"data:{mime_type};base64,{base64_image}"


async def capture_screenshot(
    target_url: str, api_key: str, device: str = "desktop"
) -> bytes:
    api_base_url = "https://api.screenshotone.com/take"

    params = {
        "access_key": api_key,
        "url": target_url,
        "full_page": "true",
        "device_scale_factor": "1",
        "format": "png",
        "block_ads": "true",
        "block_cookie_banners": "true",
        "block_trackers": "true",
        "cache": "false",
        "viewport_width": "342",
        "viewport_height": "684",
    }

    if device == "desktop":
        params["viewport_width"] = "1280"
        params["viewport_height"] = "832"

    async with httpx.AsyncClient(timeout=60) as client:
        response = await client.get(api_base_url, params=params)
        if response.status_code == 200 and response.content:
            return response.content
        else:
            raise Exception("Error taking screenshot")


class ScreenshotRequest(BaseModel):
    url: str
    apiKey: str


class ScreenshotResponse(BaseModel):
    url: str


@router.post("/api/screenshot")
async def app_screenshot(request: ScreenshotRequest):
    # Extract the URL from the request body
    url = request.url
    api_key = request.apiKey

    # TODO: Add error handling
    image_bytes = await capture_screenshot(url, api_key=api_key)

    # Convert the image bytes to a data url
    data_url = bytes_to_data_url(image_bytes, "image/png")

    return ScreenshotResponse(url=data_url)



File: backend/ws/constants.py

# WebSocket protocol (RFC 6455) allows for the use of custom close codes in the range 4000-4999
APP_ERROR_WEB_SOCKET_CODE = 4332



File: backend/ws/__init__.py




File: backend/debug/__init__.py




File: backend/debug/DebugFileWriter.py

import os
import logging
import uuid

from config import DEBUG_DIR, IS_DEBUG_ENABLED


class DebugFileWriter:
    def __init__(self):
        if not IS_DEBUG_ENABLED:
            return

        try:
            self.debug_artifacts_path = os.path.expanduser(
                f"{DEBUG_DIR}/{str(uuid.uuid4())}"
            )
            os.makedirs(self.debug_artifacts_path, exist_ok=True)
            print(f"Debugging artifacts will be stored in: {self.debug_artifacts_path}")
        except:
            logging.error("Failed to create debug directory")

    def write_to_file(self, filename: str, content: str) -> None:
        try:
            with open(os.path.join(self.debug_artifacts_path, filename), "w") as file:
                file.write(content)
        except Exception as e:
            logging.error(f"Failed to write to file: {e}")

    def extract_html_content(self, text: str) -> str:
        return str(text.split("<html>")[-1].rsplit("</html>", 1)[0] + "</html>")



File: backend/image_processing/__init__.py




File: backend/image_processing/utils.py

import base64
import io
import time
from PIL import Image

CLAUDE_IMAGE_MAX_SIZE = 5 * 1024 * 1024
CLAUDE_MAX_IMAGE_DIMENSION = 7990


# Process image so it meets Claude requirements
def process_image(image_data_url: str) -> tuple[str, str]:

    # Extract bytes and media type from base64 data URL
    media_type = image_data_url.split(";")[0].split(":")[1]
    base64_data = image_data_url.split(",")[1]
    image_bytes = base64.b64decode(base64_data)

    img = Image.open(io.BytesIO(image_bytes))

    # Check if image is under max dimensions and size
    is_under_dimension_limit = (
        img.width < CLAUDE_MAX_IMAGE_DIMENSION
        and img.height < CLAUDE_MAX_IMAGE_DIMENSION
    )
    is_under_size_limit = len(base64_data) <= CLAUDE_IMAGE_MAX_SIZE

    # If image is under both limits, no processing needed
    if is_under_dimension_limit and is_under_size_limit:
        print("[CLAUDE IMAGE PROCESSING] no processing needed")
        return (media_type, base64_data)

    # Time image processing
    start_time = time.time()

    # Check if either dimension exceeds 7900px (Claude disallows >= 8000px)
    # Resize image if needed
    if not is_under_dimension_limit:
        # Calculate the new dimensions while maintaining aspect ratio
        if img.width > img.height:
            new_width = CLAUDE_MAX_IMAGE_DIMENSION
            new_height = int((CLAUDE_MAX_IMAGE_DIMENSION / img.width) * img.height)
        else:
            new_height = CLAUDE_MAX_IMAGE_DIMENSION
            new_width = int((CLAUDE_MAX_IMAGE_DIMENSION / img.height) * img.width)

        # Resize the image
        img = img.resize((new_width, new_height), Image.DEFAULT_STRATEGY)
        print(
            f"[CLAUDE IMAGE PROCESSING] image resized: width = {new_width}, height = {new_height}"
        )

    # Convert and compress as JPEG
    # We always compress as JPEG (95% at the least) even when we resize and the original image
    # is under the size limit.
    quality = 95
    output = io.BytesIO()
    img = img.convert("RGB")  # Ensure image is in RGB mode for JPEG conversion
    img.save(output, format="JPEG", quality=quality)

    # Reduce quality until image is under max size
    while (
        len(base64.b64encode(output.getvalue())) > CLAUDE_IMAGE_MAX_SIZE
        and quality > 10
    ):
        output = io.BytesIO()
        img.save(output, format="JPEG", quality=quality)
        quality -= 5

    # Log so we know it was modified
    old_size = len(base64_data)
    new_size = len(base64.b64encode(output.getvalue()))
    print(
        f"[CLAUDE IMAGE PROCESSING] image size updated: old size = {old_size} bytes, new size = {new_size} bytes"
    )

    end_time = time.time()
    processing_time = end_time - start_time
    print(f"[CLAUDE IMAGE PROCESSING] processing time: {processing_time:.2f} seconds")

    return ("image/jpeg", base64.b64encode(output.getvalue()).decode("utf-8"))



# screenshot-to-code

A simple tool to convert screenshots, mockups and Figma designs into clean, functional code using AI. **Now supporting Claude Sonnet 3.5 and GPT-4O!**

https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045

Supported stacks:

- HTML + Tailwind
- HTML + CSS
- React + Tailwind
- Vue + Tailwind
- Bootstrap
- Ionic + Tailwind
- SVG

Supported AI models:

- Claude Sonnet 3.5 - Best model!
- GPT-4O - also recommended!
- GPT-4 Turbo (Apr 2024)
- GPT-4 Vision (Nov 2023)
- Claude 3 Sonnet
- DALL-E 3 for image generation

See the [Examples](#-examples) section below for more demos.

We also just added experimental support for taking a video/screen recording of a website in action and turning that into a functional prototype.

![google in app quick 3](https://github.com/abi/screenshot-to-code/assets/23818/8758ffa4-9483-4b9b-bb66-abd6d1594c33)

[Learn more about video here](https://github.com/abi/screenshot-to-code/wiki/Screen-Recording-to-Code).

[Follow me on Twitter for updates](https://twitter.com/_abi_).

## 🚀 Hosted Version

[Try it live on the hosted version (paid)](https://screenshottocode.com).

## 🛠 Getting Started

The app has a React/Vite frontend and a FastAPI backend.

Keys needed:

- [OpenAI API key with access to GPT-4](https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md)
- Anthropic key (optional) - only if you want to use Claude Sonnet, or for experimental video support.

Run the backend (I use Poetry for package management - `pip install poetry` if you don't have it):

```bash
cd backend
echo "OPENAI_API_KEY=sk-your-key" > .env
poetry install
poetry shell
poetry run uvicorn main:app --reload --port 7001
```

If you want to use Anthropic, add `ANTHROPIC_API_KEY` to `backend/.env`. You can also set up the keys using the settings dialog on the front-end (click the gear icon after loading the frontend).

Run the frontend:

```bash
cd frontend
yarn
yarn dev
```

Open http://localhost:5173 to use the app.

If you prefer to run the backend on a different port, update VITE_WS_BACKEND_URL in `frontend/.env.local`

For debugging purposes, if you don't want to waste GPT4-Vision credits, you can run the backend in mock mode (which streams a pre-recorded response):

```bash
MOCK=true poetry run uvicorn main:app --reload --port 7001
```

## Docker

If you have Docker installed on your system, in the root directory, run:

```bash
echo "OPENAI_API_KEY=sk-your-key" > .env
docker-compose up -d --build
```

The app will be up and running at http://localhost:5173. Note that you can't develop the application with this setup as the file changes won't trigger a rebuild.

## 🙋‍♂️ FAQs

- **I'm running into an error when setting up the backend. How can I fix it?** [Try this](https://github.com/abi/screenshot-to-code/issues/3#issuecomment-1814777959). If that still doesn't work, open an issue.
- **How do I get an OpenAI API key?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md
- **How can I configure an OpenAI proxy?** - If you're not able to access the OpenAI API directly (due to e.g. country restrictions), you can try a VPN or you can configure the OpenAI base URL to use a proxy: Set OPENAI_BASE_URL in the `backend/.env` or directly in the UI in the settings dialog. Make sure the URL has "v1" in the path so it should look like this: `https://xxx.xxxxx.xxx/v1`
- **How can I update the backend host that my front-end connects to?** - Configure VITE_HTTP_BACKEND_URL and VITE_WS_BACKEND_URL in front/.env.local For example, set VITE_HTTP_BACKEND_URL=http://124.10.20.1:7001
- **Seeing UTF-8 errors when running the backend?** - On windows, open the .env file with notepad++, then go to Encoding and select UTF-8.
- **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_).

## 📚 Examples

**NYTimes**

| Original                                                                                                                                                        | Replica                                                                                                                                                         |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <img width="1238" alt="Screenshot 2023-11-20 at 12 54 03 PM" src="https://github.com/abi/screenshot-to-code/assets/23818/3b644dfa-9ca6-4148-84a7-3405b6671922"> | <img width="1414" alt="Screenshot 2023-11-20 at 12 59 56 PM" src="https://github.com/abi/screenshot-to-code/assets/23818/26201c9f-1a28-4f35-a3b1-1f04e2b8ce2a"> |

**Instagram page (with not Taylor Swift pics)**

https://github.com/abi/screenshot-to-code/assets/23818/503eb86a-356e-4dfc-926a-dabdb1ac7ba1

**Hacker News** but it gets the colors wrong at first so we nudge it

https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-ac7410315e5d

## 🌍 Hosted Version

🆕 [Try it here (paid)](https://screenshottocode.com). Or see [Getting Started](#-getting-started) for local install instructions to use with your own API keys.
