Explore the codebase for voice transcription functionality. I need to understand:
1. Any voice input components
2. Any transcription API endpoints (e.g., /api/ai/transcribe or similar)
3. Any audio recording utilities
4. How voice input is used in forms

Search for files related to voice, transcription, audio, microphone, whisper. Report back full file paths and contents.


I'll search the codebase for voice transcription functionality, audio recording utilities, and related components.


No files found


Good! I found references to transcription endpoints. Let me search the source code more specifically:


/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts
/home/ccuser/the-50-dollar-app/src/lib/abn-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/company-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/api-errors.ts
/home/ccuser/the-50-dollar-app/src/lib/csrf.ts
/home/ccuser/the-50-dollar-app/src/lib/stripe/server.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/client.ts.backup
/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts
/home/ccuser/the-50-dollar-app/src/lib/messaging.ts
/home/ccuser/the-50-dollar-app/src/lib/sanitize.ts
/home/ccuser/the-50-dollar-app/src/lib/location-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/rate-limit.ts
/home/ccuser/the-50-dollar-app/src/lib/error-handling.tsx
/home/ccuser/the-50-dollar-app/src/lib/types.ts
/home/ccuser/the-50-dollar-app/src/lib/utils.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/client.ts


Let me search more broadly in the app directory:


Bash

find /home/ccuser/the-50-dollar-app -type f -name "*.ts" -o -name "*.tsx" | grep -E "(api|component)" | head -50


find /home/ccuser/the-50-dollar-app/app -type f \( -name "*.ts" -o -name "*.tsx" \) 2>/dev/null | head -100


<tool_use_error>Path does not exist: /home/ccuser/the-50-dollar-app/app</tool_use_error>

Error: Path does not exist: /home/ccuser/the-50-dollar-app/app


Let me check the directory structure:


ls -la /home/ccuser/the-50-dollar-app/ | head -30


total 1736
drwxrwxr-x  10 ccuser ccuser    4096 Feb  8 12:04 .
drwxr-x---  19 ccuser ccuser    4096 Feb  8 19:18 ..
-rw-r--r--   1 ccuser ccuser    2190 Feb  8 11:21 .env.local
drwxrwxr-x   8 ccuser ccuser    4096 Feb  8 19:18 .git
-rw-rw-r--   1 ccuser ccuser     496 Feb  8 11:18 .gitignore
drwxrwxr-x   8 ccuser ccuser    4096 Feb  8 13:00 .next
-rw-rw-r--   1 ccuser ccuser    4319 Feb  8 11:18 AI-PROFILE-BUILDER.md
-rw-rw-r--   1 ccuser ccuser    6216 Feb  8 11:18 APPLICATION-CODE-UPDATES-FOR-RLS-FIX.md
-rw-rw-r--   1 ccuser ccuser    5238 Feb  8 11:18 AUDIT-C3-COMPLETION.md
-rw-rw-r--   1 ccuser ccuser    2213 Feb  8 11:18 AUDIT-C4-COMPLETION.md
-rw-rw-r--   1 ccuser ccuser    6276 Feb  8 11:18 AUDIT-C5-COMPLETION.md
-rw-rw-r--   1 ccuser ccuser    5447 Feb  8 11:18 AUDIT-H1-H5-H11-H14-M16-M17-COMPLETION.md
-rw-rw-r--   1 ccuser ccuser    4394 Feb  8 11:18 CLAUDE.md
-rw-rw-r--   1 ccuser ccuser    3115 Feb  8 11:18 CSRF_IMPLEMENTATION.md
-rw-rw-r--   1 ccuser ccuser   13785 Feb  8 11:18 PRODUCT-SPEC.md
-rw-rw-r--   1 ccuser ccuser    2179 Feb  8 11:18 README.md
-rw-rw-r--   1 ccuser ccuser    9184 Feb  8 11:18 SUPABASE-KEY-ROTATION-GUIDE.md
-rw-rw-r--   1 ccuser ccuser    8977 Feb  8 11:18 UX-IMPLEMENTATION-GUIDE.md
-rw-rw-r--   1 ccuser ccuser    3480 Feb  8 11:18 VERCEL-DEPLOYMENT.md
-rw-rw-r--   1 ccuser ccuser     467 Feb  8 11:18 components.json
drwxrwxr-x   2 ccuser ccuser    4096 Feb  8 11:18 docs
-rw-rw-r--   1 ccuser ccuser     465 Feb  8 11:18 eslint.config.mjs
-rw-rw-r--   1 ccuser ccuser     247 Feb  8 11:23 next-env.d.ts
-rw-rw-r--   1 ccuser ccuser    1651 Feb  8 11:18 next.config.ts
drwxr-xr-x 321 ccuser ccuser   12288 Feb  7 20:58 node_modules
-rw-rw-r--   1 ccuser ccuser  305740 Feb  8 11:18 package-lock.json
-rw-rw-r--   1 ccuser ccuser    1080 Feb  8 11:18 package.json
-rw-rw-r--   1 ccuser ccuser      94 Feb  8 11:18 postcss.config.mjs
drwxrwxr-x   2 ccuser ccuser    4096 Feb  8 11:18 public


find /home/ccuser/the-50-dollar-app -maxdepth 2 -type d | grep -v node_modules | grep -v ".next" | head -20


/home/ccuser/the-50-dollar-app
/home/ccuser/the-50-dollar-app/supabase
/home/ccuser/the-50-dollar-app/supabase/migrations
/home/ccuser/the-50-dollar-app/supabase/.temp
/home/ccuser/the-50-dollar-app/public
/home/ccuser/the-50-dollar-app/scripts
/home/ccuser/the-50-dollar-app/src
/home/ccuser/the-50-dollar-app/src/app
/home/ccuser/the-50-dollar-app/src/lib
/home/ccuser/the-50-dollar-app/src/components
/home/ccuser/the-50-dollar-app/.git
/home/ccuser/the-50-dollar-app/.git/branches
/home/ccuser/the-50-dollar-app/.git/objects
/home/ccuser/the-50-dollar-app/.git/info
/home/ccuser/the-50-dollar-app/.git/logs
/home/ccuser/the-50-dollar-app/.git/refs
/home/ccuser/the-50-dollar-app/.git/hooks
/home/ccuser/the-50-dollar-app/docs


Now let me search in the correct src directory:


find /home/ccuser/the-50-dollar-app/src -type f \( -name "*.ts" -o -name "*.tsx" \) | sort


Found 7 files
src/app/(authenticated)/worker/signup/page.tsx
src/lib/types.ts
src/components/VoiceRecorder.tsx
src/app/api/ai/parse-profile-voice/route.ts
src/app/api/ai/transcribe-voice/route.ts
src/app/(authenticated)/worker/profile/build/page.tsx
src/app/(authenticated)/worker/signup/page.tsx.backup


Perfect! Now let me read the key files:


1→"use client";
     2→
     3→import { useState, useRef, useCallback } from "react";
     4→import { Mic, Square } from "lucide-react";
     5→
     6→interface VoiceRecorderProps {
     7→  onRecordingComplete: (blob: Blob) => void;
     8→  disabled?: boolean;
     9→}
    10→
    11→export default function VoiceRecorder({ onRecordingComplete, disabled }: VoiceRecorderProps) {
    12→  const [recording, setRecording] = useState(false);
    13→  const [error, setError] = useState<string | null>(null);
    14→  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
    15→  const chunksRef = useRef<Blob[]>([]);
    16→
    17→  const startRecording = useCallback(async () => {
    18→    setError(null);
    19→    try {
    20→      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    21→      const mediaRecorder = new MediaRecorder(stream, {
    22→        mimeType: MediaRecorder.isTypeSupported("audio/webm")
    23→          ? "audio/webm"
    24→          : "audio/mp4",
    25→      });
    26→      mediaRecorderRef.current = mediaRecorder;
    27→      chunksRef.current = [];
    28→
    29→      mediaRecorder.ondataavailable = (e) => {
    30→        if (e.data.size > 0) {
    31→          chunksRef.current.push(e.data);
    32→        }
    33→      };
    34→
    35→      mediaRecorder.onstop = () => {
    36→        const blob = new Blob(chunksRef.current, { type: mediaRecorder.mimeType });
    37→        onRecordingComplete(blob);
    38→        stream.getTracks().forEach((track) => track.stop());
    39→      };
    40→
    41→      mediaRecorder.start();
    42→      setRecording(true);
    43→    } catch {
    44→      setError("Microphone access denied. Please allow mic access and try again.");
    45→    }
    46→  }, [onRecordingComplete]);
    47→
    48→  const stopRecording = useCallback(() => {
    49→    if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") {
    50→      mediaRecorderRef.current.stop();
    51→      setRecording(false);
    52→    }
    53→  }, []);
    54→
    55→  return (
    56→

    57→      <button
    58→        type="button"
    59→        onClick={recording ? stopRecording : startRecording}
    60→        disabled={disabled}
    61→        className={`flex h-16 w-16 items-center justify-center rounded-full transition-all active:scale-95 ${
    62→          recording
    63→            ? "animate-pulse bg-red-500 text-white shadow-lg shadow-red-500/40"
    64→            : "bg-primary text-primary-foreground shadow-md hover:shadow-lg"
    65→        } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
    66→        aria-label={recording ? "Stop recording" : "Start recording"}
    67→      >
    68→        {recording ? (
    69→          <Square className="h-7 w-7" />
    70→        ) : (
    71→          <Mic className="h-7 w-7" />
    72→        )}
    73→      </button>
    74→

    75→        {recording ? "Tap to stop" : "Tap to speak"}
    76→

    77→      {error && (
    78→
{error}

    79→      )}
    80→

    81→  );
    82→}
    83→

<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 { NextRequest, NextResponse } from "next/server";
     2→import OpenAI from "openai";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     5→import { withErrorHandler, APIError, ErrorCodes } from "@/lib/api-errors";
     6→import { validateCSRFToken, isSameOrigin } from "@/lib/csrf";
     7→
     8→const openai = new OpenAI({
     9→  apiKey: process.env.OPENAI_API_KEY || "",
    10→});
    11→
    12→const AI_RATE_LIMIT = 10;
    13→const AI_WINDOW_MS = 15 * 60 * 1000;
    14→
    15→async function handler(request: Request) {
    16→  // CSRF protection: validate same-origin and token
    17→  if (!isSameOrigin(request)) {
    18→    return NextResponse.json({ error: 'Cross-origin request not allowed' }, { status: 403 });
    19→  }
    20→  const csrfValid = await validateCSRFToken(request);
    21→  if (!csrfValid) {
    22→    return NextResponse.json({ error: 'Invalid or missing CSRF token' }, { status: 403 });
    23→  }
    24→
    25→  // Auth check
    26→  const supabase = await createClient();
    27→  const { data: { user } } = await supabase.auth.getUser();
    28→  if (!user) {
    29→    throw new APIError("Unauthorized", 401, ErrorCodes.UNAUTHORIZED);
    30→  }
    31→
    32→  // Rate limit
    33→  const clientIP = getClientIP(request);
    34→  const rateLimit = checkRateLimit(`transcribe:${clientIP}`, AI_RATE_LIMIT, AI_WINDOW_MS);
    35→  if (!rateLimit.allowed) {
    36→    const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    37→    return createRateLimitResponse(clientIP, retryAfter);
    38→  }
    39→
    40→  const formData = await request.formData();
    41→  const audioFile = formData.get("audio") as File | null;
    42→
    43→  if (!audioFile) {
    44→    throw new APIError("No audio file provided", 400, ErrorCodes.INVALID_INPUT);
    45→  }
    46→
    47→  try {
    48→    const transcription = await openai.audio.transcriptions.create({
    49→      file: audioFile,
    50→      model: "whisper-1",
    51→      language: "en",
    52→    });
    53→
    54→    return Response.json({ text: transcription.text });
    55→  } catch (error) {
    56→    console.error("OpenAI transcription error:", error);
    57→    throw new APIError(
    58→      "Failed to transcribe audio",
    59→      500,
    60→      ErrorCodes.EXTERNAL_SERVICE_ERROR
    61→    );
    62→  }
    63→}
    64→
    65→export const POST = withErrorHandler(handler);
    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→import { NextRequest, NextResponse } from "next/server";
     2→import OpenAI from "openai";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     5→import { withErrorHandler, APIError, ErrorCodes } from "@/lib/api-errors";
     6→import { sanitizeInput } from "@/lib/sanitize";
     7→import { validateCSRFToken, isSameOrigin } from "@/lib/csrf";
     8→
     9→// Cast helper to use NextRequest methods while satisfying withErrorHandler signature
    10→type Handler = (request: Request) => Promise<Response>;
    11→
    12→const openai = new OpenAI({
    13→  apiKey: process.env.OPENAI_API_KEY || "",
    14→});
    15→
    16→const AI_RATE_LIMIT = 10;
    17→const AI_WINDOW_MS = 15 * 60 * 1000;
    18→
    19→const VALID_TRADES = [
    20→  "Labourer", "Steel Fixer", "Formworker", "Carpenter", "Electrician",
    21→  "Plumber", "Concreter", "Bricklayer", "Painter", "Roofer",
    22→  "Scaffolder", "Tiler", "Plasterer", "Welder", "Crane Operator",
    23→  "Excavator Operator",
    24→];
    25→
    26→const VALID_AVAILABILITY = ["full-time", "casual", "weekends", "flexible"];
    27→
    28→async function handler(request: Request) {
    29→  const nextRequest = request as NextRequest;
    30→  // CSRF protection: validate same-origin and token
    31→  if (!isSameOrigin(request)) {
    32→    return NextResponse.json({ error: 'Cross-origin request not allowed' }, { status: 403 });
    33→  }
    34→  const csrfValid = await validateCSRFToken(request);
    35→  if (!csrfValid) {
    36→    return NextResponse.json({ error: 'Invalid or missing CSRF token' }, { status: 403 });
    37→  }
    38→
    39→  // Auth check
    40→  const supabase = await createClient();
    41→  const { data: { user } } = await supabase.auth.getUser();
    42→  if (!user) {
    43→    throw new APIError("Unauthorized", 401, ErrorCodes.UNAUTHORIZED);
    44→  }
    45→
    46→  // Rate limit
    47→  const clientIP = getClientIP(request);
    48→  const rateLimit = checkRateLimit(`parse-voice:${clientIP}`, AI_RATE_LIMIT, AI_WINDOW_MS);
    49→  if (!rateLimit.allowed) {
    50→    const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    51→    return createRateLimitResponse(clientIP, retryAfter);
    52→  }
    53→
    54→  const body = await request.json();
    55→  const { transcript } = body;
    56→
    57→  if (!transcript || typeof transcript !== "string") {
    58→    throw new APIError("Missing transcript", 400, ErrorCodes.INVALID_INPUT);
    59→  }
    60→
    61→  // Sanitize transcript to prevent prompt injection
    62→  const sanitizedTranscript = sanitizeInput(transcript, { maxLength: 5000 });
    63→
    64→  try {
    65→    const completion = await openai.chat.completions.create({
    66→      model: "gpt-4o-mini",
    67→      messages: [
    68→        {
    69→          role: "system",
    70→          content: `You are a profile parser for an Australian construction hiring app. Extract structured data from a worker's spoken description.
    71→
    72→Valid trades: ${VALID_TRADES.join(", ")}
    73→Valid availability: ${VALID_AVAILABILITY.join(", ")}
    74→
    75→Return ONLY valid JSON in this exact format:
    76→{
    77→  "trade": "one of the valid trades or null",
    78→  "experience_years": number or null,
    79→  "location": "suburb/area string or null",
    80→  "availability": "one of the valid availability options or null"
    81→}
    82→
    83→Match trades loosely — e.g. "sparky" = "Electrician", "chippie" = "Carpenter", "bricky" = "Bricklayer".
    84→For experience, extract the number of years mentioned.
    85→For location, extract any suburb, city, or area name mentioned.
    86→For availability, match to the closest option.
    87→If a field is not mentioned, set it to null.`,
    88→        },
    89→        {
    90→          role: "user",
    91→          content: sanitizedTranscript,
    92→        },
    93→      ],
    94→      temperature: 0.3,
    95→      response_format: { type: "json_object" },
    96→    });
    97→
    98→    const content = completion.choices[0]?.message?.content;
    99→    if (!content) {
   100→      throw new APIError("No response from AI", 500, ErrorCodes.EXTERNAL_SERVICE_ERROR);
   101→    }
   102→
   103→    const parsed = JSON.parse(content);
   104→
   105→    // Validate trade
   106→    if (parsed.trade && !VALID_TRADES.includes(parsed.trade)) {
   107→      parsed.trade = null;
   108→    }
   109→
   110→    // Validate availability
   111→    if (parsed.availability && !VALID_AVAILABILITY.includes(parsed.availability)) {
   112→      parsed.availability = null;
   113→    }
   114→
   115→    // Validate experience_years
   116→    if (parsed.experience_years !== null && (typeof parsed.experience_years !== "number" || parsed.experience_years < 0)) {
   117→      parsed.experience_years = null;
   118→    }
   119→
   120→    return Response.json(parsed);
   121→  } catch (error) {
   122→    console.error("OpenAI parsing error:", error);
   123→    if (error instanceof APIError) {
   124→      throw error;
   125→    }
   126→    throw new APIError(
   127→      "Failed to parse voice input",
   128→      500,
   129→      ErrorCodes.EXTERNAL_SERVICE_ERROR
   130→    );
   131→  }
   132→}
   133→
   134→export const POST = withErrorHandler(handler);
   135→

<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→export type UserType = "worker" | "contractor";
     2→
     3→export type Availability = "full-time" | "casual" | "weekends" | "flexible";
     4→
     5→export type RateType = "hourly" | "daily";
     6→
     7→export type JobStatus = "draft" | "active" | "filled" | "closed";
     8→
     9→export type MatchStatus =
    10→  | "suggested"
    11→  | "applied"
    12→  | "accepted"
    13→  | "rejected"
    14→  | "hired";
    15→
    16→export type ActionStatus = "pending" | "accepted" | "rejected";
    17→
    18→export type PaymentStatus = "pending" | "charged" | "refunded";
    19→
    20→export type NotificationType = "match" | "hire_request" | "hire_confirmed" | "rating" | "job_posted" | "system";
    21→
    22→export interface Profile {
    23→  id: string;
    24→  type: UserType;
    25→  name: string | null;
    26→  phone: string | null;
    27→  email: string | null;
    28→  avatar_url: string | null;
    29→  /**
    30→   * Location as { lat, lng } object
    31→   * Note: Database stores this as PostgreSQL point type
    32→   * Use postgresPointToJson() when reading from DB
    33→   * Use jsonToPostgresPoint() when writing to DB
    34→   */
    35→  location: { lat: number; lng: number } | null;
    36→  suburb: string | null;
    37→  verified: boolean;
    38→  onboarding_completed: boolean;
    39→  created_at: string;
    40→  updated_at: string;
    41→  ai_generated: boolean;
    42→}
    43→
    44→export interface Company {
    45→  id: string;
    46→  profile_id: string;
    47→  name: string;
    48→  abn: string | null;
    49→  address: string | null;
    50→  industry: string | null;
    51→  size: string | null;
    52→  website: string | null;
    53→  logo_url: string | null;
    54→  ai_research: Record<string, unknown> | null;
    55→  created_at: string;
    56→  updated_at: string;
    57→}
    58→
    59→export interface WorkerProfile {
    60→  id: string;
    61→  profile_id: string;
    62→  trades: string[];
    63→  experience_years: number | null;
    64→  certifications: string[];
    65→  white_card_number: string | null;
    66→  white_card_verified: boolean;
    67→  availability: Availability | null;
    68→  languages: string[];
    69→  bio: string | null;
    70→  voice_note_url: string | null;
    71→  ai_extracted: Record<string, unknown> | null;
    72→  created_at: string;
    73→  updated_at: string;
    74→}
    75→
    76→export interface Job {
    77→  id: string;
    78→  company_id: string;
    79→  title: string;
    80→  description: string | null;
    81→  trade: string;
    82→  /**
    83→   * Location as { lat, lng } object
    84→   * Note: Database stores this as PostgreSQL point type
    85→   * Use postgresPointToJson() when reading from DB
    86→   * Use jsonToPostgresPoint() when writing to DB
    87→   */
    88→  location: { lat: number; lng: number } | null;
    89→  suburb: string | null;
    90→  site_address: string | null;
    91→  rate_min: number | null;
    92→  rate_max: number | null;
    93→  rate_type: RateType;
    94→  start_date: string | null;
    95→  duration: string | null;
    96→  workers_needed: number;
    97→  certifications_required: string[];
    98→  status: JobStatus;
    99→  ai_generated: boolean;
   100→  created_at: string;
   101→  updated_at: string;
   102→}
   103→
   104→export interface Match {
   105→  id: string;
   106→  job_id: string;
   107→  worker_id: string;
   108→  status: MatchStatus;
   109→  match_score: number | null;
   110→  ai_summary: string | null;
   111→  contractor_action: ActionStatus;
   112→  worker_action: ActionStatus;
   113→  hired_at: string | null;
   114→  fee_charged: boolean;
   115→  created_at: string;
   116→  updated_at: string;
   117→}
   118→
   119→export interface Rating {
   120→  id: string;
   121→  match_id: string;
   122→  from_id: string;
   123→  to_id: string;
   124→  score: number;
   125→  comment: string | null;
   126→  created_at: string;
   127→}
   128→
   129→export interface Payment {
   130→  id: string;
   131→  match_id: string;
   132→  company_id: string;
   133→  amount: number;
   134→  status: PaymentStatus;
   135→  stripe_payment_id: string | null;
   136→  created_at: string;
   137→}
   138→
   139→export interface Notification {
   140→  id: string;
   141→  profile_id: string;
   142→  type: NotificationType;
   143→  title: string;
   144→  body: string | null;
   145→  data: Record<string, unknown> | null;
   146→  read: boolean;
   147→  created_at: string;
   148→}
   149→

<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 let me check where these voice components are actually used:


1→"use client";
     2→
     3→import { useState, useEffect, useCallback } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import toast from "react-hot-toast";
     7→import { createClient } from "@/lib/supabase/client";
     8→import { Button } from "@/components/ui/button";
     9→import { Input } from "@/components/ui/input";
    10→import { Label } from "@/components/ui/label";
    11→import { Card, CardContent } from "@/components/ui/card";
    12→import VoiceRecorder from "@/components/VoiceRecorder";
    13→import {
    14→  HardHat,
    15→  ArrowLeft,
    16→  ArrowRight,
    17→  Loader2,
    18→  CheckCircle2,
    19→  Sparkles,
    20→  MapPin,
    21→  Clock,
    22→  Shield,
    23→  Edit3,
    24→  Wrench,
    25→  Calendar,
    26→  Zap,
    27→  Mic,
    28→} from "lucide-react";
    29→
    30→type Step = "trade" | "experience" | "location" | "availability" | "generating" | "review";
    31→
    32→const TRADES = [
    33→  "Labourer",
    34→  "Steel Fixer",
    35→  "Formworker",
    36→  "Carpenter",
    37→  "Electrician",
    38→  "Plumber",
    39→  "Concreter",
    40→  "Bricklayer",
    41→  "Painter",
    42→  "Roofer",
    43→  "Scaffolder",
    44→  "Tiler",
    45→  "Plasterer",
    46→  "Welder",
    47→  "Crane Operator",
    48→  "Excavator Operator",
    49→];
    50→
    51→const EXPERIENCE_OPTIONS = [
    52→  { label: "Less than 1 year", value: 0 },
    53→  { label: "1-3 years", value: 2 },
    54→  { label: "3-5 years", value: 4 },
    55→  { label: "5-10 years", value: 7 },
    56→  { label: "10+ years", value: 12 },
    57→];
    58→
    59→const AVAILABILITY_OPTIONS = [
    60→  { label: "Full-time", value: "full-time" as const, desc: "Mon-Fri, regular hours" },
    61→  { label: "Casual", value: "casual" as const, desc: "As needed, flexible" },
    62→  { label: "Weekends", value: "weekends" as const, desc: "Saturday & Sunday" },
    63→  { label: "Flexible", value: "flexible" as const, desc: "Any time, any days" },
    64→];
    65→
    66→interface ProfileData {
    67→  trade: string;
    68→  experience: number;
    69→  location: string;
    70→  availability: "full-time" | "casual" | "weekends" | "flexible";
    71→}
    72→
    73→interface GeneratedProfile {
    74→  bio: string;
    75→  skills: string[];
    76→  certifications: string[];
    77→}
    78→
    79→function closestExperience(years: number): number {
    80→  const values = EXPERIENCE_OPTIONS.map((o) => o.value);
    81→  return values.reduce((prev, curr) =>
    82→    Math.abs(curr - years) < Math.abs(prev - years) ? curr : prev
    83→  );
    84→}
    85→
    86→export default function ProfileBuildPage() {
    87→  const [step, setStep] = useState<Step>("trade");
    88→  const [loading, setLoading] = useState(true);
    89→  const [generating, setGenerating] = useState(false);
    90→  const [saving, setSaving] = useState(false);
    91→
    92→  // Form data
    93→  const [trade, setTrade] = useState<string>("");
    94→  const [experience, setExperience] = useState<number | null>(null);
    95→  const [location, setLocation] = useState("");
    96→  const [availability, setAvailability] = useState<ProfileData["availability"] | null>(null);
    97→
    98→  // Generated data
    99→  const [generatedBio, setGeneratedBio] = useState("");
   100→  const [generatedSkills, setGeneratedSkills] = useState<string[]>([]);
   101→  const [generatedCerts, setGeneratedCerts] = useState<string[]>([]);
   102→
   103→  // Editable states
   104→  const [editedBio, setEditedBio] = useState("");
   105→  const [editedSkills, setEditedSkills] = useState<string[]>([]);
   106→  const [editedCerts, setEditedCerts] = useState<string[]>([]);
   107→
   108→  // Voice flow
   109→  const [showVoice, setShowVoice] = useState(false);
   110→  const [voiceProcessing, setVoiceProcessing] = useState(false);
   111→  const [voiceStatus, setVoiceStatus] = useState("");
   112→  const [voiceError, setVoiceError] = useState<string | null>(null);
   113→
   114→  const router = useRouter();
   115→  const supabase = createClient();
   116→
   117→  useEffect(() => {
   118→    async function checkAuth() {
   119→      const {
   120→        data: { user },
   121→      } = await supabase.auth.getUser();
   122→      if (!user) {
   123→        router.push("/auth/login");
   124→        return;
   125→      }
   126→      setLoading(false);
   127→    }
   128→    checkAuth();
   129→  }, [supabase.auth, router]);
   130→
   131→  async function generateProfile() {
   132→    if (!trade || experience === null || !location || !availability) return;
   133→
   134→    setGenerating(true);
   135→    setStep("generating");
   136→
   137→    try {
   138→      const response = await fetch("/api/ai/generate-profile", {
   139→        method: "POST",
   140→        headers: { "Content-Type": "application/json" },
   141→        body: JSON.stringify({
   142→          trade,
   143→          experience,
   144→          location,
   145→          availability,
   146→        }),
   147→      });
   148→
   149→      if (!response.ok) {
   150→        throw new Error("Failed to generate profile");
   151→      }
   152→
   153→      const data: GeneratedProfile = await response.json();
   154→
   155→      setGeneratedBio(data.bio);
   156→      setGeneratedSkills(data.skills);
   157→      setGeneratedCerts(data.certifications);
   158→
   159→      // Initialize editable versions
   160→      setEditedBio(data.bio);
   161→      setEditedSkills(data.skills);
   162→      setEditedCerts(data.certifications);
   163→
   164→      setStep("review");
   165→    } catch (error) {
   166→      console.error("Generation error:", error);
   167→      toast.error("Failed to generate profile. Please try again.");
   168→      setStep("availability");
   169→    } finally {
   170→      setGenerating(false);
   171→    }
   172→  }
   173→
   174→  const handleVoiceComplete = useCallback(async (blob: Blob) => {
   175→    setVoiceProcessing(true);
   176→    setVoiceError(null);
   177→    setVoiceStatus("Transcribing your voice...");
   178→
   179→    try {
   180→      // Step 1: Transcribe
   181→      const formData = new FormData();
   182→      formData.append("audio", blob, "recording.webm");
   183→
   184→      const transcribeRes = await fetch("/api/ai/transcribe-voice", {
   185→        method: "POST",
   186→        body: formData,
   187→      });
   188→
   189→      if (!transcribeRes.ok) {
   190→        throw new Error("Failed to transcribe audio");
   191→      }
   192→
   193→      const { text } = await transcribeRes.json();
   194→      if (!text) throw new Error("No speech detected. Please try again.");
   195→
   196→      setVoiceStatus("Understanding your details...");
   197→
   198→      // Step 2: Parse profile fields
   199→      const parseRes = await fetch("/api/ai/parse-profile-voice", {
   200→        method: "POST",
   201→        headers: { "Content-Type": "application/json" },
   202→        body: JSON.stringify({ transcript: text }),
   203→      });
   204→
   205→      if (!parseRes.ok) {
   206→        throw new Error("Failed to parse your details");
   207→      }
   208→
   209→      const parsed = await parseRes.json();
   210→
   211→      // Pre-fill fields
   212→      if (parsed.trade) setTrade(parsed.trade);
   213→      if (parsed.experience_years !== null && parsed.experience_years !== undefined) {
   214→        setExperience(closestExperience(parsed.experience_years));
   215→      }
   216→      if (parsed.location) setLocation(parsed.location);
   217→      if (parsed.availability) setAvailability(parsed.availability);
   218→
   219→      // Check what we got and move to the right step
   220→      setShowVoice(false);
   221→      setVoiceStatus("");
   222→
   223→      const hasTrade = !!parsed.trade;
   224→      const hasExp = parsed.experience_years !== null && parsed.experience_years !== undefined;
   225→      const hasLoc = !!parsed.location;
   226→      const hasAvail = !!parsed.availability;
   227→
   228→      if (hasTrade && hasExp && hasLoc && hasAvail) {
   229→        // All fields filled — go straight to generating
   230→        // Need to call generateProfile after state updates, so use a timeout
   231→        setTimeout(() => {
   232→          setStep("generating");
   233→        }, 0);
   234→      } else if (!hasTrade) {
   235→        setStep("trade");
   236→      } else if (!hasExp) {
   237→        setStep("experience");
   238→      } else if (!hasLoc) {
   239→        setStep("location");
   240→      } else {
   241→        setStep("availability");
   242→      }
   243→    } catch (error) {
   244→      console.error("Voice processing error:", error);
   245→      setVoiceError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
   246→      setVoiceStatus("");
   247→    } finally {
   248→      setVoiceProcessing(false);
   249→    }
   250→  }, []);
   251→
   252→  // When step changes to "generating" from voice flow, trigger profile generation
   253→  useEffect(() => {
   254→    if (step === "generating" && !generating && trade && experience !== null && location && availability) {
   255→      generateProfile();
   256→    }
   257→    // eslint-disable-next-line react-hooks/exhaustive-deps
   258→  }, [step]);
   259→
   260→  async function handleSave() {
   261→    setSaving(true);
   262→
   263→    try {
   264→      const {
   265→        data: { user },
   266→      } = await supabase.auth.getUser();
   267→      if (!user) throw new Error("Not authenticated");
   268→
   269→      // Update or create worker profile
   270→      const { data: existingProfile } = await supabase
   271→        .from("worker_profiles")
   272→        .select("id")
   273→        .eq("profile_id", user.id)
   274→        .single();
   275→
   276→      const profileData = {
   277→        profile_id: user.id,
   278→        trades: [trade],
   279→        experience_years: experience,
   280→        availability,
   281→        certifications: editedCerts,
   282→        bio: editedBio,
   283→        ai_extracted: {
   284→          skills: editedSkills,
   285→          generated_at: new Date().toISOString(),
   286→          original_bio: generatedBio,
   287→          original_skills: generatedSkills,
   288→          original_certs: generatedCerts,
   289→        },
   290→      };
   291→
   292→      if (existingProfile) {
   293→        await supabase
   294→          .from("worker_profiles")
   295→          .update(profileData)
   296→          .eq("profile_id", user.id);
   297→      } else {
   298→        await supabase.from("worker_profiles").insert(profileData);
   299→      }
   300→
   301→      // Update main profile
   302→      await supabase
   303→        .from("profiles")
   304→        .update({
   305→          suburb: location,
   306→          onboarding_completed: true,
   307→        })
   308→        .eq("id", user.id);
   309→
   310→      router.push("/dashboard");
   311→      router.refresh();
   312→    } catch (error) {
   313→      console.error("Save error:", error);
   314→      toast.error("Failed to save profile. Please try again.");
   315→    } finally {
   316→      setSaving(false);
   317→    }
   318→  }
   319→
   320→  const stepOrder: Step[] = ["trade", "experience", "location", "availability", "generating", "review"];
   321→  const currentIndex = stepOrder.indexOf(step);
   322→  const totalSteps = 4; // Only count user-facing steps
   323→
   324→  if (loading) {
   325→    return (
   326→

   327→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
   328→

   329→    );
   330→  }
   331→
   332→  return (
   333→

   334→
   335→        {/* Progress bar */}
   336→        {step !== "generating" && step !== "review" && (
   337→

   338→            {Array.from({ length: totalSteps }, (_, i) => (
   339→              <div
   340→                key={i}
   341→                className={`h-1.5 flex-1 rounded-full transition-colors ${
   342→                  i <= currentIndex ? "bg-primary" : "bg-muted"
   343→                }`}
   344→              />
   345→            ))}
   346→

   347→        )}
   348→
   349→        {/* Step: Trade */}
   350→        {step === "trade" && (
   351→

   352→            <StepHeader
   353→              icon={<Wrench className="h-5 w-5" />}
   354→              title="What's your trade?"
   355→              subtitle="Pick your primary trade"
   356→            />
   357→
   358→            {/* Voice option */}
   359→            {!showVoice && (
   360→              <button
   361→                onClick={() => setShowVoice(true)}
   362→                className="mt-6 flex w-full items-center justify-center gap-3 rounded-xl border-2 border-dashed border-primary/40 bg-primary/5 px-5 py-4 text-base font-medium text-primary transition-colors hover:border-primary hover:bg-primary/10 active:scale-95"
   363→                style={{ minHeight: "64px" }}
   364→              >
   365→                <Mic className="h-6 w-6" />
   366→                Or tell us with your voice
   367→              </button>
   368→            )}
   369→
   370→            {showVoice && (
   371→              <Card className="mt-6">
   372→                <CardContent className="flex flex-col items-center gap-4 pt-6">
   373→

   374→                    Say something like: “I'm a chippie with 8 years experience in Parramatta, looking for full-time work”
   375→

   376→
   377→                  {voiceProcessing ? (
   378→

   379→                      <Loader2 className="h-10 w-10 animate-spin text-primary" />
   380→
{voiceStatus}

   381→

   382→                  ) : (
   383→                    <VoiceRecorder onRecordingComplete={handleVoiceComplete} />
   384→                  )}
   385→
   386→                  {voiceError && (
   387→
{voiceError}

   388→                  )}
   389→
   390→                  <button
   391→                    onClick={() => {
   392→                      setShowVoice(false);
   393→                      setVoiceError(null);
   394→                    }}
   395→                    className="text-sm text-muted-foreground hover:text-foreground"
   396→                  >
   397→                    Cancel — I'll tap instead
   398→                  </button>
   399→                </CardContent>
   400→              </Card>
   401→            )}
   402→
   403→

   404→              {TRADES.map((t) => (
   405→                <button
   406→                  key={t}
   407→                  onClick={() => {
   408→                    setTrade(t);
   409→                    setStep("experience");
   410→                  }}
   411→                  className={`rounded-xl border-2 px-4 py-4 text-left text-base font-medium transition-colors active:scale-95 ${
   412→                    trade === t
   413→                      ? "border-primary bg-primary/10 text-primary"
   414→                      : "border-border hover:border-primary/50"
   415→                  }`}
   416→                  style={{ minHeight: "64px" }}
   417→                >
   418→                  {t}
   419→                </button>
   420→              ))}
   421→

   422→

   423→        )}
   424→
   425→        {/* Step: Experience */}
   426→        {step === "experience" && (
   427→

   428→            <StepHeader
   429→              icon={<Clock className="h-5 w-5" />}
   430→              title="Your experience?"
   431→              subtitle="Years in construction"
   432→            />
   433→
   434→

   435→              {EXPERIENCE_OPTIONS.map((opt) => (
   436→                <button
   437→                  key={opt.value}
   438→                  onClick={() => {
   439→                    setExperience(opt.value);
   440→                    setStep("location");
   441→                  }}
   442→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 text-left text-base font-medium transition-colors active:scale-95 ${
   443→                    experience === opt.value
   444→                      ? "border-primary bg-primary/10 text-primary"
   445→                      : "border-border hover:border-primary/50"
   446→                  }`}
   447→                  style={{ minHeight: "64px" }}
   448→                >
   449→                  {opt.label}
   450→                  <ArrowRight className="h-5 w-5 text-muted-foreground" />
   451→                </button>
   452→              ))}
   453→

   454→
   455→            <button
   456→              onClick={() => setStep("trade")}
   457→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   458→            >
   459→              <ArrowLeft className="h-4 w-4" />
   460→              Back
   461→            </button>
   462→

   463→        )}
   464→
   465→        {/* Step: Location */}
   466→        {step === "location" && (
   467→

   468→            <StepHeader
   469→              icon={<MapPin className="h-5 w-5" />}
   470→              title="Where are you based?"
   471→              subtitle="Your suburb or area"
   472→            />
   473→
   474→            <form
   475→              onSubmit={(e) => {
   476→                e.preventDefault();
   477→                if (location.trim()) setStep("availability");
   478→              }}
   479→              className="mt-6 space-y-4"
   480→            >
   481→

   482→                Suburb or Area
   483→                <Input
   484→                  id="location"
   485→                  placeholder="e.g. Parramatta, Blacktown"
   486→                  value={location}
   487→                  onChange={(e) => setLocation(e.target.value)}
   488→                  required
   489→                  autoFocus
   490→                  className="h-14 text-base"
   491→                />
   492→

   493→

   494→                We'll match you to nearby jobs
   495→

   496→
   497→

   498→                <Button
   499→                  type="button"
   500→                  variant="outline"
   501→                  onClick={() => setStep("experience")}
   502→                  className="h-14 w-14 p-0"
   503→                >
   504→                  <ArrowLeft className="h-5 w-5" />
   505→                </Button>
   506→                <Button
   507→                  type="submit"
   508→                  disabled={!location.trim()}
   509→                  className="h-14 flex-1 gap-2 text-base font-semibold"
   510→                >
   511→                  Continue
   512→                  <ArrowRight className="h-5 w-5" />
   513→                </Button>
   514→

   515→            </form>
   516→

   517→        )}
   518→
   519→        {/* Step: Availability */}
   520→        {step === "availability" && (
   521→

   522→            <StepHeader
   523→              icon={<Calendar className="h-5 w-5" />}
   524→              title="Your availability?"
   525→              subtitle="When can you work?"
   526→            />
   527→
   528→

   529→              {AVAILABILITY_OPTIONS.map((opt) => (
   530→                <button
   531→                  key={opt.value}
   532→                  onClick={() => {
   533→                    setAvailability(opt.value);
   534→                    generateProfile();
   535→                  }}
   536→                  className={`flex w-full items-start justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors active:scale-95 ${
   537→                    availability === opt.value
   538→                      ? "border-primary bg-primary/10"
   539→                      : "border-border hover:border-primary/50"
   540→                  }`}
   541→                  style={{ minHeight: "64px" }}
   542→                >
   543→

   544→                    <p className={`text-base font-semibold ${availability === opt.value ? "text-primary" : ""}`}>
   545→                      {opt.label}
   546→

   547→
{opt.desc}

   548→

   549→                  <Zap className="h-5 w-5 shrink-0 text-primary" />
   550→                </button>
   551→              ))}
   552→

   553→
   554→            <button
   555→              onClick={() => setStep("location")}
   556→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   557→            >
   558→              <ArrowLeft className="h-4 w-4" />
   559→              Back
   560→            </button>
   561→

   562→        )}
   563→
   564→        {/* Step: Generating */}
   565→        {step === "generating" && (
   566→

   567→

   568→              <Sparkles className="h-10 w-10 animate-pulse text-primary" />
   569→

   570→
Building your profile...

   571→

   572→              AI is generating your bio, skills, and certifications
   573→

   574→

   575→

   576→

   577→

   578→

   579→

   580→        )}
   581→
   582→        {/* Step: Review */}
   583→        {step === "review" && (
   584→

   585→

   586→

   587→                <Sparkles className="h-5 w-5" />
   588→

   589→

   590→
Review & Edit

   591→

   592→                  AI generated this — customise as needed
   593→

   594→

   595→

   596→
   597→            {/* Your Answers Summary */}
   598→            <Card className="mt-6">
   599→              <CardContent className="space-y-3 pt-6">
   600→

   601→

   602→                    <Wrench className="h-4 w-4 text-muted-foreground" />
   603→                    {trade}
   604→

   605→                  <button
   606→                    onClick={() => setStep("trade")}
   607→                    className="text-xs text-primary hover:underline"
   608→                  >
   609→                    Change
   610→                  </button>
   611→

   612→

   613→

   614→                    <Clock className="h-4 w-4 text-muted-foreground" />
   615→
   616→                      {EXPERIENCE_OPTIONS.find((o) => o.value === experience)?.label}
   617→
   618→

   619→                  <button
   620→                    onClick={() => setStep("experience")}
   621→                    className="text-xs text-primary hover:underline"
   622→                  >
   623→                    Change
   624→                  </button>
   625→

   626→

   627→

   628→                    <MapPin className="h-4 w-4 text-muted-foreground" />
   629→                    {location}
   630→

   631→                  <button
   632→                    onClick={() => setStep("location")}
   633→                    className="text-xs text-primary hover:underline"
   634→                  >
   635→                    Change
   636→                  </button>
   637→

   638→              </CardContent>
   639→            </Card>
   640→
   641→            {/* Bio */}
   642→

   643→

   644→
   645→                  <Sparkles className="h-4 w-4 text-primary" />
   646→                  Your Bio
   647→
   648→                AI-generated
   649→

   650→              <textarea
   651→                value={editedBio}
   652→                onChange={(e) => setEditedBio(e.target.value)}
   653→                className="min-h-[120px] w-full rounded-xl border-2 border-border bg-background px-4 py-3 text-base focus:border-primary focus:outline-none"
   654→                placeholder="Your professional bio..."
   655→              />
   656→

   657→
   658→            {/* Skills */}
   659→

   660→

   661→
   662→                  <Zap className="h-4 w-4 text-primary" />
   663→                  Skills
   664→
   665→                Tap to remove
   666→

   667→

   668→                {editedSkills.map((skill, idx) => (
   669→                  <button
   670→                    key={idx}
   671→                    onClick={() => setEditedSkills(editedSkills.filter((_, i) => i !== idx))}
   672→                    className="rounded-full border-2 border-primary bg-primary/10 px-4 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/20 active:scale-95"
   673→                  >
   674→                    {skill} ×
   675→                  </button>
   676→                ))}
   677→

   678→

   679→
   680→            {/* Certifications */}
   681→

   682→

   683→
   684→                  <Shield className="h-4 w-4 text-primary" />
   685→                  Certifications
   686→
   687→                Tap to remove
   688→

   689→

   690→                {editedCerts.map((cert, idx) => (
   691→                  <button
   692→                    key={idx}
   693→                    onClick={() => setEditedCerts(editedCerts.filter((_, i) => i !== idx))}
   694→                    className="flex w-full items-center justify-between rounded-xl border-2 border-border bg-background px-4 py-3 text-left text-sm font-medium transition-colors hover:border-destructive hover:bg-destructive/5 active:scale-95"
   695→                  >
   696→                    {cert}
   697→                    Remove
   698→                  </button>
   699→                ))}
   700→

   701→

   702→

   703→        )}
   704→
   705→
   706→      {/* Bottom sticky CTA - only show on review step */}
   707→      {step === "review" && (
   708→

   709→

   710→            <Button
   711→              onClick={handleSave}
   712→              className="h-16 w-full gap-2 text-lg font-semibold"
   713→              disabled={saving}
   714→            >
   715→              {saving ? (
   716→                <>
   717→                  <Loader2 className="h-6 w-6 animate-spin" />
   718→                  Saving profile...
   719→                </>
   720→              ) : (
   721→                <>
   722→                  <CheckCircle2 className="h-6 w-6" />
   723→                  Save My Profile
   724→                </>
   725→              )}
   726→            </Button>
   727→

   728→

   729→      )}
   730→

   731→  );
   732→}
   733→
   734→function StepHeader({
   735→  icon,
   736→  title,
   737→  subtitle,
   738→}: {
   739→  icon: React.ReactNode;
   740→  title: string;
   741→  subtitle: string;
   742→}) {
   743→  return (
   744→

   745→

   746→        {icon}
   747→

   748→

   749→
{title}

   750→
{subtitle}

   751→

   752→

   753→  );
   754→}
   755→

<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→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import Image from "next/image";
     7→import toast from "react-hot-toast";
     8→import { createClient } from "@/lib/supabase/client";
     9→import { Button } from "@/components/ui/button";
    10→import { Input } from "@/components/ui/input";
    11→import { Label } from "@/components/ui/label";
    12→import { Card, CardContent } from "@/components/ui/card";
    13→import {
    14→  HardHat,
    15→  Keyboard,
    16→  Mic,
    17→  ArrowLeft,
    18→  ArrowRight,
    19→  Loader2,
    20→  CheckCircle2,
    21→  Sparkles,
    22→  MapPin,
    23→  Clock,
    24→  Shield,
    25→  Edit3,
    26→  Wrench,
    27→  Calendar,
    28→  Globe2,
    29→} from "lucide-react";
    30→
    31→type Step = "auth" | "method" | "trade" | "experience" | "location" | "availability" | "certs" | "review";
    32→
    33→const TRADES = [
    34→  "Labourer",
    35→  "Steel Fixer",
    36→  "Formworker",
    37→  "Carpenter",
    38→  "Electrician",
    39→  "Plumber",
    40→  "Concreter",
    41→  "Bricklayer",
    42→  "Painter",
    43→  "Roofer",
    44→  "Scaffolder",
    45→  "Tiler",
    46→  "Plasterer",
    47→  "Welder",
    48→  "Crane Operator",
    49→  "Excavator Operator",
    50→];
    51→
    52→const EXPERIENCE_OPTIONS = [
    53→  { label: "Less than 1 year", value: 0 },
    54→  { label: "1-3 years", value: 2 },
    55→  { label: "3-5 years", value: 4 },
    56→  { label: "5-10 years", value: 7 },
    57→  { label: "10+ years", value: 12 },
    58→];
    59→
    60→const AVAILABILITY_OPTIONS = [
    61→  { label: "Full-time", value: "full-time" as const, desc: "Mon-Fri, regular hours" },
    62→  { label: "Casual", value: "casual" as const, desc: "As needed, flexible" },
    63→  { label: "Weekends", value: "weekends" as const, desc: "Saturday & Sunday" },
    64→  { label: "Flexible", value: "flexible" as const, desc: "Any time, any days" },
    65→];
    66→
    67→const CERT_OPTIONS = [
    68→  "White Card",
    69→  "Forklift Licence",
    70→  "EWP (Elevated Work Platform)",
    71→  "Working at Heights",
    72→  "First Aid",
    73→  "Traffic Control",
    74→  "Rigging",
    75→  "Dogging",
    76→  "Confined Spaces",
    77→];
    78→
    79→interface WorkerData {
    80→  trades: string[];
    81→  experience: number;
    82→  suburb: string;
    83→  availability: "full-time" | "casual" | "weekends" | "flexible";
    84→  certifications: string[];
    85→}
    86→
    87→function generateBio(data: WorkerData): string {
    88→  const tradeStr = data.trades.join(" and ");
    89→  const expStr =
    90→    data.experience === 0
    91→      ? "under 1 year"
    92→      : data.experience >= 12
    93→        ? "10+ years"
    94→        : `${data.experience} years`;
    95→  const certStr =
    96→    data.certifications.length > 0
    97→      ? ` Holds ${data.certifications.join(", ")}.`
    98→      : "";
    99→
   100→  return `Experienced ${tradeStr.toLowerCase()} with ${expStr} in the construction industry. Based in ${data.suburb}, available for ${data.availability} work.${certStr}`;
   101→}
   102→
   103→export default function WorkerSignupPage() {
   104→  const [step, setStep] = useState<Step>("auth");
   105→  const [isAuthenticated, setIsAuthenticated] = useState(false);
   106→  const [loading, setLoading] = useState(true);
   107→
   108→  // Auth state
   109→  const [email, setEmail] = useState("");
   110→  const [password, setPassword] = useState("");
   111→  const [authError, setAuthError] = useState<string | null>(null);
   112→  const [authLoading, setAuthLoading] = useState(false);
   113→
   114→  // Worker data
   115→  const [selectedTrades, setSelectedTrades] = useState<string[]>([]);
   116→  const [experience, setExperience] = useState<number | null>(null);
   117→  const [suburb, setSuburb] = useState("");
   118→  const [availability, setAvailability] = useState<WorkerData["availability"] | null>(null);
   119→  const [certifications, setCertifications] = useState<string[]>([]);
   120→
   121→  const [saving, setSaving] = useState(false);
   122→
   123→  const router = useRouter();
   124→  const supabase = createClient();
   125→
   126→  useEffect(() => {
   127→    async function checkAuth() {
   128→      const {
   129→        data: { user },
   130→      } = await supabase.auth.getUser();
   131→      if (user) {
   132→        setIsAuthenticated(true);
   133→        setStep("method");
   134→      }
   135→      setLoading(false);
   136→    }
   137→    checkAuth();
   138→  }, [supabase.auth]);
   139→
   140→  async function handleAuth(e: React.FormEvent) {
   141→    e.preventDefault();
   142→    setAuthLoading(true);
   143→    setAuthError(null);
   144→
   145→    const { error } = await supabase.auth.signUp({
   146→      email,
   147→      password,
   148→      options: {
   149→        data: { type: "worker" },
   150→        emailRedirectTo: `${window.location.origin}/auth/callback`,
   151→      },
   152→    });
   153→
   154→    if (error) {
   155→      if (error.message.includes("already registered")) {
   156→        const { error: loginError } = await supabase.auth.signInWithPassword({
   157→          email,
   158→          password,
   159→        });
   160→        if (loginError) {
   161→          setAuthError(loginError.message);
   162→          setAuthLoading(false);
   163→          return;
   164→        }
   165→      } else {
   166→        setAuthError(error.message);
   167→        setAuthLoading(false);
   168→        return;
   169→      }
   170→    }
   171→
   172→    setIsAuthenticated(true);
   173→    setStep("method");
   174→    setAuthLoading(false);
   175→  }
   176→
   177→  function toggleTrade(trade: string) {
   178→    setSelectedTrades((prev) =>
   179→      prev.includes(trade) ? prev.filter((t) => t !== trade) : [...prev, trade]
   180→    );
   181→  }
   182→
   183→  function toggleCert(cert: string) {
   184→    setCertifications((prev) =>
   185→      prev.includes(cert) ? prev.filter((c) => c !== cert) : [...prev, cert]
   186→    );
   187→  }
   188→
   189→  async function handleConfirm() {
   190→    if (!availability || experience === null) return;
   191→    setSaving(true);
   192→
   193→    try {
   194→      const {
   195→        data: { user },
   196→      } = await supabase.auth.getUser();
   197→      if (!user) {
   198→        toast.error("Not logged in. Please sign in again.");
   199→        setSaving(false);
   200→        return;
   201→      }
   202→
   203→      const workerData: WorkerData = {
   204→        trades: selectedTrades,
   205→        experience,
   206→        suburb,
   207→        availability,
   208→        certifications,
   209→      };
   210→
   211→      const bio = generateBio(workerData);
   212→
   213→      console.log("Updating profile...", { userId: user.id, suburb });
   214→      const profileResult = await supabase.from("profiles").update({
   215→        name: user.email?.split("@")[0] ?? "",
   216→        suburb,
   217→        onboarding_completed: true,
   218→      }).eq("id", user.id);
   219→
   220→      if (profileResult.error) {
   221→        console.error("Profile update failed:", profileResult.error);
   222→        toast.error(`Profile update failed: ${profileResult.error.message}`);
   223→        setSaving(false);
   224→        return;
   225→      }
   226→
   227→      console.log("Inserting worker profile...", { profile_id: user.id });
   228→      const workerResult = await supabase.from("worker_profiles").insert({
   229→        profile_id: user.id,
   230→        trades: selectedTrades,
   231→        experience_years: experience,
   232→        availability,
   233→        certifications,
   234→        bio,
   235→        ai_extracted: {
   236→          source: "typed_answers",
   237→          created_at: new Date().toISOString(),
   238→        },
   239→      });
   240→
   241→      if (workerResult.error) {
   242→        console.error("Worker profile insert failed:", workerResult.error);
   243→        toast.error(`Worker profile creation failed: ${workerResult.error.message}`);
   244→        setSaving(false);
   245→        return;
   246→      }
   247→
   248→      console.log("Profile created successfully!");
   249→      router.push("/dashboard");
   250→      router.refresh();
   251→    } catch (error: any) {
   252→      console.error("Signup error:", error);
   253→      toast.error(`Something went wrong: ${error.message || "Unknown error"}`);
   254→      setSaving(false);
   255→    }
   256→  }
   257→
   258→  const workerData: WorkerData | null =
   259→    selectedTrades.length > 0 && experience !== null && suburb && availability
   260→      ? { trades: selectedTrades, experience, suburb, availability, certifications }
   261→      : null;
   262→
   263→  const stepOrder: Step[] = ["auth", "method", "trade", "experience", "location", "availability", "certs", "review"];
   264→  const activeSteps = isAuthenticated ? stepOrder.slice(1) : stepOrder;
   265→  const currentIndex = activeSteps.indexOf(step);
   266→  const totalSteps = activeSteps.length;
   267→
   268→  if (loading) {
   269→    return (
   270→

   271→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
   272→

   273→    );
   274→  }
   275→
   276→  return (
   277→

   278→      {/* Header */}
   279→
   280→

   281→          <Link href="/" className="flex items-center gap-2">
   282→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
   283→          </Link>
   284→

   285→
   286→
   287→
   288→        {/* Progress bar */}
   289→

   290→          {Array.from({ length: totalSteps }, (_, i) => (
   291→            <div
   292→              key={i}
   293→              className={`h-1.5 flex-1 rounded-full transition-colors ${
   294→                i <= currentIndex ? "bg-primary" : "bg-muted"
   295→              }`}
   296→            />
   297→          ))}
   298→

   299→
   300→        {/* Step: Auth */}
   301→        {step === "auth" && (
   302→

   303→

   304→

   305→                <HardHat className="h-5 w-5" />
   306→

   307→

   308→
Create your account

   309→

   310→                  Always free for workers
   311→

   312→

   313→

   314→
   315→            <form onSubmit={handleAuth} className="mt-6 space-y-4">
   316→

   317→                Email
   318→                <Input
   319→                  id="email"
   320→                  type="email"
   321→                  placeholder="you@email.com"
   322→                  value={email}
   323→                  onChange={(e) => setEmail(e.target.value)}
   324→                  required
   325→                  autoComplete="email"
   326→                  className="h-12 text-base"
   327→                />
   328→

   329→

   330→                Password
   331→                <Input
   332→                  id="password"
   333→                  type="password"
   334→                  placeholder="Create a password"
   335→                  value={password}
   336→                  onChange={(e) => setPassword(e.target.value)}
   337→                  required
   338→                  minLength={6}
   339→                  autoComplete="new-password"
   340→                  className="h-12 text-base"
   341→                />
   342→

   343→
   344→              {authError && (
   345→
{authError}

   346→              )}
   347→
   348→              <Button
   349→                type="submit"
   350→                className="h-12 w-full gap-2 text-base font-semibold"
   351→                disabled={authLoading}
   352→              >
   353→                {authLoading ? (
   354→                  <>
   355→                    <Loader2 className="h-5 w-5 animate-spin" />
   356→                    Creating account...
   357→                  </>
   358→                ) : (
   359→                  <>
   360→                    Continue
   361→                    <ArrowRight className="h-5 w-5" />
   362→                  </>
   363→                )}
   364→              </Button>
   365→            </form>
   366→
   367→

   368→              Already have an account?{" "}
   369→              <Link
   370→                href="/auth/login"
   371→                className="text-primary underline hover:no-underline"
   372→              >
   373→                Sign in
   374→              </Link>
   375→

   376→

   377→        )}
   378→
   379→        {/* Step: Choose method */}
   380→        {step === "method" && (
   381→

   382→

   383→

   384→                <HardHat className="h-5 w-5" />
   385→

   386→

   387→
Build your profile

   388→

   389→                  How do you want to get started?
   390→

   391→

   392→

   393→
   394→

   395→              <button
   396→                onClick={() => setStep("trade")}
   397→                className="flex w-full items-center gap-4 rounded-xl border-2 border-primary bg-primary/5 p-5 text-left transition-colors hover:bg-primary/10"
   398→              >
   399→

   400→                  <Keyboard className="h-6 w-6" />
   401→

   402→

   403→
Type my answers

   404→

   405→                    5 quick questions, under 2 minutes
   406→

   407→

   408→                <ArrowRight className="ml-auto h-5 w-5 text-primary" />
   409→              </button>
   410→
   411→              <button
   412→                disabled
   413→                className="flex w-full items-center gap-4 rounded-xl border-2 border-muted p-5 text-left opacity-50"
   414→              >
   415→

   416→                  <Mic className="h-6 w-6" />
   417→

   418→

   419→
Send a voice note

   420→

   421→                    Coming soon — speak and AI builds your profile
   422→

   423→

   424→              </button>
   425→

   426→

   427→        )}
   428→
   429→        {/* Step: Select trade */}
   430→        {step === "trade" && (
   431→

   432→            <StepHeader
   433→              icon={<Wrench className="h-5 w-5" />}
   434→              title="What's your trade?"
   435→              subtitle="Select one or more"
   436→            />
   437→
   438→

   439→              {TRADES.map((trade) => (
   440→                <button
   441→                  key={trade}
   442→                  onClick={() => toggleTrade(trade)}
   443→                  className={`rounded-xl border-2 px-4 py-3 text-left text-sm font-medium transition-colors ${
   444→                    selectedTrades.includes(trade)
   445→                      ? "border-primary bg-primary/10 text-primary"
   446→                      : "border-border hover:border-primary/50"
   447→                  }`}
   448→                >
   449→                  {trade}
   450→                </button>
   451→              ))}
   452→

   453→
   454→

   455→              <Button
   456→                variant="outline"
   457→                onClick={() => setStep("method")}
   458→                className="h-12"
   459→              >
   460→                <ArrowLeft className="h-5 w-5" />
   461→              </Button>
   462→              <Button
   463→                onClick={() => setStep("experience")}
   464→                disabled={selectedTrades.length === 0}
   465→                className="h-12 flex-1 gap-2 text-base font-semibold"
   466→              >
   467→                Continue
   468→                <ArrowRight className="h-5 w-5" />
   469→              </Button>
   470→

   471→

   472→        )}
   473→
   474→        {/* Step: Experience */}
   475→        {step === "experience" && (
   476→

   477→            <StepHeader
   478→              icon={<Clock className="h-5 w-5" />}
   479→              title="How much experience?"
   480→              subtitle="In construction overall"
   481→            />
   482→
   483→

   484→              {EXPERIENCE_OPTIONS.map((opt) => (
   485→                <button
   486→                  key={opt.value}
   487→                  onClick={() => {
   488→                    setExperience(opt.value);
   489→                    setStep("location");
   490→                  }}
   491→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 py-4 text-left text-base font-medium transition-colors ${
   492→                    experience === opt.value
   493→                      ? "border-primary bg-primary/10 text-primary"
   494→                      : "border-border hover:border-primary/50"
   495→                  }`}
   496→                >
   497→                  {opt.label}
   498→                  <ArrowRight className="h-4 w-4 text-muted-foreground" />
   499→                </button>
   500→              ))}
   501→

   502→
   503→            <button
   504→              onClick={() => setStep("trade")}
   505→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   506→            >
   507→              <ArrowLeft className="h-4 w-4" />
   508→              Back
   509→            </button>
   510→

   511→        )}
   512→
   513→        {/* Step: Location */}
   514→        {step === "location" && (
   515→

   516→            <StepHeader
   517→              icon={<MapPin className="h-5 w-5" />}
   518→              title="Where are you based?"
   519→              subtitle="Your suburb or area"
   520→            />
   521→
   522→            <form
   523→              onSubmit={(e) => {
   524→                e.preventDefault();
   525→                if (suburb.trim()) setStep("availability");
   526→              }}
   527→              className="mt-6 space-y-4"
   528→            >
   529→              <Input
   530→                placeholder="e.g. Parramatta, Blacktown, Liverpool"
   531→                value={suburb}
   532→                onChange={(e) => setSuburb(e.target.value)}
   533→                required
   534→                autoFocus
   535→                className="h-12 text-base"
   536→              />
   537→

   538→                We'll match you to jobs near this location.
   539→

   540→
   541→

   542→                <Button
   543→                  type="button"
   544→                  variant="outline"
   545→                  onClick={() => setStep("experience")}
   546→                  className="h-12"
   547→                >
   548→                  <ArrowLeft className="h-5 w-5" />
   549→                </Button>
   550→                <Button
   551→                  type="submit"
   552→                  disabled={!suburb.trim()}
   553→                  className="h-12 flex-1 gap-2 text-base font-semibold"
   554→                >
   555→                  Continue
   556→                  <ArrowRight className="h-5 w-5" />
   557→                </Button>
   558→

   559→            </form>
   560→

   561→        )}
   562→
   563→        {/* Step: Availability */}
   564→        {step === "availability" && (
   565→

   566→            <StepHeader
   567→              icon={<Calendar className="h-5 w-5" />}
   568→              title="Your availability?"
   569→              subtitle="When can you work?"
   570→            />
   571→
   572→

   573→              {AVAILABILITY_OPTIONS.map((opt) => (
   574→                <button
   575→                  key={opt.value}
   576→                  onClick={() => {
   577→                    setAvailability(opt.value);
   578→                    setStep("certs");
   579→                  }}
   580→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors ${
   581→                    availability === opt.value
   582→                      ? "border-primary bg-primary/10"
   583→                      : "border-border hover:border-primary/50"
   584→                  }`}
   585→                >
   586→

   587→                    <p className={`text-base font-medium ${availability === opt.value ? "text-primary" : ""}`}>
   588→                      {opt.label}
   589→

   590→
{opt.desc}

   591→

   592→                  <ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
   593→                </button>
   594→              ))}
   595→

   596→
   597→            <button
   598→              onClick={() => setStep("location")}
   599→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   600→            >
   601→              <ArrowLeft className="h-4 w-4" />
   602→              Back
   603→            </button>
   604→

   605→        )}
   606→
   607→        {/* Step: Certifications */}
   608→        {step === "certs" && (
   609→

   610→            <StepHeader
   611→              icon={<Shield className="h-5 w-5" />}
   612→              title="Your certifications"
   613→              subtitle="Select all that apply (optional)"
   614→            />
   615→
   616→

   617→              {CERT_OPTIONS.map((cert) => (
   618→                <button
   619→                  key={cert}
   620→                  onClick={() => toggleCert(cert)}
   621→                  className={`flex w-full items-center gap-3 rounded-xl border-2 px-5 py-3.5 text-left text-sm font-medium transition-colors ${
   622→                    certifications.includes(cert)
   623→                      ? "border-primary bg-primary/10 text-primary"
   624→                      : "border-border hover:border-primary/50"
   625→                  }`}
   626→                >
   627→

   634→                    {certifications.includes(cert) && (
   635→                      <CheckCircle2 className="h-3.5 w-3.5" />
   636→                    )}
   637→

   638→                  {cert}
   639→                </button>
   640→              ))}
   641→

   642→
   643→

   644→              <Button
   645→                variant="outline"
   646→                onClick={() => setStep("availability")}
   647→                className="h-12"
   648→              >
   649→                <ArrowLeft className="h-5 w-5" />
   650→              </Button>
   651→              <Button
   652→                onClick={() => setStep("review")}
   653→                className="h-12 flex-1 gap-2 text-base font-semibold"
   654→              >
   655→                Review my profile
   656→                <ArrowRight className="h-5 w-5" />
   657→              </Button>
   658→

   659→

   660→        )}
   661→
   662→        {/* Step: Review */}
   663→        {step === "review" && workerData && (
   664→

   665→

   666→

   667→                <CheckCircle2 className="h-5 w-5" />
   668→

   669→

   670→
Your profile

   671→

   672→                  Review and confirm
   673→

   674→

   675→

   676→
   677→            <Card className="mt-6">
   678→              <CardContent className="space-y-4 pt-6">
   679→                <ReviewField
   680→                  icon={<Wrench className="h-4 w-4" />}
   681→                  label="Trade"
   682→                  value={workerData.trades.join(", ")}
   683→                  onEdit={() => setStep("trade")}
   684→                />
   685→                <ReviewField
   686→                  icon={<Clock className="h-4 w-4" />}
   687→                  label="Experience"
   688→                  value={
   689→                    workerData.experience === 0
   690→                      ? "Less than 1 year"
   691→                      : workerData.experience >= 12
   692→                        ? "10+ years"
   693→                        : `${workerData.experience} years`
   694→                  }
   695→                  onEdit={() => setStep("experience")}
   696→                />
   697→                <ReviewField
   698→                  icon={<MapPin className="h-4 w-4" />}
   699→                  label="Location"
   700→                  value={workerData.suburb}
   701→                  onEdit={() => setStep("location")}
   702→                />
   703→                <ReviewField
   704→                  icon={<Calendar className="h-4 w-4" />}
   705→                  label="Availability"
   706→                  value={
   707→                    AVAILABILITY_OPTIONS.find(
   708→                      (o) => o.value === workerData.availability
   709→                    )?.label ?? workerData.availability
   710→                  }
   711→                  onEdit={() => setStep("availability")}
   712→                />
   713→                <ReviewField
   714→                  icon={<Shield className="h-4 w-4" />}
   715→                  label="Certifications"
   716→                  value={
   717→                    workerData.certifications.length > 0
   718→                      ? workerData.certifications.join(", ")
   719→                      : "None added"
   720→                  }
   721→                  onEdit={() => setStep("certs")}
   722→                />
   723→              </CardContent>
   724→            </Card>
   725→
   726→            {/* AI-generated bio */}
   727→

   728→

   729→                <Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
   730→

   731→

   732→                    AI-generated bio
   733→

   734→

   735→                    {generateBio(workerData)}
   736→

   737→

   738→

   739→

   740→
   741→            <Button
   742→              onClick={handleConfirm}
   743→              className="mt-6 h-14 w-full gap-2 text-base font-semibold"
   744→              disabled={saving}
   745→            >
   746→              {saving ? (
   747→                <>
   748→                  <Loader2 className="h-5 w-5 animate-spin" />
   749→                  Creating profile...
   750→                </>
   751→              ) : (
   752→                <>
   753→                  <CheckCircle2 className="h-5 w-5" />
   754→                  Looks good — create my profile
   755→                </>
   756→              )}
   757→            </Button>
   758→
   759→            <button
   760→              onClick={() => setStep("certs")}
   761→              className="mt-4 flex w-full items-center justify-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   762→            >
   763→              <ArrowLeft className="h-4 w-4" />
   764→              Go back
   765→            </button>
   766→

   767→        )}
   768→
   769→

   770→  );
   771→}
   772→
   773→function StepHeader({
   774→  icon,
   775→  title,
   776→  subtitle,
   777→}: {
   778→  icon: React.ReactNode;
   779→  title: string;
   780→  subtitle: string;
   781→}) {
   782→  return (
   783→

   784→

   785→        {icon}
   786→

   787→

   788→
{title}

   789→
{subtitle}

   790→

   791→

   792→  );
   793→}
   794→
   795→function ReviewField({
   796→  icon,
   797→  label,
   798→  value,
   799→  onEdit,
   800→}: {
   801→  icon: React.ReactNode;
   802→  label: string;
   803→  value: string;
   804→  onEdit: () => void;
   805→}) {
   806→  return (
   807→    <button
   808→      onClick={onEdit}
   809→      className="flex w-full items-start gap-3 rounded-lg p-2 text-left transition-colors hover:bg-muted"
   810→    >
   811→
{icon}

   812→

   813→
{label}

   814→
{value}

   815→

   816→      <Edit3 className="mt-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
   817→    </button>
   818→  );
   819→}
   820→

<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>


Perfect! Let me check for any additional related utilities or hooks:


src/components/VoiceRecorder.tsx:7:  onRecordingComplete: (blob: Blob) => void;
src/components/VoiceRecorder.tsx:11:export default function VoiceRecorder({ onRecordingComplete, disabled }: VoiceRecorderProps) {
src/components/VoiceRecorder.tsx:37:        onRecordingComplete(blob);
src/components/VoiceRecorder.tsx:46:  }, [onRecordingComplete]);
src/app/(authenticated)/worker/profile/build/page.tsx:383:                    <VoiceRecorder onRecordingComplete={handleVoiceComplete} />

[Showing results with pagination = limit: 50, offset: 0]


Excellent! Now let me get one more overview by checking how the API errors are handled:


     1→import { NextResponse } from "next/server";
     2→
     3→export class APIError extends Error {
     4→  constructor(
     5→    message: string,
     6→    public statusCode: number = 500,
     7→    public code?: string
     8→  ) {
     9→    super(message);
    10→    this.name = "APIError";
    11→  }
    12→}
    13→
    14→export const ErrorCodes = {
    15→  // Authentication errors
    16→  UNAUTHORIZED: "UNAUTHORIZED",
    17→  FORBIDDEN: "FORBIDDEN",
    18→  TOKEN_EXPIRED: "TOKEN_EXPIRED",
    19→
    20→  // Validation errors
    21→  VALIDATION_ERROR: "VALIDATION_ERROR",
    22→  INVALID_INPUT: "INVALID_INPUT",
    23→  MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
    24→
    25→  // Rate limiting
    26→  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
    27→
    28→  // Service errors
    29→  SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
    30→  EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
    31→
    32→  // Resource errors
    33→  NOT_FOUND: "NOT_FOUND",
    34→  CONFLICT: "CONFLICT",
    35→
    36→  // Generic error
    37→  INTERNAL_ERROR: "INTERNAL_ERROR",
    38→} as const;
    39→
    40→export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
    41→
    42→interface ErrorResponse {
    43→  error: string;
    44→  code?: ErrorCode;
    45→  details?: any;
    46→  timestamp: string;
    47→  path?: string;
    48→}
    49→
    50→/**
    51→ * Creates a standardized error response
    52→ */
    53→export function createErrorResponse(
    54→  message: string,
    55→  statusCode: number = 500,
    56→  code?: ErrorCode,
    57→  details?: any
    58→): NextResponse<ErrorResponse> {
    59→  const errorResponse: ErrorResponse = {
    60→    error: message,
    61→    code,
    62→    details: process.env.NODE_ENV === "development" ? details : undefined,
    63→    timestamp: new Date().toISOString(),
    64→  };
    65→
    66→  return NextResponse.json(errorResponse, { status: statusCode });
    67→}
    68→
    69→/**
    70→ * Common error response helpers
    71→ */
    72→export const ErrorResponses = {
    73→  unauthorized: (message = "Unauthorized") =>
    74→    createErrorResponse(message, 401, ErrorCodes.UNAUTHORIZED),
    75→
    76→  forbidden: (message = "Forbidden") =>
    77→    createErrorResponse(message, 403, ErrorCodes.FORBIDDEN),
    78→
    79→  notFound: (message = "Not found") =>
    80→    createErrorResponse(message, 404, ErrorCodes.NOT_FOUND),
    81→
    82→  validation: (message = "Validation error", details?: any) =>
    83→    createErrorResponse(message, 400, ErrorCodes.VALIDATION_ERROR, details),
    84→
    85→  rateLimit: (message = "Rate limit exceeded", retryAfter?: number) => {
    86→    const response = createErrorResponse(message, 429, ErrorCodes.RATE_LIMIT_EXCEEDED);
    87→    if (retryAfter) {
    88→      response.headers.set("Retry-After", String(retryAfter));
    89→    }
    90→    return response;
    91→  },
    92→
    93→  internal: (message = "Internal server error", details?: any) =>
    94→    createErrorResponse(message, 500, ErrorCodes.INTERNAL_ERROR, details),
    95→
    96→  serviceUnavailable: (message = "Service temporarily unavailable") =>
    97→    createErrorResponse(message, 503, ErrorCodes.SERVICE_UNAVAILABLE),
    98→} as const;
    99→
   100→/**
   101→ * Wraps an API route handler with error handling
   102→ */
   103→export function withErrorHandler<T extends Request = Request>(
   104→  handler: (request: T) => Promise<Response>
   105→) {
   106→  return async (request: T): Promise<Response> => {
   107→    try {
   108→      return await handler(request);
   109→    } catch (error) {
   110→      console.error("API route error:", error);
   111→
   112→      if (error instanceof APIError) {
   113→        return createErrorResponse(
   114→          error.message,
   115→          error.statusCode,
   116→          error.code as ErrorCode
   117→        );
   118→      }
   119→
   120→      if (error instanceof Error) {
   121→        return ErrorResponses.internal(
   122→          "An unexpected error occurred",
   123→          process.env.NODE_ENV === "development" ? error.stack : undefined
   124→        );
   125→      }
   126→
   127→      return ErrorResponses.internal("An unexpected error occurred");
   128→    }
   129→  };
   130→}

<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→ * Sanitizes user input to prevent prompt injection attacks
     3→ * Strips control characters, limits length, and escapes template injection
     4→ */
     5→
     6→/**
     7→ * Strip control characters (0x00-0x1F, 0x7F)
     8→ */
     9→function stripControlChars(str: string): string {
    10→  return str.replace(/[\x00-\x1F\x7F]/g, '');
    11→}
    12→
    13→/**
    14→ * Escape template literals to prevent injection
    15→ */
    16→function escapeTemplateLiterals(str: string): string {
    17→  return str
    18→    .replace(/\\/g, '\\\\')  // Escape backslashes first
    19→    .replace(/\$/g, '\\$')   // Escape $ to prevent ${} injection
    20→    .replace(/`/g, '\\`');   // Escape backticks
    21→}
    22→
    23→/**
    24→ * Sanitize string input
    25→ */
    26→export function sanitizeInput(
    27→  input: string,
    28→  options: {
    29→    maxLength?: number;
    30→    stripControls?: boolean;
    31→    escapeTemplates?: boolean;
    32→  } = {}
    33→): string {
    34→  if (typeof input !== 'string') {
    35→    return '';
    36→  }
    37→
    38→  let sanitized = input;
    39→
    40→  // Strip control characters by default
    41→  if (options.stripControls !== false) {
    42→    sanitized = stripControlChars(sanitized);
    43→  }
    44→
    45→  // Escape template literals by default
    46→  if (options.escapeTemplates !== false) {
    47→    sanitized = escapeTemplateLiterals(sanitized);
    48→  }
    49→
    50→  // Trim whitespace
    51→  sanitized = sanitized.trim();
    52→
    53→  // Limit length if specified
    54→  if (options.maxLength && sanitized.length > options.maxLength) {
    55→    sanitized = sanitized.substring(0, options.maxLength);
    56→  }
    57→
    58→  return sanitized;
    59→}
    60→
    61→/**
    62→ * Sanitize multiple string inputs at once
    63→ */
    64→export function sanitizeInputs(
    65→  inputs: Record<string, string>,
    66→  fieldOptions: Record<string, { maxLength?: number }> = {}
    67→): Record<string, string> {
    68→  const sanitized: Record<string, string> = {};
    69→
    70→  for (const [key, value] of Object.entries(inputs)) {
    71→    const options = fieldOptions[key] || {};
    72→    sanitized[key] = sanitizeInput(value, options);
    73→  }
    74→
    75→  return sanitized;
    76→}

<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>


Perfect! Now I have a comprehensive understanding. Let me compile my findings into a clear report.

## Voice Transcription Functionality - Complete Analysis

I've thoroughly explored the RateRight v2 codebase and found a complete voice transcription system. Here are the key findings:

---

### 1. VOICE INPUT COMPONENT

**File:** `/home/ccuser/the-50-dollar-app/src/components/VoiceRecorder.tsx`

This is a React client component that handles audio recording:
- Uses the Web Audio API (`navigator.mediaDevices.getUserMedia`)
- Supports both WebM and MP4 audio formats
- Provides a microphone button that toggles between recording/stopped states
- Visual feedback: animated pulse (red) while recording, blue at rest
- Displays "Tap to speak" / "Tap to stop" instructions
- Handles microphone permission errors gracefully
- Takes `onRecordingComplete` callback to process audio blob

---

### 2. TRANSCRIPTION API ENDPOINT

**File:** `/home/ccuser/the-50-dollar-app/src/app/api/ai/transcribe-voice/route.ts`

POST endpoint that converts audio to text:
- **Route:** `/api/ai/transcribe-voice`
- **Auth:** Requires authenticated Supabase user
- **Security:** CSRF protection + rate limiting (10 requests per 15 minutes)
- **Input:** FormData with audio file blob
- **Output:** JSON with transcribed text
- **Implementation:** Uses OpenAI's Whisper API (`whisper-1` model)
- **Languages:** Configured for English only

---

### 3. VOICE PARSING API ENDPOINT

**File:** `/home/ccuser/the-50-dollar-app/src/app/api/ai/parse-profile-voice/route.ts`

POST endpoint that extracts structured profile data from transcription:
- **Route:** `/api/ai/parse-profile-voice`
- **Auth:** Requires authenticated Supabase user
- **Security:** CSRF protection + rate limiting + input sanitization
- **Input:** JSON with `transcript` field (max 5000 chars)
- **Output:** Parsed profile object with:
  ```json
  {
    "trade": "one of the valid trades or null",
    "experience_years": "number or null",
    "location": "suburb/area string or null",
    "availability": "one of the valid availability options or null"
  }
  ```
- **Implementation:** Uses OpenAI's GPT-4o-mini model with JSON output mode
- **Validation:** Includes trade/availability mapping (e.g., "sparky" → "Electrician", "bricky" → "Bricklayer")

**Valid Trades:**
Labourer, Steel Fixer, Formworker, Carpenter, Electrician, Plumber, Concreter, Bricklayer, Painter, Roofer, Scaffolder, Tiler, Plasterer, Welder, Crane Operator, Excavator Operator

**Valid Availability:**
full-time, casual, weekends, flexible

---

### 4. VOICE INPUT IN FORMS

#### Profile Build Form (Worker)
**File:** `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/build/page.tsx`

The "Tap to speak" feature is integrated into the profile builder:
- Users can record a description at the "trade" step
- Example prompt: "I'm a chippie with 8 years experience in Parramatta, looking for full-time work"
- Flow:
  1. Record audio → Transcribe → Parse profile fields
  2. Pre-fills: trade, experience_years, location, availability
  3. Auto-advances to the appropriate next step
  4. If all fields parsed, skips straight to profile generation

#### Signup Form (Alternative)
**File:** `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/page.tsx`

- Shows "Send a voice note" button but it's **disabled** with "Coming soon" message
- Currently only supports the typed answer flow
- Uses manual form filling instead of voice input

---

### 5. DATA TYPES

**File:** `/home/ccuser/the-50-dollar-app/src/lib/types.ts`

Key type for voice functionality:
```typescript
export interface WorkerProfile {
  voice_note_url: string | null;  // Storage URL for voice recording
  ai_extracted: Record<string, unknown> | null;  // Metadata about extraction
  // ... other fields
}
```

---

### 6. SECURITY & ERROR HANDLING

**Files:**
- `/home/ccuser/the-50-dollar-app/src/lib/api-errors.ts` - Standardized API error handling
- `/home/ccuser/the-50-dollar-app/src/lib/sanitize.ts` - Input sanitization against prompt injection

Key security features:
- CSRF token validation on all voice endpoints
- Rate limiting: 10 requests per 15 minutes per IP
- Input sanitization: Strips control characters, escapes template literals
- Auth check: Requires Supabase user session
- Error standardization: Consistent error codes and responses

---

### SUMMARY TABLE

| Component | Path | Purpose | Status |
|-----------|------|---------|--------|
| VoiceRecorder | `src/components/VoiceRecorder.tsx` | Audio recording UI | ✓ Implemented |
| Transcribe API | `src/app/api/ai/transcribe-voice/route.ts` | Audio → Text | ✓ Implemented |
| Parse Profile API | `src/app/api/ai/parse-profile-voice/route.ts` | Text → Structured Data | ✓ Implemented |
| Profile Builder Integration | `src/app/(authenticated)/worker/profile/build/page.tsx` | Voice form input | ✓ Implemented |
| Signup Voice Flow | `src/app/(authenticated)/worker/signup/page.tsx` | Voice alternative signup | 🚧 UI present, feature disabled |