├── background-rater-finished ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── src │ ├── ai │ │ ├── generateImage.ts │ │ ├── index.ts │ │ ├── review.ts │ │ └── theme.ts │ ├── app │ │ ├── RQProvider.tsx │ │ ├── api │ │ │ ├── background │ │ │ │ └── [id] │ │ │ │ │ └── route.ts │ │ │ └── inngest │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── rater │ │ │ └── [backgroundId] │ │ │ ├── BackgroundMonitor.tsx │ │ │ └── page.tsx │ ├── components │ │ ├── camera.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── select.tsx │ │ │ └── skeleton.tsx │ ├── db │ │ ├── index.ts │ │ └── schema.psql │ ├── inngest │ │ ├── client.ts │ │ └── functions │ │ │ ├── getReaction.ts │ │ │ └── makeNewBackground.ts │ └── lib │ │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json └── background-rater ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── src ├── ai │ ├── generateImage.ts │ ├── index.ts │ ├── review.ts │ └── theme.ts ├── app │ ├── RQProvider.tsx │ ├── api │ │ └── background │ │ │ └── [id] │ │ │ └── route.ts │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── rater │ │ └── [backgroundId] │ │ ├── BackgroundMonitor.tsx │ │ └── page.tsx ├── components │ ├── camera.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── select.tsx │ │ └── skeleton.tsx ├── db │ ├── index.ts │ └── schema.psql └── lib │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /background-rater-finished/.env.example: -------------------------------------------------------------------------------- 1 | BLOB_READ_WRITE_TOKEN= 2 | POSTGRES_URL= 3 | POSTGRES_PRISMA_URL= 4 | POSTGRES_URL_NO_SSL= 5 | POSTGRES_URL_NON_POOLING= 6 | POSTGRES_USER= 7 | POSTGRES_HOST= 8 | POSTGRES_PASSWORD= 9 | POSTGRES_DATABASE= -------------------------------------------------------------------------------- /background-rater-finished/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /background-rater-finished/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /background-rater-finished/README.md: -------------------------------------------------------------------------------- 1 | This is an example AI application that uses [Inngest](https://www.inngest.com/) to choreograph the long running AI jobs. 2 | 3 | ## Getting Started 4 | 5 | Follow these steps to get started with the project: 6 | 7 | 1. Install the dependencies: 8 | 9 | ```bash 10 | pnpm install 11 | ``` 12 | 13 | 2. Setup your environment variables: 14 | 15 | ```bash 16 | cp .env.example .env.development.local 17 | ``` 18 | 19 | Then, open the `.env.development.local` file and fill in the required environment variables. 20 | 21 | - You'll also need an `OPENAI_API_KEY` to access the OpenAI API. 22 | - You'll need all the `POSTGRES_*` variables from the Vercel dashboard to access the Vercel Postgres API. The Vercel Postgres API is used to store the current backgrounds and their status. 23 | - You'll need a `BLOB_READ_WRITE_TOKEN` variables from the Vercel dashboard to access the Vercel Blob API. The Vercel Blob API is used to store the uploaded images. 24 | 25 | You can get all the Vercel variables by using the Vercel CLI: 26 | 27 | ```bash 28 | vercel env pull 29 | ``` 30 | 31 | 2. Initialize the database 32 | 33 | ```bash 34 | psql "" -f src/db/schema.psql 35 | ``` 36 | 37 | 3. Run Inngest locally in another terminal window: 38 | 39 | ```bash 40 | npx inngest-cli@latest dev --no-discovery -u http://localhost:3000/api/inngest 41 | ``` 42 | 43 | And open the [Inngest dashboard](http://127.0.0.1:8288/stream). 44 | 45 | 4. Run the development server: 46 | 47 | ```bash 48 | pnpm dev 49 | ``` 50 | 51 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 52 | -------------------------------------------------------------------------------- /background-rater-finished/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /background-rater-finished/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "**", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /background-rater-finished/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "background-rater", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently \"npm run dev:next\" \"npm run dev:inngest\"", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "dev:next": "next dev", 11 | "dev:inngest": "npx inngest-cli@latest dev --no-discovery -u http://localhost:3000/api/inngest" 12 | }, 13 | "dependencies": { 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-select": "^2.1.1", 16 | "@radix-ui/react-slot": "^1.1.0", 17 | "@tanstack/react-query": "^5.55.4", 18 | "@vercel/blob": "^0.23.4", 19 | "@vercel/postgres": "^0.10.0", 20 | "class-variance-authority": "^0.7.0", 21 | "clsx": "^2.1.1", 22 | "inngest": "^3.22.10", 23 | "lucide-react": "^0.439.0", 24 | "next": "14.2.9", 25 | "openai": "^4.59.0", 26 | "react": "^18", 27 | "react-dom": "^18", 28 | "react-markdown": "^9.0.1", 29 | "tailwind-merge": "^2.5.2", 30 | "tailwindcss-animate": "^1.0.7" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20", 34 | "@types/react": "^18", 35 | "@types/react-dom": "^18", 36 | "@types/w3c-image-capture": "^1.0.10", 37 | "concurrently": "^9.0.0", 38 | "eslint": "^8", 39 | "eslint-config-next": "14.2.9", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.1", 42 | "typescript": "^5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /background-rater-finished/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /background-rater-finished/src/ai/generateImage.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@/ai"; 2 | 3 | export async function generateImage(theme: string): Promise { 4 | const response = await openai.images.generate({ 5 | model: "dall-e-3", 6 | prompt: `A webcam background image with this theme: ${theme}`, 7 | size: "1792x1024", 8 | quality: "standard", 9 | n: 1, 10 | }); 11 | 12 | return response?.data[0].url ?? ""; 13 | } 14 | -------------------------------------------------------------------------------- /background-rater-finished/src/ai/index.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! }); 4 | -------------------------------------------------------------------------------- /background-rater-finished/src/ai/review.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@/ai"; 2 | import { setReview, setReviewCompleted } from "@/db"; 3 | 4 | const SYSTEM_CONTEXT = `You are a webcam background reviewer. 5 | Your job is to review the background in the image and provide feedback on the lighting, use of props, and overall tone in the image. 6 | You are very critical and have a very high standard for what makes a good background in a webcam. 7 | You are very knowledgeable about photography and have a lot of experience in the field. 8 | You are very confident in your opinions and are not afraid to share them with others. 9 | 10 | Your response should be a bulleted list of five key points you are reviewing. One sentence for each point.`; 11 | 12 | export async function addReview( 13 | backgroundId: string, 14 | url: string 15 | ): Promise { 16 | const stream = await openai.chat.completions.create({ 17 | model: "gpt-4-turbo", 18 | messages: [ 19 | { 20 | role: "system", 21 | content: SYSTEM_CONTEXT, 22 | }, 23 | { 24 | role: "user", 25 | content: [ 26 | { 27 | type: "text", 28 | text: "Rate the composition, lighting, and use of props of the background. Provide reasoning for that recommendation.", 29 | }, 30 | { 31 | type: "image_url", 32 | image_url: { 33 | url, 34 | }, 35 | }, 36 | ], 37 | }, 38 | ], 39 | max_tokens: 400, 40 | stream: true, 41 | }); 42 | 43 | let review = ""; 44 | for await (const chunk of stream) { 45 | review += chunk.choices[0].delta.content ?? ""; 46 | await setReview(+backgroundId, review); 47 | } 48 | await setReviewCompleted(+backgroundId, true); 49 | 50 | return review; 51 | } 52 | -------------------------------------------------------------------------------- /background-rater-finished/src/ai/theme.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@/ai"; 2 | 3 | export async function getTheme(url: string): Promise { 4 | const out = await openai.chat.completions.create({ 5 | model: "gpt-4-turbo", 6 | messages: [ 7 | { 8 | role: "user", 9 | content: [ 10 | { 11 | type: "text", 12 | text: `What is the overall theme of the background in this webcam image? Provide the theme as a string that can be used to promp the AI to generate a prompt for a new replacement background image`, 13 | }, 14 | { 15 | type: "image_url", 16 | image_url: { 17 | url, 18 | }, 19 | }, 20 | ], 21 | }, 22 | ], 23 | max_tokens: 400, 24 | }); 25 | 26 | return out?.choices[0].message.content ?? ""; 27 | } 28 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/RQProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | import { useState } from "react"; 4 | 5 | export default function RQProvider({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode; 9 | }>) { 10 | const [queryClient] = useState(() => new QueryClient()); 11 | return ( 12 | {children} 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/api/background/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getBackground } from "@/db"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | export const fetchCache = "force-no-store"; 7 | 8 | export async function GET( 9 | req: Request, 10 | { 11 | params: { id }, 12 | }: { 13 | params: { id: string }; 14 | } 15 | ) { 16 | const background = await getBackground(+id); 17 | return NextResponse.json(background); 18 | } 19 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/api/inngest/route.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "inngest/next"; 2 | 3 | import { inngest } from "@/inngest/client"; 4 | import { getReaction } from "@/inngest/functions/getReaction"; 5 | import { makeNewBackground } from "@/inngest/functions/makeNewBackground"; 6 | 7 | export const { GET, POST, PUT } = serve({ 8 | client: inngest, 9 | functions: [getReaction, makeNewBackground], 10 | }); 11 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/background-rater/7fad080d325415fcde4aaff96b9cad5677ea03fe/background-rater-finished/src/app/favicon.ico -------------------------------------------------------------------------------- /background-rater-finished/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/background-rater/7fad080d325415fcde4aaff96b9cad5677ea03fe/background-rater-finished/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /background-rater-finished/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/background-rater/7fad080d325415fcde4aaff96b9cad5677ea03fe/background-rater-finished/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /background-rater-finished/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | @layer utilities { 24 | .text-balance { 25 | text-wrap: balance; 26 | } 27 | } 28 | 29 | @layer base { 30 | :root { 31 | --background: 0 0% 100%; 32 | --foreground: 0 0% 3.9%; 33 | --card: 0 0% 100%; 34 | --card-foreground: 0 0% 3.9%; 35 | --popover: 0 0% 100%; 36 | --popover-foreground: 0 0% 3.9%; 37 | --primary: 0 0% 9%; 38 | --primary-foreground: 0 0% 98%; 39 | --secondary: 0 0% 96.1%; 40 | --secondary-foreground: 0 0% 9%; 41 | --muted: 0 0% 96.1%; 42 | --muted-foreground: 0 0% 45.1%; 43 | --accent: 0 0% 96.1%; 44 | --accent-foreground: 0 0% 9%; 45 | --destructive: 0 84.2% 60.2%; 46 | --destructive-foreground: 0 0% 98%; 47 | --border: 0 0% 89.8%; 48 | --input: 0 0% 89.8%; 49 | --ring: 0 0% 3.9%; 50 | --chart-1: 12 76% 61%; 51 | --chart-2: 173 58% 39%; 52 | --chart-3: 197 37% 24%; 53 | --chart-4: 43 74% 66%; 54 | --chart-5: 27 87% 67%; 55 | --radius: 0.5rem; 56 | } 57 | .dark { 58 | --background: 0 0% 3.9%; 59 | --foreground: 0 0% 98%; 60 | --card: 0 0% 3.9%; 61 | --card-foreground: 0 0% 98%; 62 | --popover: 0 0% 3.9%; 63 | --popover-foreground: 0 0% 98%; 64 | --primary: 0 0% 98%; 65 | --primary-foreground: 0 0% 9%; 66 | --secondary: 0 0% 14.9%; 67 | --secondary-foreground: 0 0% 98%; 68 | --muted: 0 0% 14.9%; 69 | --muted-foreground: 0 0% 63.9%; 70 | --accent: 0 0% 14.9%; 71 | --accent-foreground: 0 0% 98%; 72 | --destructive: 0 62.8% 30.6%; 73 | --destructive-foreground: 0 0% 98%; 74 | --border: 0 0% 14.9%; 75 | --input: 0 0% 14.9%; 76 | --ring: 0 0% 83.1%; 77 | --chart-1: 220 70% 50%; 78 | --chart-2: 160 60% 45%; 79 | --chart-3: 30 80% 55%; 80 | --chart-4: 280 65% 60%; 81 | --chart-5: 340 75% 55%; 82 | } 83 | } 84 | 85 | @layer base { 86 | * { 87 | @apply border-border; 88 | } 89 | body { 90 | @apply bg-background text-foreground; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | import RQProvider from "./RQProvider"; 6 | 7 | const geistSans = localFont({ 8 | src: "./fonts/GeistVF.woff", 9 | variable: "--font-geist-sans", 10 | weight: "100 900", 11 | }); 12 | const geistMono = localFont({ 13 | src: "./fonts/GeistMonoVF.woff", 14 | variable: "--font-geist-mono", 15 | weight: "100 900", 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Create Next App", 20 | description: "Generated by create next app", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 33 | {children} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { put } from "@vercel/blob"; 4 | import { addBackground } from "@/db"; 5 | import { inngest } from "@/inngest/client"; 6 | 7 | import { Camera } from "@/components/camera"; 8 | 9 | export default function Home() { 10 | async function onUploadImage(formData: FormData) { 11 | "use server"; 12 | const file = formData.get("image") as Blob; 13 | const buffer = await file.arrayBuffer(); 14 | const data = Buffer.from(buffer); 15 | 16 | const randomFileName = Math.random().toString(36).substring(2); 17 | 18 | const { url } = await put(`images/${randomFileName}.jpg`, data, { 19 | access: "public", 20 | }); 21 | 22 | const backgroundId = await addBackground(url); 23 | 24 | await inngest.send({ 25 | name: "rater/image-uploaded", 26 | data: { 27 | backgroundId, 28 | url, 29 | }, 30 | }); 31 | 32 | redirect(`/rater/${backgroundId}`); 33 | } 34 | 35 | return ( 36 |
37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/rater/[backgroundId]/BackgroundMonitor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from "react"; 3 | import Image from "next/image"; 4 | import Markdown from "react-markdown"; 5 | import { useQuery } from "@tanstack/react-query"; 6 | 7 | import { Skeleton } from "@/components/ui/skeleton"; 8 | 9 | import { Background } from "@/db"; 10 | 11 | export default function BackgroundMonitor({ 12 | backgroundId, 13 | background: initialBackground, 14 | }: { 15 | backgroundId: string; 16 | background: Background; 17 | }) { 18 | const [enabled, setEnabled] = useState(true); 19 | const { data } = useQuery({ 20 | queryKey: ["background", backgroundId], 21 | queryFn: async () => { 22 | const response = await fetch(`/api/background/${backgroundId}`); 23 | return response.json(); 24 | }, 25 | initialData: initialBackground, 26 | refetchInterval: 200, 27 | enabled, 28 | }); 29 | 30 | useEffect(() => { 31 | setEnabled( 32 | !data?.review_completed || !data?.theme || !data?.new_background 33 | ); 34 | }, [data?.review_completed, data?.theme, data?.new_background]); 35 | 36 | return ( 37 |
38 |
39 |

What You've Got

40 |
41 | Background 47 |
48 | {data?.review} 49 |
50 |
51 |

What You Need

52 |
53 | {data?.new_background ? ( 54 | Background 61 | ) : ( 62 | 63 | )} 64 |
65 | {data?.theme} 66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /background-rater-finished/src/app/rater/[backgroundId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getBackground } from "@/db"; 2 | import Link from "next/link"; 3 | 4 | import { Button } from "@/components/ui/button"; 5 | import BackgroundMonitor from "./BackgroundMonitor"; 6 | 7 | export const dynamic = "force-dynamic"; 8 | export const fetchCache = "force-no-store"; 9 | 10 | export default async function Camera({ 11 | params: { backgroundId }, 12 | }: { 13 | params: { backgroundId: string }; 14 | }) { 15 | const background = await getBackground(+backgroundId); 16 | 17 | if (!background) { 18 | return
Background not found
; 19 | } 20 | 21 | return ( 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /background-rater-finished/src/components/camera.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useRef, useEffect } from "react"; 3 | 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select"; 11 | import { Button } from "@/components/ui/button"; 12 | 13 | export function Camera({ 14 | onUploadImage, 15 | }: { 16 | onUploadImage: (formData: FormData) => void; 17 | }) { 18 | const [devices, setDevices] = useState([]); 19 | const [selectedDevice, setSelectedDevice] = useState(""); 20 | const [isCapturing, setIsCapturing] = useState(false); 21 | const videoRef = useRef(null); 22 | const [imageCapture, setImageCapture] = useState(null); 23 | const [uploading, setUploading] = useState(false); 24 | 25 | useEffect(() => { 26 | async function getDevices() { 27 | await navigator.mediaDevices.getUserMedia({ 28 | video: true, 29 | }); 30 | const devices = await navigator.mediaDevices.enumerateDevices(); 31 | const videoDevices = devices.filter( 32 | (device) => device.kind === "videoinput" 33 | ); 34 | setDevices(videoDevices); 35 | if (videoDevices.length > 0) { 36 | setSelectedDevice(videoDevices[0].deviceId); 37 | } 38 | } 39 | getDevices(); 40 | }, []); 41 | 42 | const startCapture = async () => { 43 | try { 44 | const stream = await navigator.mediaDevices.getUserMedia({ 45 | video: { deviceId: selectedDevice, width: 1920 / 2 }, 46 | }); 47 | if (videoRef.current) { 48 | videoRef.current.srcObject = stream; 49 | setImageCapture(new ImageCapture(stream.getVideoTracks()[0])); 50 | setIsCapturing(true); 51 | } 52 | } catch (err) { 53 | console.error("Error accessing the camera:", err); 54 | } 55 | }; 56 | 57 | const takePhoto = async () => { 58 | if (videoRef.current && imageCapture) { 59 | setUploading(true); 60 | videoRef.current.pause(); 61 | const blob = await imageCapture.takePhoto(); 62 | const formData = new FormData(); 63 | formData.append("image", blob, "image.jpg"); 64 | await onUploadImage(formData); 65 | setUploading(false); 66 | } 67 | }; 68 | 69 | return ( 70 |
71 |

72 | Take A Picture Of Your Workspace 73 |

74 | 75 |
76 |
83 | 84 | {uploading &&
Uploading...
} 85 | {!uploading && ( 86 |
87 | 99 | 100 | 103 | 104 | 107 |
108 | )} 109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /background-rater-finished/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /background-rater-finished/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /background-rater-finished/src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { 5 | CaretSortIcon, 6 | CheckIcon, 7 | ChevronDownIcon, 8 | ChevronUpIcon, 9 | } from "@radix-ui/react-icons" 10 | import * as SelectPrimitive from "@radix-ui/react-select" 11 | 12 | import { cn } from "@/lib/utils" 13 | 14 | const Select = SelectPrimitive.Root 15 | 16 | const SelectGroup = SelectPrimitive.Group 17 | 18 | const SelectValue = SelectPrimitive.Value 19 | 20 | const SelectTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, children, ...props }, ref) => ( 24 | span]:line-clamp-1", 28 | className 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | 37 | )) 38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 39 | 40 | const SelectScrollUpButton = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | 53 | 54 | )) 55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 56 | 57 | const SelectScrollDownButton = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | 70 | 71 | )) 72 | SelectScrollDownButton.displayName = 73 | SelectPrimitive.ScrollDownButton.displayName 74 | 75 | const SelectContent = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, position = "popper", ...props }, ref) => ( 79 | 80 | 91 | 92 | 99 | {children} 100 | 101 | 102 | 103 | 104 | )) 105 | SelectContent.displayName = SelectPrimitive.Content.displayName 106 | 107 | const SelectLabel = React.forwardRef< 108 | React.ElementRef, 109 | React.ComponentPropsWithoutRef 110 | >(({ className, ...props }, ref) => ( 111 | 116 | )) 117 | SelectLabel.displayName = SelectPrimitive.Label.displayName 118 | 119 | const SelectItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | SelectItem.displayName = SelectPrimitive.Item.displayName 140 | 141 | const SelectSeparator = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef 144 | >(({ className, ...props }, ref) => ( 145 | 150 | )) 151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 152 | 153 | export { 154 | Select, 155 | SelectGroup, 156 | SelectValue, 157 | SelectTrigger, 158 | SelectContent, 159 | SelectLabel, 160 | SelectItem, 161 | SelectSeparator, 162 | SelectScrollUpButton, 163 | SelectScrollDownButton, 164 | } 165 | -------------------------------------------------------------------------------- /background-rater-finished/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /background-rater-finished/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "@vercel/postgres"; 2 | 3 | export type Background = { 4 | image: string; 5 | new_background: string | null; 6 | review: string; 7 | review_completed: boolean; 8 | theme: string | null; 9 | }; 10 | 11 | export async function addBackground(image: string): Promise { 12 | await sql`INSERT INTO backgrounds (image) VALUES (${image})`; 13 | const result = 14 | await sql`SELECT currval(pg_get_serial_sequence('backgrounds', 'id'))`; 15 | return result.rows[0].currval; 16 | } 17 | 18 | export async function setNewBackground( 19 | id: number, 20 | newBackground: string 21 | ): Promise { 22 | await sql`UPDATE backgrounds SET new_background=${newBackground} WHERE id=${id}`; 23 | } 24 | 25 | export async function setReview(id: number, review: string): Promise { 26 | console.log("setting review", id, review); 27 | await sql`UPDATE backgrounds SET review=${review} WHERE id=${id}`; 28 | } 29 | 30 | export async function setReviewCompleted( 31 | id: number, 32 | reviewCompleted: boolean 33 | ): Promise { 34 | await sql`UPDATE backgrounds SET review_completed=${reviewCompleted} WHERE id=${id}`; 35 | } 36 | 37 | export async function setTheme(id: number, theme: string): Promise { 38 | await sql`UPDATE backgrounds SET theme=${theme} WHERE id=${id}`; 39 | } 40 | 41 | export async function getBackground(id: number): Promise { 42 | const result = await sql`SELECT * FROM backgrounds WHERE id=${id}`; 43 | return result.rows[0] as Background | null; 44 | } 45 | -------------------------------------------------------------------------------- /background-rater-finished/src/db/schema.psql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS backgrounds ( 2 | id SERIAL PRIMARY KEY, 3 | image TEXT NOT NULL, 4 | new_background TEXT, 5 | review TEXT, 6 | review_completed BOOLEAN DEFAULT FALSE, 7 | theme TEXT 8 | ); -------------------------------------------------------------------------------- /background-rater-finished/src/inngest/client.ts: -------------------------------------------------------------------------------- 1 | import { Inngest } from "inngest"; 2 | 3 | export const inngest = new Inngest({ id: "rate-my-background" }); 4 | -------------------------------------------------------------------------------- /background-rater-finished/src/inngest/functions/getReaction.ts: -------------------------------------------------------------------------------- 1 | import { addReview } from "@/ai/review"; 2 | import { getTheme } from "@/ai/theme"; 3 | 4 | import { setTheme } from "@/db"; 5 | 6 | import { inngest } from "@/inngest/client"; 7 | 8 | export const getReaction = inngest.createFunction( 9 | { id: "getReaction" }, 10 | { event: "rater/image-uploaded" }, 11 | async ({ event, step }) => { 12 | const { url, backgroundId } = event.data; 13 | 14 | const reviewPromise = step.run("add-review", async () => { 15 | return await addReview(backgroundId, url); 16 | }); 17 | 18 | const themePromise = step.run("get-theme", async () => { 19 | const theme = await getTheme(url); 20 | await setTheme(+backgroundId, theme); 21 | 22 | await inngest.send({ 23 | name: "rater/theme-updated", 24 | data: { 25 | backgroundId, 26 | theme, 27 | }, 28 | }); 29 | 30 | return theme; 31 | }); 32 | 33 | const [review, theme] = await Promise.all([reviewPromise, themePromise]); 34 | 35 | return { 36 | backgroundId, 37 | review, 38 | theme, 39 | }; 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /background-rater-finished/src/inngest/functions/makeNewBackground.ts: -------------------------------------------------------------------------------- 1 | import { generateImage } from "@/ai/generateImage"; 2 | import { setNewBackground } from "@/db"; 3 | 4 | import { inngest } from "@/inngest/client"; 5 | 6 | export const makeNewBackground = inngest.createFunction( 7 | { 8 | id: "makeNewBackground", 9 | throttle: { 10 | limit: 1, 11 | period: "5s", 12 | burst: 2, 13 | }, 14 | }, 15 | { event: "rater/theme-updated" }, 16 | async ({ event, step }) => { 17 | const { backgroundId, theme } = event.data; 18 | 19 | const newBackground = await step.run("generate-image", async () => { 20 | const background = await generateImage(theme); 21 | await setNewBackground(+backgroundId, background); 22 | return background; 23 | }); 24 | 25 | return { 26 | backgroundId, 27 | newBackground, 28 | }; 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /background-rater-finished/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /background-rater-finished/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./src/**/*.{js,ts,jsx,tsx,mdx}" 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | } 54 | }, 55 | borderRadius: { 56 | lg: 'var(--radius)', 57 | md: 'calc(var(--radius) - 2px)', 58 | sm: 'calc(var(--radius) - 4px)' 59 | } 60 | } 61 | }, 62 | plugins: [require("tailwindcss-animate")], 63 | }; 64 | export default config; 65 | -------------------------------------------------------------------------------- /background-rater-finished/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /background-rater/.env.example: -------------------------------------------------------------------------------- 1 | BLOB_READ_WRITE_TOKEN= 2 | POSTGRES_URL= 3 | POSTGRES_PRISMA_URL= 4 | POSTGRES_URL_NO_SSL= 5 | POSTGRES_URL_NON_POOLING= 6 | POSTGRES_USER= 7 | POSTGRES_HOST= 8 | POSTGRES_PASSWORD= 9 | POSTGRES_DATABASE= -------------------------------------------------------------------------------- /background-rater/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /background-rater/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /background-rater/README.md: -------------------------------------------------------------------------------- 1 | This is an example AI application that uses [Inngest](https://www.inngest.com/) to choreograph the long running AI jobs. 2 | 3 | ## Getting Started 4 | 5 | Follow these steps to get started with the project: 6 | 7 | 1. Install the dependencies: 8 | 9 | ```bash 10 | pnpm install 11 | ``` 12 | 13 | 2. Setup your environment variables: 14 | 15 | ```bash 16 | cp .env.example .env.development.local 17 | ``` 18 | 19 | Then, open the `.env.development.local` file and fill in the required environment variables. 20 | 21 | - You'll also need an `OPENAI_API_KEY` to access the OpenAI API. 22 | - You'll need all the `POSTGRES_*` variables from the Vercel dashboard to access the Vercel Postgres API. The Vercel Postgres API is used to store the current backgrounds and their status. 23 | - You'll need a `BLOB_READ_WRITE_TOKEN` variables from the Vercel dashboard to access the Vercel Blob API. The Vercel Blob API is used to store the uploaded images. 24 | 25 | You can get all the Vercel variables by using the Vercel CLI: 26 | 27 | ```bash 28 | vercel env pull 29 | ``` 30 | 31 | 2. Initialize the database 32 | 33 | ```bash 34 | psql "" -f src/db/schema.psql 35 | ``` 36 | 37 | 3. Run Inngest locally in another terminal window: 38 | 39 | ```bash 40 | npx inngest-cli@latest dev --no-discovery -u http://localhost:3000/api/inngest 41 | ``` 42 | 43 | And open the [Inngest dashboard](http://127.0.0.1:8288/stream). 44 | 45 | 4. Run the development server: 46 | 47 | ```bash 48 | pnpm dev 49 | ``` 50 | 51 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 52 | -------------------------------------------------------------------------------- /background-rater/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /background-rater/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "**", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /background-rater/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "background-rater", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint", 9 | "dev": "next dev" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-icons": "^1.3.0", 13 | "@radix-ui/react-select": "^2.1.1", 14 | "@radix-ui/react-slot": "^1.1.0", 15 | "@tanstack/react-query": "^5.55.4", 16 | "@vercel/blob": "^0.23.4", 17 | "@vercel/postgres": "^0.10.0", 18 | "class-variance-authority": "^0.7.0", 19 | "clsx": "^2.1.1", 20 | "inngest": "^3.22.10", 21 | "lucide-react": "^0.439.0", 22 | "next": "14.2.9", 23 | "openai": "^4.59.0", 24 | "react": "^18", 25 | "react-dom": "^18", 26 | "react-markdown": "^9.0.1", 27 | "tailwind-merge": "^2.5.2", 28 | "tailwindcss-animate": "^1.0.7" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "@types/w3c-image-capture": "^1.0.10", 35 | "concurrently": "^9.0.0", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.2.9", 38 | "postcss": "^8", 39 | "tailwindcss": "^3.4.1", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /background-rater/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /background-rater/src/ai/generateImage.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@/ai"; 2 | 3 | export async function generateImage(theme: string): Promise { 4 | const response = await openai.images.generate({ 5 | model: "dall-e-3", 6 | prompt: `A webcam background image with this theme: ${theme}`, 7 | size: "1792x1024", 8 | quality: "standard", 9 | n: 1, 10 | }); 11 | 12 | return response?.data[0].url ?? ""; 13 | } 14 | -------------------------------------------------------------------------------- /background-rater/src/ai/index.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! }); 4 | -------------------------------------------------------------------------------- /background-rater/src/ai/review.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@/ai"; 2 | import { setReview, setReviewCompleted } from "@/db"; 3 | 4 | const SYSTEM_CONTEXT = `You are a webcam background reviewer. 5 | Your job is to review the background in the image and provide feedback on the lighting, use of props, and overall tone in the image. 6 | You are very critical and have a very high standard for what makes a good background in a webcam. 7 | You are very knowledgeable about photography and have a lot of experience in the field. 8 | You are very confident in your opinions and are not afraid to share them with others. 9 | 10 | Your response should be a bulleted list of five key points you are reviewing. One sentence for each point.`; 11 | 12 | export async function addReview( 13 | backgroundId: string, 14 | url: string 15 | ): Promise { 16 | const stream = await openai.chat.completions.create({ 17 | model: "gpt-4-turbo", 18 | messages: [ 19 | { 20 | role: "system", 21 | content: SYSTEM_CONTEXT, 22 | }, 23 | { 24 | role: "user", 25 | content: [ 26 | { 27 | type: "text", 28 | text: "Rate the composition, lighting, and use of props of the background. Provide reasoning for that recommendation.", 29 | }, 30 | { 31 | type: "image_url", 32 | image_url: { 33 | url, 34 | }, 35 | }, 36 | ], 37 | }, 38 | ], 39 | max_tokens: 400, 40 | stream: true, 41 | }); 42 | 43 | let review = ""; 44 | for await (const chunk of stream) { 45 | review += chunk.choices[0].delta.content ?? ""; 46 | await setReview(+backgroundId, review); 47 | } 48 | await setReviewCompleted(+backgroundId, true); 49 | 50 | return review; 51 | } 52 | -------------------------------------------------------------------------------- /background-rater/src/ai/theme.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@/ai"; 2 | 3 | export async function getTheme(url: string): Promise { 4 | const out = await openai.chat.completions.create({ 5 | model: "gpt-4-turbo", 6 | messages: [ 7 | { 8 | role: "user", 9 | content: [ 10 | { 11 | type: "text", 12 | text: `What is the overall theme of the background in this webcam image? Provide the theme as a string that can be used to promp the AI to generate a prompt for a new replacement background image`, 13 | }, 14 | { 15 | type: "image_url", 16 | image_url: { 17 | url, 18 | }, 19 | }, 20 | ], 21 | }, 22 | ], 23 | max_tokens: 400, 24 | }); 25 | 26 | return out?.choices[0].message.content ?? ""; 27 | } 28 | -------------------------------------------------------------------------------- /background-rater/src/app/RQProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | import { useState } from "react"; 4 | 5 | export default function RQProvider({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode; 9 | }>) { 10 | const [queryClient] = useState(() => new QueryClient()); 11 | return ( 12 | {children} 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /background-rater/src/app/api/background/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getBackground } from "@/db"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | export const fetchCache = "force-no-store"; 7 | 8 | export async function GET( 9 | req: Request, 10 | { 11 | params: { id }, 12 | }: { 13 | params: { id: string }; 14 | } 15 | ) { 16 | const background = await getBackground(+id); 17 | return NextResponse.json(background); 18 | } 19 | -------------------------------------------------------------------------------- /background-rater/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/background-rater/7fad080d325415fcde4aaff96b9cad5677ea03fe/background-rater/src/app/favicon.ico -------------------------------------------------------------------------------- /background-rater/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/background-rater/7fad080d325415fcde4aaff96b9cad5677ea03fe/background-rater/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /background-rater/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/background-rater/7fad080d325415fcde4aaff96b9cad5677ea03fe/background-rater/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /background-rater/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | @layer utilities { 24 | .text-balance { 25 | text-wrap: balance; 26 | } 27 | } 28 | 29 | @layer base { 30 | :root { 31 | --background: 0 0% 100%; 32 | --foreground: 0 0% 3.9%; 33 | --card: 0 0% 100%; 34 | --card-foreground: 0 0% 3.9%; 35 | --popover: 0 0% 100%; 36 | --popover-foreground: 0 0% 3.9%; 37 | --primary: 0 0% 9%; 38 | --primary-foreground: 0 0% 98%; 39 | --secondary: 0 0% 96.1%; 40 | --secondary-foreground: 0 0% 9%; 41 | --muted: 0 0% 96.1%; 42 | --muted-foreground: 0 0% 45.1%; 43 | --accent: 0 0% 96.1%; 44 | --accent-foreground: 0 0% 9%; 45 | --destructive: 0 84.2% 60.2%; 46 | --destructive-foreground: 0 0% 98%; 47 | --border: 0 0% 89.8%; 48 | --input: 0 0% 89.8%; 49 | --ring: 0 0% 3.9%; 50 | --chart-1: 12 76% 61%; 51 | --chart-2: 173 58% 39%; 52 | --chart-3: 197 37% 24%; 53 | --chart-4: 43 74% 66%; 54 | --chart-5: 27 87% 67%; 55 | --radius: 0.5rem; 56 | } 57 | .dark { 58 | --background: 0 0% 3.9%; 59 | --foreground: 0 0% 98%; 60 | --card: 0 0% 3.9%; 61 | --card-foreground: 0 0% 98%; 62 | --popover: 0 0% 3.9%; 63 | --popover-foreground: 0 0% 98%; 64 | --primary: 0 0% 98%; 65 | --primary-foreground: 0 0% 9%; 66 | --secondary: 0 0% 14.9%; 67 | --secondary-foreground: 0 0% 98%; 68 | --muted: 0 0% 14.9%; 69 | --muted-foreground: 0 0% 63.9%; 70 | --accent: 0 0% 14.9%; 71 | --accent-foreground: 0 0% 98%; 72 | --destructive: 0 62.8% 30.6%; 73 | --destructive-foreground: 0 0% 98%; 74 | --border: 0 0% 14.9%; 75 | --input: 0 0% 14.9%; 76 | --ring: 0 0% 83.1%; 77 | --chart-1: 220 70% 50%; 78 | --chart-2: 160 60% 45%; 79 | --chart-3: 30 80% 55%; 80 | --chart-4: 280 65% 60%; 81 | --chart-5: 340 75% 55%; 82 | } 83 | } 84 | 85 | @layer base { 86 | * { 87 | @apply border-border; 88 | } 89 | body { 90 | @apply bg-background text-foreground; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /background-rater/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | import RQProvider from "./RQProvider"; 6 | 7 | const geistSans = localFont({ 8 | src: "./fonts/GeistVF.woff", 9 | variable: "--font-geist-sans", 10 | weight: "100 900", 11 | }); 12 | const geistMono = localFont({ 13 | src: "./fonts/GeistMonoVF.woff", 14 | variable: "--font-geist-mono", 15 | weight: "100 900", 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Create Next App", 20 | description: "Generated by create next app", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 33 | {children} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /background-rater/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { put } from "@vercel/blob"; 4 | import { addBackground } from "@/db"; 5 | import { Camera } from "@/components/camera"; 6 | 7 | export default function Home() { 8 | async function onUploadImage(formData: FormData) { 9 | "use server"; 10 | const file = formData.get("image") as Blob; 11 | const buffer = await file.arrayBuffer(); 12 | const data = Buffer.from(buffer); 13 | 14 | const randomFileName = Math.random().toString(36).substring(2); 15 | 16 | const { url } = await put(`images/${randomFileName}.jpg`, data, { 17 | access: "public", 18 | }); 19 | 20 | const backgroundId = await addBackground(url); 21 | 22 | redirect(`/rater/${backgroundId}`); 23 | } 24 | 25 | return ( 26 |
27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /background-rater/src/app/rater/[backgroundId]/BackgroundMonitor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from "react"; 3 | import Image from "next/image"; 4 | import Markdown from "react-markdown"; 5 | import { useQuery } from "@tanstack/react-query"; 6 | 7 | import { Skeleton } from "@/components/ui/skeleton"; 8 | 9 | import { Background } from "@/db"; 10 | 11 | export default function BackgroundMonitor({ 12 | backgroundId, 13 | background: initialBackground, 14 | }: { 15 | backgroundId: string; 16 | background: Background; 17 | }) { 18 | const { data } = useQuery({ 19 | queryKey: ["background", backgroundId], 20 | queryFn: async () => { 21 | const response = await fetch(`/api/background/${backgroundId}`); 22 | return response.json(); 23 | }, 24 | initialData: initialBackground, 25 | refetchInterval: 200, 26 | }); 27 | 28 | return ( 29 |
30 |
31 |

What You've Got

32 |
33 | Background 39 |
40 | {data?.review} 41 |
42 |
43 |

What You Need

44 |
45 | {data?.new_background ? ( 46 | Background 53 | ) : ( 54 | 55 | )} 56 |
57 | {data?.theme} 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /background-rater/src/app/rater/[backgroundId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getBackground } from "@/db"; 2 | import Link from "next/link"; 3 | 4 | import { Button } from "@/components/ui/button"; 5 | import BackgroundMonitor from "./BackgroundMonitor"; 6 | 7 | export const dynamic = "force-dynamic"; 8 | export const fetchCache = "force-no-store"; 9 | 10 | export default async function Camera({ 11 | params: { backgroundId }, 12 | }: { 13 | params: { backgroundId: string }; 14 | }) { 15 | const background = await getBackground(+backgroundId); 16 | 17 | if (!background) { 18 | return
Background not found
; 19 | } 20 | 21 | return ( 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /background-rater/src/components/camera.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useRef, useEffect } from "react"; 3 | 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select"; 11 | import { Button } from "@/components/ui/button"; 12 | 13 | export function Camera({ 14 | onUploadImage, 15 | }: { 16 | onUploadImage: (formData: FormData) => void; 17 | }) { 18 | const [devices, setDevices] = useState([]); 19 | const [selectedDevice, setSelectedDevice] = useState(""); 20 | const [isCapturing, setIsCapturing] = useState(false); 21 | const videoRef = useRef(null); 22 | const [imageCapture, setImageCapture] = useState(null); 23 | const [uploading, setUploading] = useState(false); 24 | 25 | useEffect(() => { 26 | async function getDevices() { 27 | await navigator.mediaDevices.getUserMedia({ 28 | video: true, 29 | }); 30 | const devices = await navigator.mediaDevices.enumerateDevices(); 31 | const videoDevices = devices.filter( 32 | (device) => device.kind === "videoinput" 33 | ); 34 | setDevices(videoDevices); 35 | if (videoDevices.length > 0) { 36 | setSelectedDevice(videoDevices[0].deviceId); 37 | } 38 | } 39 | getDevices(); 40 | }, []); 41 | 42 | const startCapture = async () => { 43 | try { 44 | const stream = await navigator.mediaDevices.getUserMedia({ 45 | video: { deviceId: selectedDevice, width: 1920 / 2 }, 46 | }); 47 | if (videoRef.current) { 48 | videoRef.current.srcObject = stream; 49 | setImageCapture(new ImageCapture(stream.getVideoTracks()[0])); 50 | setIsCapturing(true); 51 | } 52 | } catch (err) { 53 | console.error("Error accessing the camera:", err); 54 | } 55 | }; 56 | 57 | const takePhoto = async () => { 58 | if (videoRef.current && imageCapture) { 59 | setUploading(true); 60 | videoRef.current.pause(); 61 | const blob = await imageCapture.takePhoto(); 62 | const formData = new FormData(); 63 | formData.append("image", blob, "image.jpg"); 64 | await onUploadImage(formData); 65 | setUploading(false); 66 | } 67 | }; 68 | 69 | return ( 70 |
71 |

72 | Take A Picture Of Your Workspace 73 |

74 | 75 |
76 |
83 | 84 | {uploading &&
Uploading...
} 85 | {!uploading && ( 86 |
87 | 99 | 100 | 103 | 104 | 107 |
108 | )} 109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /background-rater/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /background-rater/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /background-rater/src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { 5 | CaretSortIcon, 6 | CheckIcon, 7 | ChevronDownIcon, 8 | ChevronUpIcon, 9 | } from "@radix-ui/react-icons" 10 | import * as SelectPrimitive from "@radix-ui/react-select" 11 | 12 | import { cn } from "@/lib/utils" 13 | 14 | const Select = SelectPrimitive.Root 15 | 16 | const SelectGroup = SelectPrimitive.Group 17 | 18 | const SelectValue = SelectPrimitive.Value 19 | 20 | const SelectTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, children, ...props }, ref) => ( 24 | span]:line-clamp-1", 28 | className 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | 37 | )) 38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 39 | 40 | const SelectScrollUpButton = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | 53 | 54 | )) 55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 56 | 57 | const SelectScrollDownButton = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | 70 | 71 | )) 72 | SelectScrollDownButton.displayName = 73 | SelectPrimitive.ScrollDownButton.displayName 74 | 75 | const SelectContent = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, position = "popper", ...props }, ref) => ( 79 | 80 | 91 | 92 | 99 | {children} 100 | 101 | 102 | 103 | 104 | )) 105 | SelectContent.displayName = SelectPrimitive.Content.displayName 106 | 107 | const SelectLabel = React.forwardRef< 108 | React.ElementRef, 109 | React.ComponentPropsWithoutRef 110 | >(({ className, ...props }, ref) => ( 111 | 116 | )) 117 | SelectLabel.displayName = SelectPrimitive.Label.displayName 118 | 119 | const SelectItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | SelectItem.displayName = SelectPrimitive.Item.displayName 140 | 141 | const SelectSeparator = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef 144 | >(({ className, ...props }, ref) => ( 145 | 150 | )) 151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 152 | 153 | export { 154 | Select, 155 | SelectGroup, 156 | SelectValue, 157 | SelectTrigger, 158 | SelectContent, 159 | SelectLabel, 160 | SelectItem, 161 | SelectSeparator, 162 | SelectScrollUpButton, 163 | SelectScrollDownButton, 164 | } 165 | -------------------------------------------------------------------------------- /background-rater/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /background-rater/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "@vercel/postgres"; 2 | 3 | export type Background = { 4 | image: string; 5 | new_background: string | null; 6 | review: string; 7 | review_completed: boolean; 8 | theme: string | null; 9 | }; 10 | 11 | export async function addBackground(image: string): Promise { 12 | await sql`INSERT INTO backgrounds (image) VALUES (${image})`; 13 | const result = 14 | await sql`SELECT currval(pg_get_serial_sequence('backgrounds', 'id'))`; 15 | return result.rows[0].currval; 16 | } 17 | 18 | export async function setNewBackground( 19 | id: number, 20 | newBackground: string 21 | ): Promise { 22 | await sql`UPDATE backgrounds SET new_background=${newBackground} WHERE id=${id}`; 23 | } 24 | 25 | export async function setReview(id: number, review: string): Promise { 26 | console.log("setting review", id, review); 27 | await sql`UPDATE backgrounds SET review=${review} WHERE id=${id}`; 28 | } 29 | 30 | export async function setReviewCompleted( 31 | id: number, 32 | reviewCompleted: boolean 33 | ): Promise { 34 | await sql`UPDATE backgrounds SET review_completed=${reviewCompleted} WHERE id=${id}`; 35 | } 36 | 37 | export async function setTheme(id: number, theme: string): Promise { 38 | await sql`UPDATE backgrounds SET theme=${theme} WHERE id=${id}`; 39 | } 40 | 41 | export async function getBackground(id: number): Promise { 42 | const result = await sql`SELECT * FROM backgrounds WHERE id=${id}`; 43 | return result.rows[0] as Background | null; 44 | } 45 | -------------------------------------------------------------------------------- /background-rater/src/db/schema.psql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS backgrounds ( 2 | id SERIAL PRIMARY KEY, 3 | image TEXT NOT NULL, 4 | new_background TEXT, 5 | review TEXT, 6 | review_completed BOOLEAN DEFAULT FALSE, 7 | theme TEXT 8 | ); -------------------------------------------------------------------------------- /background-rater/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /background-rater/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./src/**/*.{js,ts,jsx,tsx,mdx}" 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | } 54 | }, 55 | borderRadius: { 56 | lg: 'var(--radius)', 57 | md: 'calc(var(--radius) - 2px)', 58 | sm: 'calc(var(--radius) - 4px)' 59 | } 60 | } 61 | }, 62 | plugins: [require("tailwindcss-animate")], 63 | }; 64 | export default config; 65 | -------------------------------------------------------------------------------- /background-rater/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------