├── backend ├── evals │ ├── __init__.py │ ├── config.py │ ├── utils.py │ └── core.py ├── pyrightconfig.json ├── README.md ├── start.py ├── build.sh ├── routes │ ├── home.py │ ├── evals.py │ ├── screenshot.py │ └── generate_code.py ├── prompts │ ├── types.py │ ├── __init__.py │ ├── imported_code_prompts.py │ └── screenshot_system_prompts.py ├── config.py ├── Dockerfile ├── .pre-commit-config.yaml ├── pyproject.toml ├── access_token.py ├── main.py ├── llm.py ├── run_evals.py ├── utils.py ├── .gitignore ├── image_generation.py └── mock_llm.py ├── frontend ├── .env.example ├── src │ ├── vite-env.d.ts │ ├── constants.ts │ ├── lib │ │ ├── utils.ts │ │ └── stacks.ts │ ├── components │ │ ├── ui │ │ │ ├── collapsible.tsx │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── separator.tsx │ │ │ ├── progress.tsx │ │ │ ├── input.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── switch.tsx │ │ │ ├── badge.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── popover.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── button.tsx │ │ │ ├── tabs.tsx │ │ │ ├── accordion.tsx │ │ │ ├── dialog.tsx │ │ │ ├── alert-dialog.tsx │ │ │ └── select.tsx │ │ ├── CodePreview.tsx │ │ ├── history │ │ │ ├── history_types.ts │ │ │ ├── utils.ts │ │ │ ├── HistoryDisplay.tsx │ │ │ └── utils.test.ts │ │ ├── OnboardingNote.tsx │ │ ├── evals │ │ │ ├── RatingPicker.tsx │ │ │ └── EvalsPage.tsx │ │ ├── PicoBadge.tsx │ │ ├── Preview.tsx │ │ ├── Spinner.tsx │ │ ├── ImportCodeSection.tsx │ │ ├── OutputSettingsSection.tsx │ │ ├── UrlInputSection.tsx │ │ ├── CodeMirror.tsx │ │ ├── CodeTab.tsx │ │ ├── TermsOfServiceDialog.tsx │ │ ├── settings │ │ │ └── AccessCodeSection.tsx │ │ ├── ImageUpload.tsx │ │ └── SettingsDialog.tsx │ ├── config.ts │ ├── hooks │ │ ├── usePersistedState.ts │ │ └── useThrottle.ts │ ├── main.tsx │ ├── types.ts │ ├── generateCode.ts │ ├── index.css │ └── App.tsx ├── postcss.config.js ├── public │ └── brand │ │ └── twitter-summary-card.png ├── tsconfig.node.json ├── .gitignore ├── components.json ├── Dockerfile ├── .eslintrc.cjs ├── tsconfig.json ├── vite.config.ts ├── index.html ├── tailwind.config.js └── package.json ├── .gitattributes ├── .vscode └── settings.json ├── design-docs.md ├── .gitignore ├── docker-compose.yml ├── LICENSE ├── Troubleshooting.md ├── sweep.yaml └── README.md /backend/evals/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/evals/config.py: -------------------------------------------------------------------------------- 1 | EVALS_DIR = "./evals_data" 2 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_WS_BACKEND_URL=ws://127.0.0.1:7001 2 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const USER_CLOSE_WEB_SOCKET_CODE = 4333; 2 | -------------------------------------------------------------------------------- /backend/pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["image_generation.py"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "strict" 3 | } 4 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Run the type checker 2 | 3 | poetry run pyright 4 | 5 | # Run tests 6 | 7 | poetry run pytest 8 | -------------------------------------------------------------------------------- /backend/start.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("main:app", port=7001, reload=True) 5 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/brand/twitter-summary-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hacksider/screenshot-to-code/HEAD/frontend/public/brand/twitter-summary-card.png -------------------------------------------------------------------------------- /design-docs.md: -------------------------------------------------------------------------------- 1 | ## Version History 2 | 3 | Version history is stored as a tree on the client-side. 4 | 5 | ![Screenshot to Code](https://github.com/abi/screenshot-to-code/assets/23818/e35644aa-b90a-4aa7-8027-b8732796fd7c) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .aider* 2 | 3 | # Project-related files 4 | 5 | # Run logs 6 | backend/run_logs/* 7 | 8 | # Weird Docker setup related files 9 | backend/backend/* 10 | 11 | # Env vars 12 | frontend/.env.local 13 | .env 14 | 15 | # Mac files 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /backend/evals/utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | async def image_to_data_url(filepath: str): 5 | with open(filepath, "rb") as image_file: 6 | encoded_string = base64.b64encode(image_file.read()).decode() 7 | return f"data:image/png;base64,{encoded_string}" 8 | -------------------------------------------------------------------------------- /backend/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # exit on error 3 | set -o errexit 4 | 5 | echo "Installing the latest version of poetry..." 6 | pip install --upgrade pip 7 | pip install poetry==1.4.1 8 | 9 | rm poetry.lock 10 | poetry lock 11 | python -m poetry install 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function capitalize(str: string) { 9 | return str.charAt(0).toUpperCase() + str.slice(1); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /backend/routes/home.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastapi.responses import HTMLResponse 3 | 4 | 5 | router = APIRouter() 6 | 7 | 8 | @router.get("/") 9 | async def get_status(): 10 | return HTMLResponse( 11 | content="

Your backend is running correctly. Please open the front-end URL (default is http://localhost:5173) to use screenshot-to-code.

" 12 | ) 13 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Env files 27 | .env* 28 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/prompts/types.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, TypedDict 2 | 3 | 4 | class SystemPrompts(TypedDict): 5 | html_tailwind: str 6 | react_tailwind: str 7 | bootstrap: str 8 | ionic_tailwind: str 9 | vue_tailwind: str 10 | svg: str 11 | 12 | 13 | Stack = Literal[ 14 | "html_tailwind", 15 | "react_tailwind", 16 | "bootstrap", 17 | "ionic_tailwind", 18 | "vue_tailwind", 19 | "svg", 20 | ] 21 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.9-bullseye-slim 2 | 3 | # Set the working directory in the container 4 | WORKDIR /app 5 | 6 | # Copy package.json and yarn.lock 7 | COPY package.json yarn.lock /app/ 8 | 9 | # Install dependencies 10 | RUN yarn install 11 | 12 | # Copy the current directory contents into the container at /app 13 | COPY ./ /app/ 14 | 15 | # Expose port 5173 to access the server 16 | EXPOSE 5173 17 | 18 | # Command to run the application 19 | CMD ["yarn", "dev", "--host", "0.0.0.0"] 20 | -------------------------------------------------------------------------------- /frontend/src/config.ts: -------------------------------------------------------------------------------- 1 | // Default to false if set to anything other than "true" or unset 2 | export const IS_RUNNING_ON_CLOUD = 3 | import.meta.env.VITE_IS_DEPLOYED === "true" || false; 4 | 5 | export const WS_BACKEND_URL = 6 | import.meta.env.VITE_WS_BACKEND_URL || "ws://127.0.0.1:7001"; 7 | 8 | export const HTTP_BACKEND_URL = 9 | import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001"; 10 | 11 | export const PICO_BACKEND_FORM_SECRET = 12 | import.meta.env.VITE_PICO_BACKEND_FORM_SECRET || null; 13 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /backend/config.py: -------------------------------------------------------------------------------- 1 | # Useful for debugging purposes when you don't want to waste GPT4-Vision credits 2 | # Setting to True will stream a mock response instead of calling the OpenAI API 3 | # TODO: Should only be set to true when value is 'True', not any abitrary truthy value 4 | import os 5 | 6 | 7 | SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) 8 | 9 | # Set to True when running in production (on the hosted version) 10 | # Used as a feature flag to enable or disable certain features 11 | IS_PROD = os.environ.get("IS_PROD", False) 12 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim-bullseye 2 | 3 | ENV POETRY_VERSION 1.4.1 4 | 5 | # Install system dependencies 6 | RUN pip install "poetry==$POETRY_VERSION" 7 | 8 | # Set work directory 9 | WORKDIR /app 10 | 11 | # Copy only requirements to cache them in docker layer 12 | COPY poetry.lock pyproject.toml /app/ 13 | 14 | # Disable the creation of virtual environments 15 | RUN poetry config virtualenvs.create false 16 | 17 | # Install dependencies 18 | RUN poetry install 19 | 20 | # Copy the current directory contents into the container at /app 21 | COPY ./ /app/ 22 | -------------------------------------------------------------------------------- /backend/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: end-of-file-fixer 8 | - id: check-yaml 9 | - id: check-added-large-files 10 | - repo: local 11 | hooks: 12 | - id: poetry-pytest 13 | name: Run pytest with Poetry 14 | entry: poetry run --directory backend pytest 15 | language: system 16 | pass_filenames: false 17 | always_run: true 18 | files: ^backend/ 19 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Abi Raja "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | fastapi = "^0.95.0" 11 | uvicorn = "^0.25.0" 12 | websockets = "^12.0" 13 | openai = "^1.2.4" 14 | python-dotenv = "^1.0.0" 15 | beautifulsoup4 = "^4.12.2" 16 | httpx = "^0.25.1" 17 | pre-commit = "^3.6.2" 18 | 19 | [tool.poetry.group.dev.dependencies] 20 | pytest = "^7.4.3" 21 | pyright = "^1.1.345" 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /frontend/src/hooks/usePersistedState.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useState } from 'react'; 2 | 3 | type PersistedState = [T, Dispatch>]; 4 | 5 | function usePersistedState(defaultValue: T, key: string): PersistedState { 6 | const [value, setValue] = useState(() => { 7 | const value = window.localStorage.getItem(key); 8 | 9 | return value ? (JSON.parse(value) as T) : defaultValue; 10 | }); 11 | 12 | useEffect(() => { 13 | window.localStorage.setItem(key, JSON.stringify(value)); 14 | }, [key, value]); 15 | 16 | return [value, setValue]; 17 | } 18 | 19 | export { usePersistedState }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/CodePreview.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | interface Props { 4 | code: string; 5 | } 6 | 7 | function CodePreview({ code }: Props) { 8 | const scrollRef = useRef(null); 9 | 10 | useEffect(() => { 11 | if (scrollRef.current) { 12 | scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; 13 | } 14 | }, [code]); 15 | 16 | return ( 17 |
22 | {code} 23 |
24 | ); 25 | } 26 | 27 | export default CodePreview; 28 | -------------------------------------------------------------------------------- /backend/access_token.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import httpx 4 | 5 | 6 | async def validate_access_token(access_code: str): 7 | async with httpx.AsyncClient() as client: 8 | url = ( 9 | "https://backend.buildpicoapps.com/screenshot_to_code/validate_access_token" 10 | ) 11 | data = json.dumps( 12 | { 13 | "access_code": access_code, 14 | "secret": os.environ.get("PICO_BACKEND_SECRET"), 15 | } 16 | ) 17 | headers = {"Content-Type": "application/json"} 18 | 19 | response = await client.post(url, content=data, headers=headers) 20 | response_data = response.json() 21 | return response_data 22 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | # Load environment variables first 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | from fastapi import FastAPI 8 | from fastapi.middleware.cors import CORSMiddleware 9 | from routes import screenshot, generate_code, home, evals 10 | 11 | app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) 12 | 13 | # Configure CORS settings 14 | app.add_middleware( 15 | CORSMiddleware, 16 | allow_origins=["*"], 17 | allow_credentials=True, 18 | allow_methods=["*"], 19 | allow_headers=["*"], 20 | ) 21 | 22 | # Add routes 23 | app.include_router(generate_code.router) 24 | app.include_router(screenshot.router) 25 | app.include_router(home.router) 26 | app.include_router(evals.router) 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | backend: 5 | build: 6 | context: ./backend 7 | dockerfile: Dockerfile 8 | 9 | env_file: 10 | - .env 11 | 12 | # or 13 | # environment: 14 | #- BACKEND_PORT=7001 # if you change the port, make sure to also change the VITE_WS_BACKEND_URL at frontend/.env.local 15 | # - OPENAI_API_KEY=your_openai_api_key 16 | 17 | ports: 18 | - "${BACKEND_PORT:-7001}:${BACKEND_PORT:-7001}" 19 | 20 | command: poetry run uvicorn main:app --host 0.0.0.0 --port ${BACKEND_PORT:-7001} 21 | 22 | frontend: 23 | build: 24 | context: ./frontend 25 | dockerfile: Dockerfile 26 | ports: 27 | - "5173:5173" 28 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import { Toaster } from "react-hot-toast"; 6 | import EvalsPage from "./components/evals/EvalsPage.tsx"; 7 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 8 | 9 | ReactDOM.createRoot(document.getElementById("root")!).render( 10 | 11 | 12 | 13 | } /> 14 | } /> 15 | 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /backend/evals/core.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from llm import stream_openai_response 4 | from prompts import assemble_prompt 5 | from prompts.types import Stack 6 | 7 | 8 | async def generate_code_core(image_url: str, stack: Stack) -> str: 9 | prompt_messages = assemble_prompt(image_url, stack) 10 | openai_api_key = os.environ.get("OPENAI_API_KEY") 11 | openai_base_url = None 12 | 13 | async def process_chunk(content: str): 14 | pass 15 | 16 | if not openai_api_key: 17 | raise Exception("OpenAI API key not found") 18 | 19 | completion = await stream_openai_response( 20 | prompt_messages, 21 | api_key=openai_api_key, 22 | base_url=openai_base_url, 23 | callback=lambda x: process_chunk(x), 24 | ) 25 | 26 | return completion 27 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "baseUrl": ".", 24 | "paths": { 25 | "@/*": ["./src/*"] 26 | } 27 | }, 28 | "include": ["src"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /frontend/src/lib/stacks.ts: -------------------------------------------------------------------------------- 1 | // Keep in sync with backend (prompts/types.py) 2 | export enum Stack { 3 | HTML_TAILWIND = "html_tailwind", 4 | REACT_TAILWIND = "react_tailwind", 5 | BOOTSTRAP = "bootstrap", 6 | VUE_TAILWIND = "vue_tailwind", 7 | IONIC_TAILWIND = "ionic_tailwind", 8 | SVG = "svg", 9 | } 10 | 11 | export const STACK_DESCRIPTIONS: { 12 | [key in Stack]: { components: string[]; inBeta: boolean }; 13 | } = { 14 | html_tailwind: { components: ["HTML", "Tailwind"], inBeta: false }, 15 | react_tailwind: { components: ["React", "Tailwind"], inBeta: false }, 16 | bootstrap: { components: ["Bootstrap"], inBeta: false }, 17 | vue_tailwind: { components: ["Vue", "Tailwind"], inBeta: true }, 18 | ionic_tailwind: { components: ["Ionic", "Tailwind"], inBeta: true }, 19 | svg: { components: ["SVG"], inBeta: true }, 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |