Build a Next.js frontend dashboard for the Maya Transcribe API. This is a transcription pipeline evaluation tool.

## Backend API
The backend is running at `http://localhost:8000`. Here are the endpoints:

### 1. GET /api/models
Returns available Gemini models with pricing:
```json
[
  {
    "model": "gemini-2.5-flash",
    "input_text_per_1m": 0.30,
    "input_audio_per_1m": 1.00,
    "output_per_1m": 2.50,
    "supports_thinking": true,
    "supports_structured_output": true,
    "max_output_tokens": 65536
  }
]
```

### 2. GET /api/videos/{video_id}/status
Check video availability. Returns:
```json
{
  "video_id": "abc123",
  "exists_in_supabase": true,
  "status": "complete",
  "exists_in_r2": true,
  "segment_count": 307,
  "segments": ["videoID/segments/SPEAKER_00_0000.flac", ...],
  "metadata": { "video_title": "...", "language": "bengali" }
}
```

### 3. POST /api/videos/{video_id}/transcribe
Transcribe segments:
```json
// Request
{
  "segments": ["videoID/segments/SPEAKER_00_0000.flac"],
  "models": ["gemini-2.5-flash", "gemini-2.0-flash"],
  "prompt": "Custom prompt or null for default",
  "language": "auto",
  "temperature": 0.0
}
// Response
{
  "results": [
    { "segment": "...", "model": "gemini-2.5-flash", "transcription": "...", "processing_time": 3.2 }
  ],
  "errors": [],
  "total_time": 9.5
}
```

### 4. POST /api/transcribe/upload (multipart/form-data)
Upload audio file directly:
- file: audio file
- models: comma-separated model names
- language: language hint
- temperature: 0.0-2.0

## UI Requirements

### Theme
- Dark theme, modern aesthetic
- Use shadcn/ui components or similar
- Clean, minimal design

### Layout
1. **Header**: "Maya Transcribe" title, dark theme

2. **Video Input Section**:
   - Text input for YouTube video ID
   - "Check Status" button
   - Status display showing: Supabase status, R2 status, segment count
   - Show video title and language from metadata

3. **Segment Selection**:
   - Toggle: "All Segments" vs "Select Specific"
   - When specific: searchable/filterable list of segments
   - Show segment name and duration
   - Multi-select checkboxes

4. **Model Selection**:
   - Multi-select cards/chips for models
   - Show pricing per model
   - Show capabilities (thinking, audio support)
   - Available models: gemini-3-pro-preview, gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.0-flash, gemini-2.0-flash-lite

5. **Options Panel**:
   - Custom prompt textarea (show default prompt as placeholder)
   - Language dropdown: auto, telugu, hindi, tamil, kannada, malayalam, bengali, gujarati, marathi, punjabi, odia, assamese, english
   - Temperature slider (0.0 - 2.0)

6. **Audio Upload Tab**:
   - Drag & drop zone
   - Or file browser button
   - Same model/options as above

7. **Results Section**:
   - Tabbed view by model for comparison
   - Show transcription text
   - Show processing time
   - Copy button per result
   - Download all as JSON button

8. **Transcribe Button**:
   - Large, prominent button
   - Show loading state during transcription
   - Progress indicator for multiple segments

## Tech Stack
- Next.js 14+ (App Router)
- TypeScript
- Tailwind CSS
- shadcn/ui or similar component library
- React Query or SWR for data fetching

## File Structure
Create in: /home/ubuntu/maya_transcribe/frontend/

## Environment
The Vercel API key is available in .env as VERCEL=8rILFwMnHvUlNe2gNvDD6k5n

Create the complete frontend application with all components. Make it production-ready.


Bash

List contents of maya_transcribe directory


total 160
drwxrwxr-x  5 ubuntu ubuntu  4096 Jan 23 04:51 .
drwxr-x--- 34 ubuntu ubuntu  4096 Jan 23 04:20 ..
-rw-rw-r--  1 ubuntu ubuntu  6981 Jan 23 04:51 API_SPEC.md
drwxrwxr-x  2 ubuntu ubuntu  4096 Jan 23 05:03 .claude
drwxrwxr-x  3 ubuntu ubuntu  4096 Jan 23 01:09 docs
-rw-rw-r--  1 ubuntu ubuntu  1203 Jan 23 04:49 .env
-rw-rw-r--  1 ubuntu ubuntu  3458 Jan 23 04:41 gemini_models.md
-rw-rw-r--  1 ubuntu ubuntu 35625 Jan 23 04:04 indicwhisper_eval_10samples.json
-rw-rw-r--  1 ubuntu ubuntu 17798 Jan 23 04:02 indicwhisper_eval_results.json
-rw-rw-r--  1 ubuntu ubuntu 35536 Jan 23 04:19 indicwhisper_gemini25_eval.json
-rw-rw-r--  1 ubuntu ubuntu 11880 Jan 23 02:08 PROMPT.md
drwxrwxr-x 12 ubuntu ubuntu  4096 Jan 23 04:50 transcribe
-rw-rw-r--  1 ubuntu ubuntu 17039 Jan 23 00:39 User.md


Create frontend directory


cd /home/ubuntu/maya_transcribe/frontend && npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm --yes 2>&1

Create Next.js app with TypeScript and Tailwind


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript

npm warn exec The following package was not found and will be installed: create-next-app@16.1.4
Creating a new Next.js app in /home/ubuntu/maya_transcribe/frontend.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- next
- react
- react-dom
Installing devDependencies:
- @tailwindcss/postcss
- @types/node
- @types/react
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript


Output:
- typescript
added 357 packages, and audited 358 packages in 26s
142 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities

npm warn exec The following package was not found and will be installed: create-next-app@16.1.4
Creating a new Next.js app in /home/ubuntu/maya_transcribe/frontend.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- next
- react
- react-dom
Installing devDependencies:
- @tailwindcss/postcss
- @types/node
- @types/react
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript
added 357 packages, and audited 358 packages in 26s
142 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities


npm warn exec The following package was not found and will be installed: create-next-app@16.1.4
Creating a new Next.js app in /home/ubuntu/maya_transcribe/frontend.

Using npm.

Initializing project with template: app-tw

Installing dependencies:
- next
- react
- react-dom

Installing devDependencies:
- @tailwindcss/postcss
- @types/node
- @types/react
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript

added 357 packages, and audited 358 packages in 26s

142 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Generating route types...
✓ Types generated successfully

Initialized a git repository.

Success! Created frontend at /home/ubuntu/maya_transcribe/frontend


cd /home/ubuntu/maya_transcribe/frontend && npx shadcn@latest init -d 2>&1

Initialize shadcn/ui


Output: npm warn exec The following package was not found and will be installed: shadcn@3.7.0

npm warn exec The following package was not found and will be installed: shadcn@3.7.0


Output: npm warn exec The following package was not found and will be installed: shadcn@3.7.0


Output: npm warn exec The following package was not found and will be installed: shadcn@3.7.0


Output: npm warn exec The following package was not found and will be installed: shadcn@3.7.0


Output: npm warn exec The following package was not found and will be installed: shadcn@3.7.0


Output: npm warn exec The following package was not found and will be installed: shadcn@3.7.0


Output: npm warn exec The following package was not found and will be installed: shadcn@3.7.0


Output:
npm warn exec The following package was not found and will be installed: shadcn@3.7.0
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead

npm warn exec The following package was not found and will be installed: shadcn@3.7.0
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead


Output:
npm warn exec The following package was not found and will be installed: shadcn@3.7.0
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead


Output:
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.

npm warn exec The following package was not found and will be installed: shadcn@3.7.0
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead
- Preflight checks.
✔ Preflight checks.
- Verifying framework.
✔ Verifying framework. Found Next.js.
- Validating Tailwind CSS config. Found v4.
✔ Validating Tailwind CSS config. Found v4.
- Validating import alias.
✔ Validating import alias.
- Writing components.json.
✔ Writing components.json.
- Checking registry.
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.


Output:
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.


Output:
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.


Output:
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.


Output:
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.


Output:
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.

npm warn exec The following package was not found and will be installed: shadcn@3.7.0
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead
- Preflight checks.
✔ Preflight checks.
- Verifying framework.
✔ Verifying framework. Found Next.js.
- Validating Tailwind CSS config. Found v4.
✔ Validating Tailwind CSS config. Found v4.
- Validating import alias.
✔ Validating import alias.
- Writing components.json.
✔ Writing components.json.
- Checking registry.
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.


npm warn exec The following package was not found and will be installed: shadcn@3.7.0
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead
- Preflight checks.
✔ Preflight checks.
- Verifying framework.
✔ Verifying framework. Found Next.js.
- Validating Tailwind CSS config. Found v4.
✔ Validating Tailwind CSS config. Found v4.
- Validating import alias.
✔ Validating import alias.
- Writing components.json.
✔ Writing components.json.
- Checking registry.
✔ Checking registry.
- Updating CSS variables in src/app/globals.css
✔ Updating CSS variables in src/app/globals.css
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.
✔ Created 1 file:
  - src/lib/utils.ts

Success! Project initialization completed.
You may now add components.


cd /home/ubuntu/maya_transcribe/frontend && npx shadcn@latest add button card input label select slider tabs textarea checkbox badge scroll-area progress toast separator switch -y 2>&1

Add shadcn/ui components


Error: Exit code 1

The toast component is deprecated. Use the sonner component instead.


cd /home/ubuntu/maya_transcribe/frontend && npx shadcn@latest add button card input label select slider tabs textarea checkbox badge scroll-area progress sonner separator switch -y 2>&1

Add shadcn/ui components with sonner instead of toast


Output:
- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.

- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.


Output:
- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.


Output:
- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.


Output:
- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.


Output:
- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.


Output:
- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.


Output:
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.

- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.


Output:
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.


Output:
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.


- Checking registry.
✔ Checking registry.
- Installing dependencies.
- Installing dependencies.
✔ Installing dependencies.
- Updating files.
✔ Created 15 files:
  - src/components/ui/button.tsx
  - src/components/ui/card.tsx
  - src/components/ui/input.tsx
  - src/components/ui/label.tsx
  - src/components/ui/select.tsx
  - src/components/ui/slider.tsx
  - src/components/ui/tabs.tsx
  - src/components/ui/textarea.tsx
  - src/components/ui/checkbox.tsx
  - src/components/ui/badge.tsx
  - src/components/ui/scroll-area.tsx
  - src/components/ui/progress.tsx
  - src/components/ui/sonner.tsx
  - src/components/ui/separator.tsx
  - src/components/ui/switch.tsx


cd /home/ubuntu/maya_transcribe/frontend && npm install @tanstack/react-query lucide-react 2>&1

Install React Query and Lucide icons


added 2 packages, and audited 428 packages in 2s

150 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities


Now let me read the current files to understand the setup.


     1→@import "tailwindcss";
     2→@import "tw-animate-css";
     3→
     4→@custom-variant dark (&:is(.dark *));
     5→
     6→@theme inline {
     7→  --color-background: var(--background);
     8→  --color-foreground: var(--foreground);
     9→  --font-sans: var(--font-geist-sans);
    10→  --font-mono: var(--font-geist-mono);
    11→  --color-sidebar-ring: var(--sidebar-ring);
    12→  --color-sidebar-border: var(--sidebar-border);
    13→  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
    14→  --color-sidebar-accent: var(--sidebar-accent);
    15→  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
    16→  --color-sidebar-primary: var(--sidebar-primary);
    17→  --color-sidebar-foreground: var(--sidebar-foreground);
    18→  --color-sidebar: var(--sidebar);
    19→  --color-chart-5: var(--chart-5);
    20→  --color-chart-4: var(--chart-4);
    21→  --color-chart-3: var(--chart-3);
    22→  --color-chart-2: var(--chart-2);
    23→  --color-chart-1: var(--chart-1);
    24→  --color-ring: var(--ring);
    25→  --color-input: var(--input);
    26→  --color-border: var(--border);
    27→  --color-destructive: var(--destructive);
    28→  --color-accent-foreground: var(--accent-foreground);
    29→  --color-accent: var(--accent);
    30→  --color-muted-foreground: var(--muted-foreground);
    31→  --color-muted: var(--muted);
    32→  --color-secondary-foreground: var(--secondary-foreground);
    33→  --color-secondary: var(--secondary);
    34→  --color-primary-foreground: var(--primary-foreground);
    35→  --color-primary: var(--primary);
    36→  --color-popover-foreground: var(--popover-foreground);
    37→  --color-popover: var(--popover);
    38→  --color-card-foreground: var(--card-foreground);
    39→  --color-card: var(--card);
    40→  --radius-sm: calc(var(--radius) - 4px);
    41→  --radius-md: calc(var(--radius) - 2px);
    42→  --radius-lg: var(--radius);
    43→  --radius-xl: calc(var(--radius) + 4px);
    44→  --radius-2xl: calc(var(--radius) + 8px);
    45→  --radius-3xl: calc(var(--radius) + 12px);
    46→  --radius-4xl: calc(var(--radius) + 16px);
    47→}
    48→
    49→:root {
    50→  --radius: 0.625rem;
    51→  --background: oklch(1 0 0);
    52→  --foreground: oklch(0.145 0 0);
    53→  --card: oklch(1 0 0);
    54→  --card-foreground: oklch(0.145 0 0);
    55→  --popover: oklch(1 0 0);
    56→  --popover-foreground: oklch(0.145 0 0);
    57→  --primary: oklch(0.205 0 0);
    58→  --primary-foreground: oklch(0.985 0 0);
    59→  --secondary: oklch(0.97 0 0);
    60→  --secondary-foreground: oklch(0.205 0 0);
    61→  --muted: oklch(0.97 0 0);
    62→  --muted-foreground: oklch(0.556 0 0);
    63→  --accent: oklch(0.97 0 0);
    64→  --accent-foreground: oklch(0.205 0 0);
    65→  --destructive: oklch(0.577 0.245 27.325);
    66→  --border: oklch(0.922 0 0);
    67→  --input: oklch(0.922 0 0);
    68→  --ring: oklch(0.708 0 0);
    69→  --chart-1: oklch(0.646 0.222 41.116);
    70→  --chart-2: oklch(0.6 0.118 184.704);
    71→  --chart-3: oklch(0.398 0.07 227.392);
    72→  --chart-4: oklch(0.828 0.189 84.429);
    73→  --chart-5: oklch(0.769 0.188 70.08);
    74→  --sidebar: oklch(0.985 0 0);
    75→  --sidebar-foreground: oklch(0.145 0 0);
    76→  --sidebar-primary: oklch(0.205 0 0);
    77→  --sidebar-primary-foreground: oklch(0.985 0 0);
    78→  --sidebar-accent: oklch(0.97 0 0);
    79→  --sidebar-accent-foreground: oklch(0.205 0 0);
    80→  --sidebar-border: oklch(0.922 0 0);
    81→  --sidebar-ring: oklch(0.708 0 0);
    82→}
    83→
    84→.dark {
    85→  --background: oklch(0.145 0 0);
    86→  --foreground: oklch(0.985 0 0);
    87→  --card: oklch(0.205 0 0);
    88→  --card-foreground: oklch(0.985 0 0);
    89→  --popover: oklch(0.205 0 0);
    90→  --popover-foreground: oklch(0.985 0 0);
    91→  --primary: oklch(0.922 0 0);
    92→  --primary-foreground: oklch(0.205 0 0);
    93→  --secondary: oklch(0.269 0 0);
    94→  --secondary-foreground: oklch(0.985 0 0);
    95→  --muted: oklch(0.269 0 0);
    96→  --muted-foreground: oklch(0.708 0 0);
    97→  --accent: oklch(0.269 0 0);
    98→  --accent-foreground: oklch(0.985 0 0);
    99→  --destructive: oklch(0.704 0.191 22.216);
   100→  --border: oklch(1 0 0 / 10%);
   101→  --input: oklch(1 0 0 / 15%);
   102→  --ring: oklch(0.556 0 0);
   103→  --chart-1: oklch(0.488 0.243 264.376);
   104→  --chart-2: oklch(0.696 0.17 162.48);
   105→  --chart-3: oklch(0.769 0.188 70.08);
   106→  --chart-4: oklch(0.627 0.265 303.9);
   107→  --chart-5: oklch(0.645 0.246 16.439);
   108→  --sidebar: oklch(0.205 0 0);
   109→  --sidebar-foreground: oklch(0.985 0 0);
   110→  --sidebar-primary: oklch(0.488 0.243 264.376);
   111→  --sidebar-primary-foreground: oklch(0.985 0 0);
   112→  --sidebar-accent: oklch(0.269 0 0);
   113→  --sidebar-accent-foreground: oklch(0.985 0 0);
   114→  --sidebar-border: oklch(1 0 0 / 10%);
   115→  --sidebar-ring: oklch(0.556 0 0);
   116→}
   117→
   118→@layer base {
   119→  * {
   120→    @apply border-border outline-ring/50;
   121→  }
   122→  body {
   123→    @apply bg-background text-foreground;
   124→  }
   125→}
   126→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import type { Metadata } from "next";
     2→import { Geist, Geist_Mono } from "next/font/google";
     3→import "./globals.css";
     4→
     5→const geistSans = Geist({
     6→  variable: "--font-geist-sans",
     7→  subsets: ["latin"],
     8→});
     9→
    10→const geistMono = Geist_Mono({
    11→  variable: "--font-geist-mono",
    12→  subsets: ["latin"],
    13→});
    14→
    15→export const metadata: Metadata = {
    16→  title: "Create Next App",
    17→  description: "Generated by create next app",
    18→};
    19→
    20→export default function RootLayout({
    21→  children,
    22→}: Readonly<{
    23→  children: React.ReactNode;
    24→}>) {
    25→  return (
    26→    <html lang="en">
    27→      <body
    28→        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
    29→      >
    30→        {children}
    31→      </body>
    32→    </html>
    33→  );
    34→}
    35→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→import Image from "next/image";
     2→
     3→export default function Home() {
     4→  return (
     5→

     6→
     7→        <Image
     8→          className="dark:invert"
     9→          src="/next.svg"
    10→          alt="Next.js logo"
    11→          width={100}
    12→          height={20}
    13→          priority
    14→        />
    15→

    16→

    17→            To get started, edit the page.tsx file.
    18→

    19→

    20→            Looking for a starting point or more instructions? Head over to{" "}
    21→
    25→              Templates
    26→            {" "}
    27→            or the{" "}
    28→
    32→              Learning
    33→            {" "}
    34→            center.
    35→

    36→

    37→

    38→
    44→            <Image
    45→              className="dark:invert"
    46→              src="/vercel.svg"
    47→              alt="Vercel logomark"
    48→              width={16}
    49→              height={16}
    50→            />
    51→            Deploy Now
    52→
    53→
    59→            Documentation
    60→
    61→

    62→
    63→

    64→  );
    65→}
    66→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→{
     2→  "$schema": "https://ui.shadcn.com/schema.json",
     3→  "style": "new-york",
     4→  "rsc": true,
     5→  "tsx": true,
     6→  "tailwind": {
     7→    "config": "",
     8→    "css": "src/app/globals.css",
     9→    "baseColor": "neutral",
    10→    "cssVariables": true,
    11→    "prefix": ""
    12→  },
    13→  "iconLibrary": "lucide",
    14→  "aliases": {
    15→    "components": "@/components",
    16→    "utils": "@/lib/utils",
    17→    "ui": "@/components/ui",
    18→    "lib": "@/lib",
    19→    "hooks": "@/hooks"
    20→  },
    21→  "registries": {}
    22→}
    23→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now I'll create the complete frontend application. Let me start by creating the types, API client, and then the components.


Write

// API Types for Maya Transcribe

export interface GeminiModel {
  model: string;
  input_text_per_1m: number;
  input_audio_per_1m: number;
  output_per_1m: number;
  supports_thinking: boolean;
  supports_structured_output: boolean;
  max_output_tokens: number;
}

export interface VideoMetadata {
  video_title?: string;
  language?: string;
  [key: string]: unknown;
}

export interface VideoStatus {
  video_id: string;
  exists_in_supabase: boolean;
  status: string;
  exists_in_r2: boolean;
  segment_count: number;
  segments: string[];
  metadata: VideoMetadata;
}

export interface TranscribeRequest {
  segments: string[];
  models: string[];
  prompt: string | null;
  language: string;
  temperature: number;
}

export interface TranscriptionResult {
  segment: string;
  model: string;
  transcription: string;
  processing_time: number;
}

export interface TranscribeResponse {
  results: TranscriptionResult[];
  errors: string[];
  total_time: number;
}

export interface UploadResponse {
  results: TranscriptionResult[];
  errors: string[];
  total_time: number;
}

export type Language =
  | "auto"
  | "telugu"
  | "hindi"
  | "tamil"
  | "kannada"
  | "malayalam"
  | "bengali"
  | "gujarati"
  | "marathi"
  | "punjabi"
  | "odia"
  | "assamese"
  | "english";

export const LANGUAGES: { value: Language; label: string }[] = [
  { value: "auto", label: "Auto Detect" },
  { value: "telugu", label: "Telugu" },
  { value: "hindi", label: "Hindi" },
  { value: "tamil", label: "Tamil" },
  { value: "kannada", label: "Kannada" },
  { value: "malayalam", label: "Malayalam" },
  { value: "bengali", label: "Bengali" },
  { value: "gujarati", label: "Gujarati" },
  { value: "marathi", label: "Marathi" },
  { value: "punjabi", label: "Punjabi" },
  { value: "odia", label: "Odia" },
  { value: "assamese", label: "Assamese" },
  { value: "english", label: "English" },
];

export const DEFAULT_PROMPT = `You are a transcription assistant. Transcribe the audio accurately, preserving the original language and any code-switching. Output only the transcription without any additional commentary.`;


File created successfully at: /home/ubuntu/maya_transcribe/frontend/src/types/index.ts


// API Client for Maya Transcribe Backend

import type {
  GeminiModel,
  VideoStatus,
  TranscribeRequest,
  TranscribeResponse,
  UploadResponse,
} from "@/types";

const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";

class ApiError extends Error {
  constructor(
    public status: number,
    message: string
  ) {
    super(message);
    this.name = "ApiError";
  }
}

async function handleResponse<T>(response: Response): Promise<T> {
  if (!response.ok) {
    const text = await response.text();
    let message = `HTTP ${response.status}`;
    try {
      const json = JSON.parse(text);
      message = json.detail || json.message || message;
    } catch {
      message = text || message;
    }
    throw new ApiError(response.status, message);
  }
  return response.json();
}

export async function getModels(): Promise<GeminiModel[]> {
  const response = await fetch(`${API_BASE_URL}/api/models`);
  return handleResponse<GeminiModel[]>(response);
}

export async function getVideoStatus(videoId: string): Promise<VideoStatus> {
  const response = await fetch(`${API_BASE_URL}/api/videos/${encodeURIComponent(videoId)}/status`);
  return handleResponse<VideoStatus>(response);
}

export async function transcribeSegments(
  videoId: string,
  request: TranscribeRequest
): Promise<TranscribeResponse> {
  const response = await fetch(
    `${API_BASE_URL}/api/videos/${encodeURIComponent(videoId)}/transcribe`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(request),
    }
  );
  return handleResponse<TranscribeResponse>(response);
}

export async function uploadAndTranscribe(
  file: File,
  models: string[],
  language: string,
  temperature: number
): Promise<UploadResponse> {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("models", models.join(","));
  formData.append("language", language);
  formData.append("temperature", temperature.toString());

  const response = await fetch(`${API_BASE_URL}/api/transcribe/upload`, {
    method: "POST",
    body: formData,
  });
  return handleResponse<UploadResponse>(response);
}

export { ApiError };


File created successfully at: /home/ubuntu/maya_transcribe/frontend/src/lib/api.ts


"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";
import { Toaster } from "@/components/ui/sonner";

export function Providers({ children }: { children: ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000,
            retry: 1,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <Toaster position="top-right" richColors />
    </QueryClientProvider>
  );
}


File created successfully at: /home/ubuntu/maya_transcribe/frontend/src/components/providers.tsx


"use client";

import { AudioLines } from "lucide-react";

export function Header() {
  return (

          <AudioLines className="h-6 w-6 text-primary" />
          Maya Transcribe

            Transcription Pipeline Evaluation

  );
}


File created successfully at: /home/ubuntu/maya_transcribe/frontend/src/components/header.tsx


"use client";

import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { Loader2, CheckCircle2, XCircle, Database, Cloud, FileAudio } from "lucide-react";
import { toast } from "sonner";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";

import { getVideoStatus } from "@/lib/api";
import type { VideoStatus } from "@/types";

interface VideoInputProps {
  onVideoStatusLoaded: (status: VideoStatus) => void;
}

export function VideoInput({ onVideoStatusLoaded }: VideoInputProps) {
  const [videoId, setVideoId] = useState("");
  const [videoStatus, setVideoStatus] = useState<VideoStatus | null>(null);

  const checkStatus = useMutation({
    mutationFn: (id: string) => getVideoStatus(id),
    onSuccess: (data) => {
      setVideoStatus(data);
      onVideoStatusLoaded(data);
      toast.success("Video status loaded successfully");
    },
    onError: (error) => {
      toast.error(`Failed to check video status: ${error.message}`);
      setVideoStatus(null);
    },
  });

  const handleCheck = () => {
    if (!videoId.trim()) {
      toast.error("Please enter a video ID");
      return;
    }
    checkStatus.mutate(videoId.trim());
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") {
      handleCheck();
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <FileAudio className="h-5 w-5" />
          Video Input
        </CardTitle>
        <CardDescription>
          Enter a YouTube video ID to check availability and load segments
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">

              Video ID

            <Input
              id="video-id"
              placeholder="Enter YouTube video ID (e.g., abc123)"
              value={videoId}
              onChange={(e) => setVideoId(e.target.value)}
              onKeyDown={handleKeyDown}
              disabled={checkStatus.isPending}
            />

          <Button onClick={handleCheck} disabled={checkStatus.isPending || !videoId.trim()}>
            {checkStatus.isPending ? (
              <>
                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                Checking...
              </>
            ) : (
              "Check Status"
            )}
          </Button>

        {videoStatus && (

              <StatusBadge
                icon={<Database className="h-3.5 w-3.5" />}
                label="Supabase"
                status={videoStatus.exists_in_supabase}
              />
              <StatusBadge
                icon={<Cloud className="h-3.5 w-3.5" />}
                label="R2 Storage"
                status={videoStatus.exists_in_r2}
              />
              <Badge variant="outline" className="gap-1">
                <FileAudio className="h-3.5 w-3.5" />
                {videoStatus.segment_count} segments
              </Badge>
              {videoStatus.status && (
                <Badge
                  variant={videoStatus.status === "complete" ? "default" : "secondary"}
                  className="capitalize"
                >
                  {videoStatus.status}
                </Badge>
              )}

            {videoStatus.metadata && (

                {videoStatus.metadata.video_title && (

                    Title:{" "}
                    {videoStatus.metadata.video_title}

                )}
                {videoStatus.metadata.language && (

                    Language:{" "}
                    {videoStatus.metadata.language}

                )}

            )}

        )}
      </CardContent>
    </Card>
  );
}

function StatusBadge({
  icon,
  label,
  status,
}: {
  icon: React.ReactNode;
  label: string;
  status: boolean;
}) {
  return (
    <Badge variant={status ? "default" : "destructive"} className="gap-1">
      {icon}
      {label}
      {status ? (
        <CheckCircle2 className="h-3.5 w-3.5" />
      ) : (
        <XCircle className="h-3.5 w-3.5" />
      )}
    </Badge>
  );
}


File created successfully at: /home/ubuntu/maya_transcribe/frontend/src/components/video-input.tsx


"use client";

import { useState, useMemo } from "react";
import { Search, FileAudio, CheckSquare, Square } from "lucide-react";

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";

interface SegmentSelectorProps {
  segments: string[];
  selectedSegments: string[];
  onSelectionChange: (segments: string[]) => void;
  selectAll: boolean;
  onSelectAllChange: (selectAll: boolean) => void;
}

export function SegmentSelector({
  segments,
  selectedSegments,
  onSelectionChange,
  selectAll,
  onSelectAllChange,
}: SegmentSelectorProps) {
  const [searchQuery, setSearchQuery] = useState("");

  const filteredSegments = useMemo(() => {
    if (!searchQuery.trim()) return segments;
    const query = searchQuery.toLowerCase();
    return segments.filter((segment) => segment.toLowerCase().includes(query));
  }, [segments, searchQuery]);

  const getSegmentName = (segment: string) => {
    const parts = segment.split("/");
    return parts[parts.length - 1];
  };

  const getSpeakerId = (segment: string) => {
    const name = getSegmentName(segment);
    const match = name.match(/SPEAKER_(\d+)/);
    return match ? `Speaker ${match[1]}` : null;
  };

  const toggleSegment = (segment: string) => {
    if (selectedSegments.includes(segment)) {
      onSelectionChange(selectedSegments.filter((s) => s !== segment));
    } else {
      onSelectionChange([...selectedSegments, segment]);
    }
  };

  const selectAllFiltered = () => {
    const newSelection = new Set(selectedSegments);
    filteredSegments.forEach((s) => newSelection.add(s));
    onSelectionChange(Array.from(newSelection));
  };

  const deselectAllFiltered = () => {
    const filteredSet = new Set(filteredSegments);
    onSelectionChange(selectedSegments.filter((s) => !filteredSet.has(s)));
  };

  const isAllFilteredSelected = filteredSegments.every((s) =>
    selectedSegments.includes(s)
  );

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <FileAudio className="h-5 w-5" />
          Segment Selection
        </CardTitle>
        <CardDescription>
          Select which audio segments to transcribe
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">

            <Switch
              id="select-all"
              checked={selectAll}
              onCheckedChange={onSelectAllChange}
            />

              All Segments ({segments.length})

          {!selectAll && (
            <Badge variant="secondary">
              {selectedSegments.length} selected
            </Badge>
          )}

        {!selectAll && segments.length > 0 && (

                <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
                <Input
                  placeholder="Search segments..."
                  value={searchQuery}
                  onChange={(e) => setSearchQuery(e.target.value)}
                  className="pl-8"
                />

              <Button
                variant="outline"
                size="sm"
                onClick={isAllFilteredSelected ? deselectAllFiltered : selectAllFiltered}
                className="whitespace-nowrap"
              >
                {isAllFilteredSelected ? (
                  <>
                    <Square className="mr-1.5 h-3.5 w-3.5" />
                    Deselect All
                  </>
                ) : (
                  <>
                    <CheckSquare className="mr-1.5 h-3.5 w-3.5" />
                    Select All
                  </>
                )}
              </Button>

            <ScrollArea className="h-[280px] rounded-md border">

                {filteredSegments.length === 0 ? (

                    No segments found matching "{searchQuery}"

                ) : (
                  filteredSegments.map((segment) => (

                      <Checkbox
                        checked={selectedSegments.includes(segment)}
                        onCheckedChange={() => toggleSegment(segment)}
                      />

                          {getSegmentName(segment)}

                        {getSpeakerId(segment) && (

                            {getSpeakerId(segment)}

                        )}

                  ))
                )}

            </ScrollArea>

        )}

        {!selectAll && segments.length === 0 && (

            No segments available. Please check a video status first.

        )}
      </CardContent>
    </Card>
  );
}


Error: Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). Try a different approach or report the limitation to complete your task.


[Request interrupted by user for tool use]