From 42c9400e6d82fb6078a7930e0994a2cdc428c6a9 Mon Sep 17 00:00:00 2001 From: DMleadgen Date: Thu, 26 Mar 2026 15:21:43 -0600 Subject: [PATCH] deploy: clean Jessica chat and add manuals sync --- STAGING_DEPLOYMENT.md | 150 +++++ app/api/chat/route.ts | 35 +- app/api/livekit/token/route.ts | 38 ++ app/layout.tsx | 17 +- components/forms/sms-consent-fields.tsx | 42 +- components/site-chat-widget.tsx | 18 +- convex/_generated/api.ts | 4 + convex/_generated/server.ts | 9 + convex/admin.ts | 108 ++++ convex/leads.ts | 158 ++++++ convex/manuals.ts | 108 ++++ convex/orders.ts | 200 +++++++ convex/products.ts | 124 ++++ convex/schema.ts | 223 ++++++++ convex/tsconfig.json | 25 + convex/voiceSessions.ts | 129 +++++ lib/manuals-types.ts | 13 +- lib/voice-assistant/persistence.ts | 266 +++++++++ lib/voice-assistant/prompt.ts | 47 ++ lib/voice-assistant/server.ts | 99 ++++ lib/voice-assistant/shared.ts | 53 ++ lib/voice-assistant/types.ts | 81 +++ livekit-agent/README.md | 57 ++ livekit-agent/worker.ts | 722 ++++++++++++++++++++++++ package.json | 30 +- public/android-chrome-192x192.png | Bin 0 -> 18048 bytes public/android-chrome-512x512.png | Bin 0 -> 84603 bytes public/apple-touch-icon.png | Bin 0 -> 16264 bytes public/favicon-16x16.png | Bin 0 -> 478 bytes public/favicon-32x32.png | Bin 0 -> 1213 bytes public/favicon.ico | Bin 0 -> 4003 bytes scripts/deploy-readiness.mjs | 238 ++++++++ scripts/sync-manuals-to-convex.ts | 130 +++++ scripts/upload-to-r2.js | 11 +- 34 files changed, 3076 insertions(+), 59 deletions(-) create mode 100644 STAGING_DEPLOYMENT.md create mode 100644 app/api/livekit/token/route.ts create mode 100644 convex/_generated/api.ts create mode 100644 convex/_generated/server.ts create mode 100644 convex/admin.ts create mode 100644 convex/leads.ts create mode 100644 convex/manuals.ts create mode 100644 convex/orders.ts create mode 100644 convex/products.ts create mode 100644 convex/schema.ts create mode 100644 convex/tsconfig.json create mode 100644 convex/voiceSessions.ts create mode 100644 lib/voice-assistant/persistence.ts create mode 100644 lib/voice-assistant/prompt.ts create mode 100644 lib/voice-assistant/server.ts create mode 100644 lib/voice-assistant/shared.ts create mode 100644 lib/voice-assistant/types.ts create mode 100644 livekit-agent/README.md create mode 100644 livekit-agent/worker.ts create mode 100644 public/android-chrome-192x192.png create mode 100644 public/android-chrome-512x512.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon-32x32.png create mode 100644 public/favicon.ico create mode 100644 scripts/deploy-readiness.mjs create mode 100644 scripts/sync-manuals-to-convex.ts diff --git a/STAGING_DEPLOYMENT.md b/STAGING_DEPLOYMENT.md new file mode 100644 index 00000000..2cf7f8a7 --- /dev/null +++ b/STAGING_DEPLOYMENT.md @@ -0,0 +1,150 @@ +# Rocky Mountain Vending Staging Deployment + +This is the canonical staging deployment runbook for Rocky Mountain Vending. + +## Repo And Build Source + +- Deploy from the `code/` repository root, not the parent workspace folder. +- Forgejo remote: `https://git.abundancepartners.app/matt/Rocky_Mountain_Vending.git` +- Branch: `main` +- Coolify build source: repository root Dockerfile at `/Dockerfile` +- Container port: `3001` +- Health check port: `3001` + +The parent workspace contains older migration notes and deployment docs that do not reflect the current Coolify app layout. Treat this file as the source of truth for staging execution. + +## Staging Targets + +- Staging host: `https://rmv.abundancepartners.app` +- Preview URL: `https://bsowk840kccg08coocwwc44c.85.239.237.247.sslip.io` +- Coolify project: `Rocky Mountain Vending` +- Coolify app UUID: `bsowk840kccg08coocwwc44c` +- Coolify environment UUID: `ew8k8og0gw48swck4ckk84kk` + +## Required Environment Contract + +Use the environment variables the app reads today. + +### Core Site + +- `NEXT_PUBLIC_SITE_URL` +- `NEXT_PUBLIC_SITE_DOMAIN` +- `NEXT_PUBLIC_CONVEX_URL` + +### Voice And Chat + +- `LIVEKIT_URL` +- `LIVEKIT_API_KEY` +- `LIVEKIT_API_SECRET` +- `XAI_API_KEY` +- `VOICE_ASSISTANT_SITE_URL` + +Optional: + +- `XAI_REALTIME_MODEL` +- `NEXT_PUBLIC_CALL_PHONE_DISPLAY` +- `NEXT_PUBLIC_CALL_PHONE_E164` +- `NEXT_PUBLIC_SMS_PHONE_DISPLAY` +- `NEXT_PUBLIC_SMS_PHONE_E164` +- `NEXT_PUBLIC_MANUALS_BASE_URL` +- `NEXT_PUBLIC_THUMBNAILS_BASE_URL` +- `VOICE_RECORDING_ENABLED` +- `VOICE_RECORDING_BUCKET` +- `VOICE_RECORDING_ENDPOINT` +- `VOICE_RECORDING_PUBLIC_BASE_URL` +- `VOICE_RECORDING_ACCESS_KEY_ID` +- `VOICE_RECORDING_SECRET_ACCESS_KEY` +- `VOICE_RECORDING_REGION` + +### Admin And Auth + +- `ADMIN_EMAIL` +- `ADMIN_PASSWORD` +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` + +### Stripe + +- `STRIPE_SECRET_KEY` +- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` +- `STRIPE_WEBHOOK_SECRET` + +### Current Non-Contract Handoff Vars + +Do not rely on these names for current code behavior without a separate alignment change: + +- `GHL_API_TOKEN` +- `ADMIN_API_TOKEN` +- `ADMIN_UI_ENABLED` +- `USESEND_API_KEY` +- `USESEND_BASE_URL` +- `CONVEX_URL` +- `CONVEX_SELF_HOSTED_URL` +- `CONVEX_SELF_HOSTED_ADMIN_KEY` + +## Lead Routing Notes + +- `/api/contact` and `/api/request-machine` prefer Convex whenever `NEXT_PUBLIC_CONVEX_URL` is present. +- The legacy webhook fallback still expects `GHL_CONTACT_WEBHOOK_URL` and `GHL_REQUEST_MACHINE_WEBHOOK_URL`. +- Do not treat `GHL_API_TOKEN` as a fallback for staging readiness. + +## Pre-Deploy Checklist + +1. Start from a clean reviewed commit on `main`. +2. Confirm required staging env coverage: + +```bash +pnpm deploy:staging:env +``` + +3. Run the full local preflight: + +```bash +pnpm deploy:staging:preflight +``` + +4. Push the reviewed release commit to `origin/main`. +5. Verify Coolify is building from the repo root Dockerfile and exposing port `3001`. + +## Post-Deploy Smoke Tests + +Run the smoke test suite against either the preview URL or the staging host: + +```bash +pnpm deploy:staging:smoke --base-url https://rmv.abundancepartners.app +``` + +If you want to verify real admin sign-in, pass credentials without committing them: + +```bash +pnpm deploy:staging:smoke --base-url https://rmv.abundancepartners.app --admin-email "$ADMIN_EMAIL" --admin-password "$ADMIN_PASSWORD" +``` + +If you need to exercise valid contact or request-machine submissions, provide JSON payload files explicitly. This is opt-in because those requests create real records: + +```bash +pnpm deploy:staging:smoke \ + --base-url https://rmv.abundancepartners.app \ + --contact-payload-file ./tmp/contact.json \ + --request-machine-payload-file ./tmp/request-machine.json +``` + +## Acceptance Criteria + +Staging is ready only when all of the following pass on the deployed host: + +- `GET /` +- `GET /contact-us` +- `GET /products` +- `GET /manuals/dashboard` +- `POST /api/contact` with `{}` returns `400` +- `POST /api/request-machine` with `{}` returns `400` +- `POST /api/chat` succeeds +- `POST /api/livekit/token` succeeds +- Admin auth is configured and sign-in works +- Stripe product fetch works +- Stripe checkout route is configured + +## Known Non-Blocking Issue + +- `app/manuals/dashboard/page.tsx` still logs a missing parent-directory JSON file during builds when local manual-report artifacts are absent. The build currently completes successfully, so treat this as follow-up work rather than a staging blocker. diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 83eff21a..b8d130b0 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -57,14 +57,9 @@ type ChatVisitorProfile = { serviceTextConsent: boolean } -function readRequiredEnv(name: string) { +function getOptionalEnv(name: string) { const value = process.env[name] - - if (!value) { - throw new Error(`Missing required site chat environment variable: ${name}`) - } - - return value + return typeof value === "string" && value.trim() ? value.trim() : "" } function getClientIp(request: NextRequest) { @@ -259,7 +254,22 @@ export async function POST(request: NextRequest) { consumeChatRequest({ ip, requestWindowMs: SITE_CHAT_REQUEST_WINDOW_MS, sessionId }) - const xaiApiKey = readRequiredEnv("XAI_API_KEY") + const xaiApiKey = getOptionalEnv("XAI_API_KEY") + if (!xaiApiKey) { + console.warn("[site-chat] missing XAI_API_KEY", { + pathname, + sessionId, + }) + + return NextResponse.json( + { + error: "Jessica is temporarily unavailable right now. Please call us or use the contact form.", + sessionId, + }, + { status: 503, headers: responseHeaders }, + ) + } + const completionResponse = await fetch("https://api.x.ai/v1/chat/completions", { method: "POST", headers: { @@ -344,9 +354,16 @@ export async function POST(request: NextRequest) { } catch (error) { console.error("[site-chat] request failed", error) + const safeError = + error instanceof Error && error.message.startsWith("Missing required site chat environment variable:") + ? "Jessica is temporarily unavailable right now. Please call us or use the contact form." + : error instanceof Error + ? error.message + : "Chat failed unexpectedly." + return NextResponse.json( { - error: error instanceof Error ? error.message : "Chat failed unexpectedly.", + error: safeError, }, { status: 500, headers: responseHeaders }, ) diff --git a/app/api/livekit/token/route.ts b/app/api/livekit/token/route.ts new file mode 100644 index 00000000..7e002c4f --- /dev/null +++ b/app/api/livekit/token/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server" +import { createVoiceAssistantTokenResponse } from "@/lib/voice-assistant/server" +import { isVoiceAssistantSuppressedRoute } from "@/lib/voice-assistant/shared" + +export const runtime = "nodejs" + +type TokenRequestBody = { + pathname?: string +} + +export async function POST(request: NextRequest) { + try { + const body = (await request.json().catch(() => ({}))) as TokenRequestBody + const pathname = typeof body.pathname === "string" && body.pathname.trim() ? body.pathname.trim() : "/" + + if (isVoiceAssistantSuppressedRoute(pathname)) { + console.info("[voice-assistant/token] blocked on suppressed route", { pathname }) + return NextResponse.json({ error: "Voice assistant is not available on this route." }, { status: 403 }) + } + + const tokenResponse = await createVoiceAssistantTokenResponse(pathname) + console.info("[voice-assistant/token] issued token", { + pathname, + roomName: tokenResponse.roomName, + participantIdentity: tokenResponse.participantIdentity, + }) + return NextResponse.json(tokenResponse) + } catch (error) { + console.error("Failed to create LiveKit voice assistant token:", error) + + return NextResponse.json( + { + error: error instanceof Error ? error.message : "Failed to create voice assistant token", + }, + { status: 500 }, + ) + } +} diff --git a/app/layout.tsx b/app/layout.tsx index ab8cef77..31efce42 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -56,19 +56,18 @@ export const metadata: Metadata = { icons: { icon: [ { - url: "/icon-light-32x32.png", - media: "(prefers-color-scheme: light)", + url: "/favicon-16x16.png", + sizes: "16x16", + type: "image/png", }, { - url: "/icon-dark-32x32.png", - media: "(prefers-color-scheme: dark)", - }, - { - url: "/icon.svg", - type: "image/svg+xml", + url: "/favicon-32x32.png", + sizes: "32x32", + type: "image/png", }, ], - apple: "/apple-icon.png", + shortcut: "/favicon.ico", + apple: "/apple-touch-icon.png", }, openGraph: { type: "website", diff --git a/components/forms/sms-consent-fields.tsx b/components/forms/sms-consent-fields.tsx index f12344a5..13312191 100644 --- a/components/forms/sms-consent-fields.tsx +++ b/components/forms/sms-consent-fields.tsx @@ -34,6 +34,7 @@ type SmsConsentFieldsProps = { idPrefix: string marketingChecked: boolean marketingError?: string + mode?: "chat" | "forms" onMarketingChange: (checked: boolean) => void onServiceChange: (checked: boolean) => void serviceChecked: boolean @@ -44,6 +45,7 @@ export function SmsConsentFields({ idPrefix, marketingChecked, marketingError, + mode = "forms", onMarketingChange, onServiceChange, serviceChecked, @@ -70,24 +72,28 @@ export function SmsConsentFields({ {serviceError ?

{serviceError}

: null} -
- onMarketingChange(Boolean(checked))} - className="mt-0.5" - /> - -
- {marketingError ?

{marketingError}

: null} + {mode === "forms" ? ( + <> +
+ onMarketingChange(Boolean(checked))} + className="mt-0.5" + /> + +
+ {marketingError ?

{marketingError}

: null} + + ) : null} ) } diff --git a/components/site-chat-widget.tsx b/components/site-chat-widget.tsx index 7198aec6..d17683ba 100644 --- a/components/site-chat-widget.tsx +++ b/components/site-chat-widget.tsx @@ -59,6 +59,8 @@ type ChatApiResponse = { limits?: ChatLimitStatus } +const CHAT_UNAVAILABLE_MESSAGE = "Jessica is temporarily unavailable right now. Please call or use the contact form." + const SESSION_STORAGE_KEY = "rmv-site-chat-session" const PROFILE_STORAGE_KEY = "rmv-site-chat-profile" const PANEL_MAX_HEIGHT = "min(40rem, calc(100vh - 7rem))" @@ -348,16 +350,16 @@ export function SiteChatWidget() { } if (!response.ok || !data.reply) { - throw new Error(data.error || "Jessica could not reply right now.") + throw new Error(data.error || CHAT_UNAVAILABLE_MESSAGE) } setMessages((current) => [...current, createMessage("assistant", data.reply || "")].slice(-12)) } catch (chatError) { - const message = chatError instanceof Error ? chatError.message : "Jessica could not reply right now." + const message = chatError instanceof Error ? chatError.message : CHAT_UNAVAILABLE_MESSAGE setError(message) setMessages((current) => [ ...current, - createMessage("assistant", "I'm having trouble right now. Please try again or call us."), + createMessage("assistant", "I'm temporarily unavailable right now. Please call us or use the contact form."), ].slice(-12)) } finally { setIsSending(false) @@ -457,6 +459,7 @@ export function SiteChatWidget() {
@@ -467,14 +470,7 @@ export function SiteChatWidget() { consentSourcePage: pathname || "/", })) } - onMarketingChange={(checked) => - setProfileDraft((current) => ({ - ...current, - marketingTextConsent: checked, - consentVersion: SMS_CONSENT_VERSION, - consentSourcePage: pathname || "/", - })) - } + onMarketingChange={() => undefined} serviceError={profileError && !profileDraft.serviceTextConsent ? profileError : undefined} />
diff --git a/convex/_generated/api.ts b/convex/_generated/api.ts new file mode 100644 index 00000000..ccc0e103 --- /dev/null +++ b/convex/_generated/api.ts @@ -0,0 +1,4 @@ +import { anyApi } from "convex/server"; + +export const api = anyApi; +export const internal = anyApi; diff --git a/convex/_generated/server.ts b/convex/_generated/server.ts new file mode 100644 index 00000000..ee8faebf --- /dev/null +++ b/convex/_generated/server.ts @@ -0,0 +1,9 @@ +export { + actionGeneric as action, + httpActionGeneric as httpAction, + internalActionGeneric as internalAction, + internalMutationGeneric as internalMutation, + internalQueryGeneric as internalQuery, + mutationGeneric as mutation, + queryGeneric as query, +} from "convex/server"; diff --git a/convex/admin.ts b/convex/admin.ts new file mode 100644 index 00000000..2d92ac19 --- /dev/null +++ b/convex/admin.ts @@ -0,0 +1,108 @@ +// @ts-nocheck +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +export const ensureAdminUser = mutation({ + args: { + email: v.string(), + name: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const existing = await ctx.db + .query("adminUsers") + .withIndex("by_email", (q) => q.eq("email", args.email)) + .unique(); + const now = Date.now(); + + if (existing) { + await ctx.db.patch(existing._id, { + active: true, + name: args.name ?? existing.name, + updatedAt: now, + lastLoginAt: now, + }); + return await ctx.db.get(existing._id); + } + + const id = await ctx.db.insert("adminUsers", { + email: args.email, + name: args.name, + role: "admin", + active: true, + createdAt: now, + updatedAt: now, + lastLoginAt: now, + }); + return await ctx.db.get(id); + }, +}); + +export const createSession = mutation({ + args: { + email: v.string(), + tokenHash: v.string(), + expiresAt: v.number(), + }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("adminUsers") + .withIndex("by_email", (q) => q.eq("email", args.email)) + .unique(); + if (!user) { + throw new Error("Admin user not found"); + } + + const id = await ctx.db.insert("adminSessions", { + adminUserId: user._id, + tokenHash: args.tokenHash, + expiresAt: args.expiresAt, + createdAt: Date.now(), + }); + return { sessionId: id, user }; + }, +}); + +export const destroySession = mutation({ + args: { + tokenHash: v.string(), + }, + handler: async (ctx, args) => { + const session = await ctx.db + .query("adminSessions") + .withIndex("by_tokenHash", (q) => q.eq("tokenHash", args.tokenHash)) + .unique(); + if (session) { + await ctx.db.delete(session._id); + } + return { success: true }; + }, +}); + +export const validateSession = query({ + args: { + tokenHash: v.string(), + }, + handler: async (ctx, args) => { + const session = await ctx.db + .query("adminSessions") + .withIndex("by_tokenHash", (q) => q.eq("tokenHash", args.tokenHash)) + .unique(); + if (!session || session.expiresAt < Date.now()) { + return null; + } + const user = await ctx.db.get(session.adminUserId); + if (!user || !user.active) { + return null; + } + return { + user: { + id: user._id, + email: user.email, + name: user.name, + role: user.role, + }, + sessionId: session._id, + expiresAt: session.expiresAt, + }; + }, +}); diff --git a/convex/leads.ts b/convex/leads.ts new file mode 100644 index 00000000..80ffba8f --- /dev/null +++ b/convex/leads.ts @@ -0,0 +1,158 @@ +// @ts-nocheck +import { action, mutation } from "./_generated/server"; +import { v } from "convex/values"; + +const baseLead = { + firstName: v.string(), + lastName: v.string(), + email: v.string(), + phone: v.string(), + company: v.optional(v.string()), + intent: v.optional(v.string()), + message: v.optional(v.string()), + source: v.optional(v.string()), + page: v.optional(v.string()), + url: v.optional(v.string()), + serviceTextConsent: v.optional(v.boolean()), + marketingTextConsent: v.optional(v.boolean()), + consentVersion: v.optional(v.string()), + consentCapturedAt: v.optional(v.string()), + consentSourcePage: v.optional(v.string()), +}; + +async function sendWebhook(webhookUrl: string | undefined, payload: Record) { + if (!webhookUrl) { + return { success: false, error: "Webhook URL not configured" }; + } + + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + return { + success: false, + error: `${response.status} ${response.statusText}`, + }; + } + + return { success: true }; +} + +export const createLead = mutation({ + args: { + type: v.union(v.literal("contact"), v.literal("requestMachine")), + firstName: v.string(), + lastName: v.string(), + email: v.string(), + phone: v.string(), + company: v.optional(v.string()), + intent: v.optional(v.string()), + message: v.optional(v.string()), + source: v.optional(v.string()), + page: v.optional(v.string()), + url: v.optional(v.string()), + employeeCount: v.optional(v.string()), + machineType: v.optional(v.string()), + machineCount: v.optional(v.string()), + serviceTextConsent: v.optional(v.boolean()), + marketingTextConsent: v.optional(v.boolean()), + consentVersion: v.optional(v.string()), + consentCapturedAt: v.optional(v.string()), + consentSourcePage: v.optional(v.string()), + marketingConsent: v.optional(v.boolean()), + termsAgreement: v.optional(v.boolean()), + status: v.union(v.literal("pending"), v.literal("delivered"), v.literal("failed")), + error: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const now = Date.now(); + return await ctx.db.insert("leadSubmissions", { + ...args, + createdAt: now, + updatedAt: now, + deliveredAt: args.status === "delivered" ? now : undefined, + }); + }, +}); + +export const submitContact = action({ + args: { + ...baseLead, + }, + handler: async (ctx, args) => { + const payload = { + firstName: args.firstName, + lastName: args.lastName, + email: args.email, + phone: args.phone, + company: args.company ?? "", + custom1: args.message ?? "", + custom2: args.source ?? "website", + custom3: args.page ?? "", + custom4: new Date().toISOString(), + custom5: args.url ?? "", + custom6: args.intent ?? "", + custom7: args.serviceTextConsent ? "Consented" : "Not Consented", + custom8: args.marketingTextConsent ? "Consented" : "Not Consented", + custom9: args.consentVersion ?? "", + custom10: args.consentCapturedAt ?? "", + custom11: args.consentSourcePage ?? "", + }; + + const result = await sendWebhook(process.env.GHL_CONTACT_WEBHOOK_URL, payload); + await ctx.runMutation("leads:createLead", { + type: "contact", + ...args, + status: result.success ? "delivered" : "failed", + error: result.error, + }); + return result; + }, +}); + +export const submitRequestMachine = action({ + args: { + ...baseLead, + employeeCount: v.string(), + machineType: v.string(), + machineCount: v.string(), + }, + handler: async (ctx, args) => { + const payload = { + firstName: args.firstName, + lastName: args.lastName, + email: args.email, + phone: args.phone, + company: args.company ?? "", + custom1: args.employeeCount, + custom2: args.machineType, + custom3: args.machineCount, + custom4: args.message ?? "", + custom5: args.source ?? "website", + custom6: args.page ?? "", + custom7: new Date().toISOString(), + custom8: args.url ?? "", + custom9: args.serviceTextConsent ? "Consented" : "Not Consented", + custom10: args.marketingTextConsent ? "Consented" : "Not Consented", + custom11: args.consentVersion ?? "", + custom12: args.consentCapturedAt ?? "", + custom13: args.consentSourcePage ?? "", + custom14: "Free Consultation Request", + tags: ["Machine Request"], + }; + + const result = await sendWebhook(process.env.GHL_REQUEST_MACHINE_WEBHOOK_URL, payload); + await ctx.runMutation("leads:createLead", { + type: "requestMachine", + ...args, + status: result.success ? "delivered" : "failed", + error: result.error, + }); + return result; + }, +}); diff --git a/convex/manuals.ts b/convex/manuals.ts new file mode 100644 index 00000000..e150afa1 --- /dev/null +++ b/convex/manuals.ts @@ -0,0 +1,108 @@ +// @ts-nocheck +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +const manualInput = v.object({ + filename: v.string(), + path: v.string(), + manufacturer: v.string(), + category: v.string(), + size: v.optional(v.number()), + lastModified: v.optional(v.number()), + searchTerms: v.optional(v.array(v.string())), + commonNames: v.optional(v.array(v.string())), + thumbnailUrl: v.optional(v.string()), + manualUrl: v.optional(v.string()), + hasParts: v.optional(v.boolean()), + assetSource: v.optional(v.string()), + sourcePath: v.optional(v.string()), + sourceSite: v.optional(v.string()), + sourceDomain: v.optional(v.string()), + siteVisibility: v.optional(v.array(v.string())), + importBatch: v.optional(v.string()), +}); + +export const list = query({ + args: {}, + handler: async (ctx) => { + const manuals = await ctx.db.query("manuals").collect(); + return manuals.sort((a, b) => a.filename.localeCompare(b.filename)); + }, +}); + +export const dashboard = query({ + args: {}, + handler: async (ctx) => { + const manuals = await ctx.db.query("manuals").collect(); + const categories = await ctx.db.query("manualCategories").collect(); + + const manufacturerMap = new Map(); + const categoryMap = new Map(); + for (const manual of manuals) { + manufacturerMap.set( + manual.manufacturer, + (manufacturerMap.get(manual.manufacturer) ?? 0) + 1, + ); + categoryMap.set( + manual.category, + (categoryMap.get(manual.category) ?? 0) + 1, + ); + } + + return { + missingManuals: { + summary: { + total_expected_models: manuals.length, + models_missing_all: 0, + models_partial: 0, + }, + }, + qaData: [], + metadata: manuals, + structuredData: [], + semanticIndex: { + total_chunks: manuals.length, + }, + acquisitionList: { + total_items: 0, + high_priority: 0, + medium_priority: 0, + low_priority: 0, + acquisition_list: [], + }, + nameMapping: categories, + }; + }, +}); + +export const upsertMany = mutation({ + args: { + manuals: v.array(manualInput), + }, + handler: async (ctx, args) => { + const now = Date.now(); + const results = []; + for (const manual of args.manuals) { + const existing = await ctx.db + .query("manuals") + .withIndex("by_path", (q) => q.eq("path", manual.path)) + .unique(); + + if (existing) { + await ctx.db.patch(existing._id, { + ...manual, + updatedAt: now, + }); + results.push(await ctx.db.get(existing._id)); + } else { + const id = await ctx.db.insert("manuals", { + ...manual, + createdAt: now, + updatedAt: now, + }); + results.push(await ctx.db.get(id)); + } + } + return results; + }, +}); diff --git a/convex/orders.ts b/convex/orders.ts new file mode 100644 index 00000000..911d6795 --- /dev/null +++ b/convex/orders.ts @@ -0,0 +1,200 @@ +// @ts-nocheck +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +const orderStatus = v.union( + v.literal("pending"), + v.literal("paid"), + v.literal("fulfilled"), + v.literal("cancelled"), + v.literal("refunded"), +); + +const orderItem = v.object({ + productId: v.optional(v.id("products")), + stripeProductId: v.optional(v.string()), + stripePriceId: v.string(), + productName: v.string(), + image: v.optional(v.string()), + price: v.number(), + quantity: v.number(), +}); + +const shippingAddress = v.object({ + name: v.optional(v.string()), + address: v.optional(v.string()), + city: v.optional(v.string()), + state: v.optional(v.string()), + zipCode: v.optional(v.string()), + country: v.optional(v.string()), +}); + +async function attachItems(ctx: any, order: any) { + const items = await ctx.db + .query("orderItems") + .withIndex("by_orderId", (q: any) => q.eq("orderId", order._id)) + .collect(); + + return { + id: order._id, + customerEmail: order.customerEmail, + customerName: order.customerName, + totalAmount: order.totalAmount, + currency: order.currency, + status: order.status, + stripeSessionId: order.stripeSessionId ?? null, + paymentIntentId: order.stripePaymentIntentId ?? null, + createdAt: new Date(order.createdAt).toISOString(), + updatedAt: new Date(order.updatedAt).toISOString(), + shippingAddress: order.shippingAddress, + items: items.map((item: any) => ({ + productId: item.productId ?? null, + productName: item.productName, + price: item.price, + quantity: item.quantity, + priceId: item.stripePriceId, + image: item.image, + })), + }; +} + +export const listAdmin = query({ + args: { + status: v.optional(orderStatus), + search: v.optional(v.string()), + }, + handler: async (ctx, args) => { + let orders = await ctx.db + .query("orders") + .withIndex("by_createdAt") + .collect(); + orders = orders.sort((a, b) => b.createdAt - a.createdAt); + + if (args.status) { + orders = orders.filter((order) => order.status === args.status); + } + + const search = args.search?.trim().toLowerCase(); + if (search) { + orders = orders.filter((order) => { + return ( + order.customerEmail.toLowerCase().includes(search) || + order.customerName?.toLowerCase().includes(search) || + order._id.toLowerCase().includes(search) + ); + }); + } + + return await Promise.all(orders.map((order) => attachItems(ctx, order))); + }, +}); + +export const getMetrics = query({ + args: {}, + handler: async (ctx) => { + const orders = await ctx.db.query("orders").collect(); + const paidOrders = orders.filter((order) => order.status === "paid" || order.status === "fulfilled"); + const revenue = paidOrders.reduce((sum, order) => sum + order.totalAmount, 0); + + return { + totalOrders: orders.length, + totalRevenue: revenue, + pendingOrders: orders.filter((order) => order.status === "pending").length, + completedOrders: orders.filter((order) => order.status === "fulfilled").length, + paidOrders: orders.filter((order) => order.status === "paid").length, + refundedOrders: orders.filter((order) => order.status === "refunded").length, + }; + }, +}); + +export const upsertStripeOrder = mutation({ + args: { + customerEmail: v.string(), + customerName: v.optional(v.string()), + status: orderStatus, + totalAmount: v.number(), + currency: v.string(), + stripeSessionId: v.optional(v.string()), + stripePaymentIntentId: v.optional(v.string()), + shippingAddress: v.optional(shippingAddress), + items: v.array(orderItem), + }, + handler: async (ctx, args) => { + const now = Date.now(); + let existing = + args.stripeSessionId + ? await ctx.db + .query("orders") + .withIndex("by_stripeSessionId", (q) => + q.eq("stripeSessionId", args.stripeSessionId), + ) + .unique() + : null; + + let orderId; + if (existing) { + orderId = existing._id; + await ctx.db.patch(orderId, { + customerEmail: args.customerEmail, + customerName: args.customerName, + status: args.status, + totalAmount: args.totalAmount, + currency: args.currency, + stripePaymentIntentId: args.stripePaymentIntentId, + shippingAddress: args.shippingAddress, + updatedAt: now, + }); + + const existingItems = await ctx.db + .query("orderItems") + .withIndex("by_orderId", (q) => q.eq("orderId", orderId)) + .collect(); + for (const item of existingItems) { + await ctx.db.delete(item._id); + } + } else { + orderId = await ctx.db.insert("orders", { + customerEmail: args.customerEmail, + customerName: args.customerName, + status: args.status, + totalAmount: args.totalAmount, + currency: args.currency, + stripeSessionId: args.stripeSessionId, + stripePaymentIntentId: args.stripePaymentIntentId, + shippingAddress: args.shippingAddress, + createdAt: now, + updatedAt: now, + }); + } + + for (const item of args.items) { + await ctx.db.insert("orderItems", { + orderId, + productId: item.productId, + stripeProductId: item.stripeProductId, + stripePriceId: item.stripePriceId, + productName: item.productName, + image: item.image, + price: item.price, + quantity: item.quantity, + createdAt: now, + }); + } + + return await attachItems(ctx, (await ctx.db.get(orderId))!); + }, +}); + +export const updateStatus = mutation({ + args: { + id: v.id("orders"), + status: orderStatus, + }, + handler: async (ctx, args) => { + await ctx.db.patch(args.id, { + status: args.status, + updatedAt: Date.now(), + }); + return await attachItems(ctx, (await ctx.db.get(args.id))!); + }, +}); diff --git a/convex/products.ts b/convex/products.ts new file mode 100644 index 00000000..4eb53b68 --- /dev/null +++ b/convex/products.ts @@ -0,0 +1,124 @@ +// @ts-nocheck +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +const productArgs = { + id: v.optional(v.id("products")), + name: v.string(), + description: v.optional(v.string()), + price: v.number(), + currency: v.string(), + images: v.array(v.string()), + metadata: v.optional(v.record(v.string(), v.string())), + stripeProductId: v.optional(v.string()), + stripePriceId: v.optional(v.string()), + active: v.boolean(), + featured: v.optional(v.boolean()), +}; + +export const listActive = query({ + args: {}, + handler: async (ctx) => { + return await ctx.db + .query("products") + .withIndex("by_active", (q) => q.eq("active", true)) + .collect(); + }, +}); + +export const listAdmin = query({ + args: { + search: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const products = await ctx.db.query("products").collect(); + const search = args.search?.trim().toLowerCase(); + if (!search) { + return products.sort((a, b) => a.name.localeCompare(b.name)); + } + return products + .filter((product) => { + return ( + product.name.toLowerCase().includes(search) || + product.description?.toLowerCase().includes(search) + ); + }) + .sort((a, b) => a.name.localeCompare(b.name)); + }, +}); + +export const getById = query({ + args: { + id: v.id("products"), + }, + handler: async (ctx, args) => { + return await ctx.db.get(args.id); + }, +}); + +export const upsert = mutation({ + args: productArgs, + handler: async (ctx, args) => { + const now = Date.now(); + if (args.id) { + await ctx.db.patch(args.id, { + name: args.name, + description: args.description, + price: args.price, + currency: args.currency, + images: args.images, + metadata: args.metadata, + stripeProductId: args.stripeProductId, + stripePriceId: args.stripePriceId, + active: args.active, + featured: args.featured, + updatedAt: now, + }); + return await ctx.db.get(args.id); + } + + const id = await ctx.db.insert("products", { + name: args.name, + description: args.description, + price: args.price, + currency: args.currency, + images: args.images, + metadata: args.metadata, + stripeProductId: args.stripeProductId, + stripePriceId: args.stripePriceId, + active: args.active, + featured: args.featured, + createdAt: now, + updatedAt: now, + }); + return await ctx.db.get(id); + }, +}); + +export const deactivate = mutation({ + args: { + id: v.id("products"), + }, + handler: async (ctx, args) => { + await ctx.db.patch(args.id, { + active: false, + updatedAt: Date.now(), + }); + return await ctx.db.get(args.id); + }, +}); + +export const bulkDeactivate = mutation({ + args: { + ids: v.array(v.id("products")), + }, + handler: async (ctx, args) => { + const now = Date.now(); + const results = []; + for (const id of args.ids) { + await ctx.db.patch(id, { active: false, updatedAt: now }); + results.push(await ctx.db.get(id)); + } + return results; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts new file mode 100644 index 00000000..2b11b065 --- /dev/null +++ b/convex/schema.ts @@ -0,0 +1,223 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +const orderStatus = v.union( + v.literal("pending"), + v.literal("paid"), + v.literal("fulfilled"), + v.literal("cancelled"), + v.literal("refunded"), +); + +export default defineSchema({ + products: defineTable({ + name: v.string(), + description: v.optional(v.string()), + price: v.number(), + currency: v.string(), + images: v.array(v.string()), + metadata: v.optional(v.record(v.string(), v.string())), + stripeProductId: v.optional(v.string()), + stripePriceId: v.optional(v.string()), + active: v.boolean(), + featured: v.optional(v.boolean()), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_active", ["active"]) + .index("by_stripeProductId", ["stripeProductId"]), + + orders: defineTable({ + customerEmail: v.string(), + customerName: v.optional(v.string()), + status: orderStatus, + totalAmount: v.number(), + currency: v.string(), + stripeSessionId: v.optional(v.string()), + stripePaymentIntentId: v.optional(v.string()), + shippingAddress: v.optional( + v.object({ + name: v.optional(v.string()), + address: v.optional(v.string()), + city: v.optional(v.string()), + state: v.optional(v.string()), + zipCode: v.optional(v.string()), + country: v.optional(v.string()), + }), + ), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_createdAt", ["createdAt"]) + .index("by_status", ["status"]) + .index("by_stripeSessionId", ["stripeSessionId"]) + .index("by_customerEmail", ["customerEmail"]), + + orderItems: defineTable({ + orderId: v.id("orders"), + productId: v.optional(v.id("products")), + stripeProductId: v.optional(v.string()), + stripePriceId: v.string(), + productName: v.string(), + image: v.optional(v.string()), + price: v.number(), + quantity: v.number(), + createdAt: v.number(), + }).index("by_orderId", ["orderId"]), + + manuals: defineTable({ + filename: v.string(), + path: v.string(), + manufacturer: v.string(), + category: v.string(), + size: v.optional(v.number()), + lastModified: v.optional(v.number()), + searchTerms: v.optional(v.array(v.string())), + commonNames: v.optional(v.array(v.string())), + thumbnailUrl: v.optional(v.string()), + manualUrl: v.optional(v.string()), + hasParts: v.optional(v.boolean()), + assetSource: v.optional(v.string()), + sourcePath: v.optional(v.string()), + sourceSite: v.optional(v.string()), + sourceDomain: v.optional(v.string()), + siteVisibility: v.optional(v.array(v.string())), + importBatch: v.optional(v.string()), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_manufacturer", ["manufacturer"]) + .index("by_category", ["category"]) + .index("by_path", ["path"]), + + manualCategories: defineTable({ + name: v.string(), + slug: v.string(), + description: v.optional(v.string()), + icon: v.optional(v.string()), + createdAt: v.number(), + updatedAt: v.number(), + }).index("by_slug", ["slug"]), + + leadSubmissions: defineTable({ + type: v.union(v.literal("contact"), v.literal("requestMachine")), + status: v.union(v.literal("pending"), v.literal("delivered"), v.literal("failed")), + firstName: v.string(), + lastName: v.string(), + email: v.string(), + phone: v.string(), + company: v.optional(v.string()), + intent: v.optional(v.string()), + message: v.optional(v.string()), + source: v.optional(v.string()), + page: v.optional(v.string()), + url: v.optional(v.string()), + employeeCount: v.optional(v.string()), + machineType: v.optional(v.string()), + machineCount: v.optional(v.string()), + serviceTextConsent: v.optional(v.boolean()), + marketingTextConsent: v.optional(v.boolean()), + consentVersion: v.optional(v.string()), + consentCapturedAt: v.optional(v.string()), + consentSourcePage: v.optional(v.string()), + marketingConsent: v.optional(v.boolean()), + termsAgreement: v.optional(v.boolean()), + error: v.optional(v.string()), + deliveredAt: v.optional(v.number()), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_type", ["type"]) + .index("by_status", ["status"]) + .index("by_createdAt", ["createdAt"]), + + adminUsers: defineTable({ + email: v.string(), + name: v.optional(v.string()), + role: v.union(v.literal("admin")), + active: v.boolean(), + createdAt: v.number(), + updatedAt: v.number(), + lastLoginAt: v.optional(v.number()), + }).index("by_email", ["email"]), + + adminSessions: defineTable({ + adminUserId: v.id("adminUsers"), + tokenHash: v.string(), + expiresAt: v.number(), + createdAt: v.number(), + }) + .index("by_tokenHash", ["tokenHash"]) + .index("by_adminUserId", ["adminUserId"]), + + siteSettings: defineTable({ + key: v.string(), + value: v.string(), + description: v.optional(v.string()), + updatedAt: v.number(), + }).index("by_key", ["key"]), + + syncJobs: defineTable({ + kind: v.string(), + status: v.union( + v.literal("pending"), + v.literal("running"), + v.literal("completed"), + v.literal("failed"), + ), + message: v.optional(v.string()), + metadata: v.optional(v.string()), + startedAt: v.optional(v.number()), + completedAt: v.optional(v.number()), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_kind", ["kind"]) + .index("by_status", ["status"]), + + voiceSessions: defineTable({ + roomName: v.string(), + participantIdentity: v.string(), + siteUrl: v.optional(v.string()), + pathname: v.optional(v.string()), + pageUrl: v.optional(v.string()), + source: v.optional(v.string()), + startedAt: v.number(), + endedAt: v.optional(v.number()), + recordingDisclosureAt: v.optional(v.number()), + recordingStatus: v.optional( + v.union( + v.literal("pending"), + v.literal("starting"), + v.literal("recording"), + v.literal("completed"), + v.literal("failed"), + ), + ), + recordingId: v.optional(v.string()), + recordingUrl: v.optional(v.string()), + recordingError: v.optional(v.string()), + metadata: v.optional(v.string()), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_roomName", ["roomName"]) + .index("by_participantIdentity", ["participantIdentity"]) + .index("by_startedAt", ["startedAt"]), + + voiceTranscriptTurns: defineTable({ + sessionId: v.id("voiceSessions"), + roomName: v.string(), + participantIdentity: v.string(), + role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")), + kind: v.optional(v.string()), + text: v.string(), + isFinal: v.optional(v.boolean()), + language: v.optional(v.string()), + source: v.optional(v.string()), + createdAt: v.number(), + }) + .index("by_sessionId", ["sessionId"]) + .index("by_roomName", ["roomName"]) + .index("by_createdAt", ["createdAt"]), +}); diff --git a/convex/tsconfig.json b/convex/tsconfig.json new file mode 100644 index 00000000..41bfbb9b --- /dev/null +++ b/convex/tsconfig.json @@ -0,0 +1,25 @@ +{ + /* This TypeScript project config describes the environment that + * Convex functions run in and is used to typecheck them. + * You can modify it, but some settings are required to use Convex. + */ + "compilerOptions": { + /* These settings are not required by Convex and can be modified. */ + "allowJs": true, + "strict": true, + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + + /* These compiler options are required by Convex */ + "target": "ESNext", + "lib": ["ES2023", "dom"], + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated"] +} diff --git a/convex/voiceSessions.ts b/convex/voiceSessions.ts new file mode 100644 index 00000000..bd9eed73 --- /dev/null +++ b/convex/voiceSessions.ts @@ -0,0 +1,129 @@ +// @ts-nocheck +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +export const getByRoom = query({ + args: { + roomName: v.string(), + }, + handler: async (ctx, args) => { + return await ctx.db + .query("voiceSessions") + .withIndex("by_roomName", (q) => q.eq("roomName", args.roomName)) + .unique(); + }, +}); + +export const createSession = mutation({ + args: { + roomName: v.string(), + participantIdentity: v.string(), + siteUrl: v.optional(v.string()), + pathname: v.optional(v.string()), + pageUrl: v.optional(v.string()), + source: v.optional(v.string()), + metadata: v.optional(v.string()), + startedAt: v.optional(v.number()), + recordingDisclosureAt: v.optional(v.number()), + recordingStatus: v.optional( + v.union( + v.literal("pending"), + v.literal("starting"), + v.literal("recording"), + v.literal("completed"), + v.literal("failed"), + ), + ), + }, + handler: async (ctx, args) => { + const now = args.startedAt ?? Date.now(); + return await ctx.db.insert("voiceSessions", { + ...args, + startedAt: now, + createdAt: now, + updatedAt: now, + }); + }, +}); + +export const addTranscriptTurn = mutation({ + args: { + sessionId: v.id("voiceSessions"), + roomName: v.string(), + participantIdentity: v.string(), + role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")), + text: v.string(), + kind: v.optional(v.string()), + isFinal: v.optional(v.boolean()), + language: v.optional(v.string()), + source: v.optional(v.string()), + createdAt: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const createdAt = args.createdAt ?? Date.now(); + return await ctx.db.insert("voiceTranscriptTurns", { + ...args, + text: args.text.trim(), + createdAt, + }); + }, +}); + +export const updateRecording = mutation({ + args: { + sessionId: v.id("voiceSessions"), + recordingStatus: v.optional( + v.union( + v.literal("pending"), + v.literal("starting"), + v.literal("recording"), + v.literal("completed"), + v.literal("failed"), + ), + ), + recordingId: v.optional(v.string()), + recordingUrl: v.optional(v.string()), + recordingError: v.optional(v.string()), + }, + handler: async (ctx, args) => { + await ctx.db.patch(args.sessionId, { + recordingStatus: args.recordingStatus, + recordingId: args.recordingId, + recordingUrl: args.recordingUrl, + recordingError: args.recordingError, + updatedAt: Date.now(), + }); + return await ctx.db.get(args.sessionId); + }, +}); + +export const completeSession = mutation({ + args: { + sessionId: v.id("voiceSessions"), + endedAt: v.optional(v.number()), + recordingStatus: v.optional( + v.union( + v.literal("pending"), + v.literal("starting"), + v.literal("recording"), + v.literal("completed"), + v.literal("failed"), + ), + ), + recordingId: v.optional(v.string()), + recordingUrl: v.optional(v.string()), + recordingError: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const endedAt = args.endedAt ?? Date.now(); + await ctx.db.patch(args.sessionId, { + endedAt, + recordingStatus: args.recordingStatus, + recordingId: args.recordingId, + recordingUrl: args.recordingUrl, + recordingError: args.recordingError, + updatedAt: endedAt, + }); + return await ctx.db.get(args.sessionId); + }, +}); diff --git a/lib/manuals-types.ts b/lib/manuals-types.ts index 1dea372e..4f299a9d 100644 --- a/lib/manuals-types.ts +++ b/lib/manuals-types.ts @@ -17,6 +17,10 @@ export interface ManualGroup { } } +function isAbsoluteUrl(value: string) { + return /^https?:\/\//i.test(value) +} + /** * Get public URL for a manual file * Supports both local static files and external hosting (e.g., Cloudflare) @@ -26,6 +30,10 @@ export interface ManualGroup { * - If not set: Uses local static path "/manuals/" */ export function getManualUrl(manual: Manual): string { + if (isAbsoluteUrl(manual.path)) { + return manual.path + } + // Handle both absolute and relative paths // If path is absolute (contains /manuals/), extract relative portion // Otherwise, assume it's already relative @@ -82,6 +90,10 @@ export function getThumbnailUrl(manual: Manual): string | null { if (!manual.thumbnailUrl) { return null } + + if (isAbsoluteUrl(manual.thumbnailUrl)) { + return manual.thumbnailUrl + } // Handle both absolute and relative paths let relativePath: string @@ -119,4 +131,3 @@ export function getThumbnailUrl(manual: Manual): string | null { // Use local static path for GHL static hosting return `/thumbnails/${encodedPath}` } - diff --git a/lib/voice-assistant/persistence.ts b/lib/voice-assistant/persistence.ts new file mode 100644 index 00000000..c9265e22 --- /dev/null +++ b/lib/voice-assistant/persistence.ts @@ -0,0 +1,266 @@ +import { ConvexHttpClient } from "convex/browser" +import { makeFunctionReference } from "convex/server" +import { + EgressClient, + EncodedFileOutput, + EncodedFileType, + S3Upload, +} from "livekit-server-sdk" +import { VOICE_ASSISTANT_SOURCE } from "@/lib/voice-assistant/types" + +type VoiceRecordingStatus = "pending" | "starting" | "recording" | "completed" | "failed" + +type CreateVoiceSessionArgs = { + roomName: string + participantIdentity: string + siteUrl?: string + pathname?: string + pageUrl?: string + source?: string + metadata?: string + startedAt?: number + recordingDisclosureAt?: number + recordingStatus?: VoiceRecordingStatus +} + +type AddTranscriptTurnArgs = { + sessionId: string + roomName: string + participantIdentity: string + role: "user" | "assistant" | "system" + text: string + kind?: string + isFinal?: boolean + language?: string + source?: string + createdAt?: number +} + +type UpdateVoiceRecordingArgs = { + sessionId: string + recordingStatus?: VoiceRecordingStatus + recordingId?: string + recordingUrl?: string + recordingError?: string +} + +type CompleteVoiceSessionArgs = UpdateVoiceRecordingArgs & { + endedAt?: number +} + +type VoiceRecordingConfig = { + bucket: string + egressClient: EgressClient + endpoint: string + forcePathStyle: boolean + pathPrefix: string + publicBaseUrl?: string + region: string + secret: string + accessKey: string +} + +type VoicePersistenceServices = { + convexClient: ConvexHttpClient | null + recording: VoiceRecordingConfig | null +} + +const CREATE_VOICE_SESSION = makeFunctionReference<"mutation">("voiceSessions:createSession") +const ADD_VOICE_TRANSCRIPT_TURN = makeFunctionReference<"mutation">("voiceSessions:addTranscriptTurn") +const UPDATE_VOICE_RECORDING = makeFunctionReference<"mutation">("voiceSessions:updateRecording") +const COMPLETE_VOICE_SESSION = makeFunctionReference<"mutation">("voiceSessions:completeSession") + +function readOptionalEnv(name: string) { + const value = process.env[name] + return typeof value === "string" && value.trim() ? value.trim() : "" +} + +function readBooleanEnv(name: string) { + const value = readOptionalEnv(name).toLowerCase() + if (!value) { + return undefined + } + + return value === "1" || value === "true" || value === "yes" +} + +function sanitizePathSegment(value: string) { + return value + .replace(/[^a-zA-Z0-9/_-]+/g, "-") + .replace(/-+/g, "-") + .replace(/\/+/g, "/") + .replace(/^-|-$/g, "") +} + +function buildRecordingFilepath(roomName: string) { + const prefix = readOptionalEnv("VOICE_RECORDING_PATH_PREFIX") || "livekit-recordings" + const date = new Date() + const year = date.getUTCFullYear() + const month = String(date.getUTCMonth() + 1).padStart(2, "0") + const day = String(date.getUTCDate()).padStart(2, "0") + const stamp = date.toISOString().replace(/[:.]/g, "-") + const safeRoom = sanitizePathSegment(roomName) + return `${sanitizePathSegment(prefix)}/${year}/${month}/${day}/${safeRoom}-${stamp}.mp3` +} + +function buildRecordingUrl(publicBaseUrl: string | undefined, bucket: string, filepath: string) { + const encodedPath = filepath + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/") + + if (publicBaseUrl) { + return `${publicBaseUrl.replace(/\/$/, "")}/${encodedPath}` + } + + return `s3://${bucket}/${filepath}` +} + +export function getVoicePersistenceServices(args: { + livekitUrl: string + livekitApiKey: string + livekitApiSecret: string +}) { + const convexUrl = readOptionalEnv("NEXT_PUBLIC_CONVEX_URL") || readOptionalEnv("CONVEX_URL") + const convexClient = convexUrl ? new ConvexHttpClient(convexUrl) : null + + const accessKey = + readOptionalEnv("VOICE_RECORDING_ACCESS_KEY_ID") || + readOptionalEnv("CLOUDFLARE_R2_ACCESS_KEY_ID") || + readOptionalEnv("AWS_ACCESS_KEY_ID") || + readOptionalEnv("AWS_ACCESS_KEY") + const secret = + readOptionalEnv("VOICE_RECORDING_SECRET_ACCESS_KEY") || + readOptionalEnv("CLOUDFLARE_R2_SECRET_ACCESS_KEY") || + readOptionalEnv("AWS_SECRET_ACCESS_KEY") || + readOptionalEnv("AWS_SECRET_KEY") + const endpoint = + readOptionalEnv("VOICE_RECORDING_ENDPOINT") || + readOptionalEnv("CLOUDFLARE_R2_ENDPOINT") + const bucket = readOptionalEnv("VOICE_RECORDING_BUCKET") + const region = readOptionalEnv("VOICE_RECORDING_REGION") || readOptionalEnv("AWS_DEFAULT_REGION") || "auto" + const publicBaseUrl = readOptionalEnv("VOICE_RECORDING_PUBLIC_BASE_URL") || undefined + const forcePathStyle = readBooleanEnv("VOICE_RECORDING_FORCE_PATH_STYLE") ?? true + const requested = readBooleanEnv("VOICE_RECORDING_ENABLED") + const hasRecordingEnv = Boolean(accessKey && secret && endpoint && bucket) + const recordingEnabled = requested ?? hasRecordingEnv + + const recording = + recordingEnabled && hasRecordingEnv + ? { + accessKey, + bucket, + egressClient: new EgressClient(args.livekitUrl, args.livekitApiKey, args.livekitApiSecret), + endpoint, + forcePathStyle, + pathPrefix: readOptionalEnv("VOICE_RECORDING_PATH_PREFIX") || "livekit-recordings", + publicBaseUrl, + region, + secret, + } + : null + + return { + convexClient, + recording, + } satisfies VoicePersistenceServices +} + +async function safeMutation( + client: ConvexHttpClient | null, + reference: ReturnType>, + args: TArgs, +) { + if (!client) { + return null + } + + return await client.mutation(reference, args) +} + +export async function createVoiceSessionRecord( + services: VoicePersistenceServices, + args: CreateVoiceSessionArgs, +) { + return await safeMutation(services.convexClient, CREATE_VOICE_SESSION, { + ...args, + source: args.source || VOICE_ASSISTANT_SOURCE, + }) +} + +export async function addVoiceTranscriptTurn( + services: VoicePersistenceServices, + args: AddTranscriptTurnArgs, +) { + const text = args.text.replace(/\s+/g, " ").trim() + if (!text) { + return null + } + + return await safeMutation(services.convexClient, ADD_VOICE_TRANSCRIPT_TURN, { + ...args, + source: args.source || VOICE_ASSISTANT_SOURCE, + text, + }) +} + +export async function updateVoiceRecording( + services: VoicePersistenceServices, + args: UpdateVoiceRecordingArgs, +) { + return await safeMutation(services.convexClient, UPDATE_VOICE_RECORDING, args) +} + +export async function completeVoiceSession( + services: VoicePersistenceServices, + args: CompleteVoiceSessionArgs, +) { + return await safeMutation(services.convexClient, COMPLETE_VOICE_SESSION, args) +} + +export async function startVoiceRecording( + services: VoicePersistenceServices, + roomName: string, +) { + if (!services.recording) { + return null + } + + const filepath = buildRecordingFilepath(roomName) + const output = new EncodedFileOutput({ + fileType: EncodedFileType.MP3, + filepath, + output: { + case: "s3", + value: new S3Upload({ + accessKey: services.recording.accessKey, + secret: services.recording.secret, + region: services.recording.region, + endpoint: services.recording.endpoint, + bucket: services.recording.bucket, + forcePathStyle: services.recording.forcePathStyle, + }), + }, + }) + + const info = await services.recording.egressClient.startRoomCompositeEgress(roomName, output, { + audioOnly: true, + }) + + return { + recordingId: info.egressId, + recordingStatus: "recording" as const, + recordingUrl: buildRecordingUrl(services.recording.publicBaseUrl, services.recording.bucket, filepath), + } +} + +export async function stopVoiceRecording( + services: VoicePersistenceServices, + recordingId: string | null | undefined, +) { + if (!services.recording || !recordingId) { + return null + } + + return await services.recording.egressClient.stopEgress(recordingId) +} diff --git a/lib/voice-assistant/prompt.ts b/lib/voice-assistant/prompt.ts new file mode 100644 index 00000000..3f416ab7 --- /dev/null +++ b/lib/voice-assistant/prompt.ts @@ -0,0 +1,47 @@ +import { businessConfig, serviceAreas } from "@/lib/seo-config" + +const SERVICE_AREA_LIST = serviceAreas.map((area) => area.city).join(", ") + +export const VOICE_ASSISTANT_SYSTEM_PROMPT = `You are Jessica, the friendly and upbeat voice assistant for ${businessConfig.legalName} in Utah. + +Use this exact knowledge base and do not go beyond it: +- Free vending placement is only for qualifying businesses. Rocky Mountain Vending installs, stocks, maintains, and repairs those machines at no cost to the business. +- Repairs and maintenance are for machines the customer owns. +- Moving requests can be for a vending machine or a safe, and they follow the same intake flow as repairs. +- Vending machine sales can include new or used machines plus card readers, bill and coin mechanisms, and accessories. +- Manuals and parts support are available. +- Current public service area follows the live website coverage across Utah, including ${SERVICE_AREA_LIST}. + +Conversation rules: +- When the caller name, email, and intent are available in context, use them naturally in your opening reply. +- At the start of every recorded voice session, briefly disclose that the call may be recorded and transcribed for service and quality purposes. +- Keep spoken replies concise, warm, and natural. Default to 1 to 3 short sentences. +- End every reply with one short next-step question. +- For repairs or moving, ask for machine details and a clear description of the issue. If the move is involved, clarify whether it is for a vending machine or a safe. Tell callers to text photos or videos to ${businessConfig.publicSmsNumber} or use the contact form so the team can diagnose remotely first. +- For free placement, confirm it is for a business and ask for the business type plus the approximate number of people. +- For sales, ask whether they want new or used equipment, which features they need, and their budget range. +- For manuals or parts, ask for the machine model or the specific part details. + +Safety rules: +- Never mention prices, service call fees, repair rates, hourly rates, parts costs, or internal policies. +- If the caller asks about pricing or cost, say: "Our complete vending service, including installation, stocking, and maintenance, is provided at no cost to qualifying businesses. I can get a few details so our team can schedule a quick call with you." +- Do not invent timelines, guarantees, inventory, contract terms, or legal details. +- If something needs confirmation, say a team member can confirm it. + +Lead capture rules: +- For general contact requests, collect first name, last name, email, phone, optional company, a short message about their needs, required service-text consent, and optional marketing-text consent. +- For free machine requests, collect first name, last name, email, phone, company, estimated employee count or audience size, desired machine type, desired machine count, optional notes, required service-text consent, and optional marketing-text consent. +- Before using any submission tool, summarize the collected details and ask for explicit confirmation. +- Only use a submission tool after the user clearly confirms. +- If the user does not confirm, keep helping and do not submit anything. + +Tool rules: +- Use submit_contact_lead for general contact or callback requests. +- Use submit_machine_request for free machine placement or machine request flows. +- Use handoff_to_human when the user wants direct human help or asks for something uncertain, sensitive, or commercial. +- After a successful tool call, briefly confirm what happened and what the visitor should expect next. +` + +export function buildHandoffMessage() { + return `You can reach ${businessConfig.name} at ${businessConfig.publicCallNumber}, text photos or videos to ${businessConfig.publicSmsNumber}, email ${businessConfig.email}, or use the contact form on ${businessConfig.website}.` +} diff --git a/lib/voice-assistant/server.ts b/lib/voice-assistant/server.ts new file mode 100644 index 00000000..d6f11945 --- /dev/null +++ b/lib/voice-assistant/server.ts @@ -0,0 +1,99 @@ +import { randomUUID } from "node:crypto" +import { AccessToken, RoomAgentDispatch, RoomConfiguration } from "livekit-server-sdk" +import { + getVoiceAssistantAgentName, + getVoiceAssistantBootstrap, + getVoiceAssistantSiteUrl, +} from "@/lib/voice-assistant/shared" +import type { + LiveKitTokenResponse, + VoiceAssistantVisitorMetadata, +} from "@/lib/voice-assistant/types" +import { VOICE_ASSISTANT_SOURCE } from "@/lib/voice-assistant/types" + +function readRequiredEnv(name: string) { + const value = process.env[name] + + if (!value) { + throw new Error(`Missing required voice assistant environment variable: ${name}`) + } + + return value +} + +export function getVoiceAssistantServerEnv() { + const livekitUrl = readRequiredEnv("LIVEKIT_URL") + const livekitApiKey = readRequiredEnv("LIVEKIT_API_KEY") + const livekitApiSecret = readRequiredEnv("LIVEKIT_API_SECRET") + const xaiApiKey = readRequiredEnv("XAI_API_KEY") + const realtimeModel = process.env.XAI_REALTIME_MODEL || "grok-4-1-fast-non-reasoning" + + return { + livekitUrl, + livekitApiKey, + livekitApiSecret, + xaiApiKey, + realtimeModel, + siteUrl: getVoiceAssistantSiteUrl(), + } +} + +function buildRoomName() { + return `rmv-voice-${randomUUID().slice(0, 8)}` +} + +function buildParticipantIdentity() { + return `website-visitor-${randomUUID().slice(0, 10)}` +} + +function buildVisitorMetadata(pathname: string): VoiceAssistantVisitorMetadata { + const siteUrl = getVoiceAssistantSiteUrl() + const safePathname = pathname.startsWith("/") ? pathname : `/${pathname}` + + return { + source: VOICE_ASSISTANT_SOURCE, + pathname: safePathname, + pageUrl: new URL(safePathname, siteUrl).toString(), + startedAt: new Date().toISOString(), + } +} + +export async function createVoiceAssistantTokenResponse(pathname: string): Promise { + const env = getVoiceAssistantServerEnv() + const roomName = buildRoomName() + const participantIdentity = buildParticipantIdentity() + const visitorMetadata = buildVisitorMetadata(pathname) + const agentName = getVoiceAssistantAgentName() + + const token = new AccessToken(env.livekitApiKey, env.livekitApiSecret, { + identity: participantIdentity, + name: "Website visitor", + metadata: JSON.stringify(visitorMetadata), + ttl: "1 hour", + }) + + token.addGrant({ + roomJoin: true, + room: roomName, + canPublish: true, + canPublishData: true, + canSubscribe: true, + }) + + token.roomConfig = new RoomConfiguration({ + agents: [ + new RoomAgentDispatch({ + agentName, + metadata: JSON.stringify(visitorMetadata), + }), + ], + }) + + return { + serverUrl: env.livekitUrl, + participantToken: await token.toJwt(), + roomName, + participantIdentity, + bootstrap: getVoiceAssistantBootstrap(), + } +} diff --git a/lib/voice-assistant/shared.ts b/lib/voice-assistant/shared.ts new file mode 100644 index 00000000..6ecb8dfd --- /dev/null +++ b/lib/voice-assistant/shared.ts @@ -0,0 +1,53 @@ +import { businessConfig } from "@/lib/seo-config" +import { + VOICE_ASSISTANT_AGENT_NAME, + VOICE_ASSISTANT_SOURCE, + XAI_REALTIME_MODEL, + XAI_REALTIME_VOICE, + type VoiceAssistantBootstrap, +} from "@/lib/voice-assistant/types" + +export const VOICE_ASSISTANT_SUPPRESSED_ROUTE_PREFIXES = [ + "/admin", + "/auth", + "/sign-in", + "/stripe-setup", + "/style-guide", + "/manuals/dashboard", + "/test-page", +] as const + +export function isVoiceAssistantSuppressedRoute(pathname: string) { + return VOICE_ASSISTANT_SUPPRESSED_ROUTE_PREFIXES.some( + (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`), + ) +} + +export function getVoiceAssistantBootstrap(): VoiceAssistantBootstrap { + return { + assistantName: "Jessica", + businessName: businessConfig.name, + phone: businessConfig.publicCallNumber, + phoneUrl: businessConfig.publicCallUrl, + email: businessConfig.email, + model: XAI_REALTIME_MODEL, + voice: XAI_REALTIME_VOICE, + } +} + +export function getVoiceAssistantSiteUrl() { + return ( + process.env.VOICE_ASSISTANT_SITE_URL || + process.env.NEXT_PUBLIC_APP_URL || + process.env.NEXT_PUBLIC_SITE_URL || + businessConfig.website + ) +} + +export function getVoiceAssistantAgentName() { + return VOICE_ASSISTANT_AGENT_NAME +} + +export function getVoiceAssistantSource() { + return VOICE_ASSISTANT_SOURCE +} diff --git a/lib/voice-assistant/types.ts b/lib/voice-assistant/types.ts new file mode 100644 index 00000000..97150fea --- /dev/null +++ b/lib/voice-assistant/types.ts @@ -0,0 +1,81 @@ +export const VOICE_ASSISTANT_AGENT_NAME = "rocky-mountain-vending-voice-assistant" +export const XAI_REALTIME_MODEL = "grok-4-1-fast-non-reasoning" +export const XAI_REALTIME_VOICE = "Ara" +export const VOICE_ASSISTANT_SOURCE = "voice-assistant" + +export type VoiceAssistantUiState = + | "disconnected" + | "connecting" + | "connected_text_ready" + | "awaiting_mic_permission" + | "listening" + | "thinking" + | "speaking" + | "mic_blocked" + | "error" + +export type VoiceAssistantMicIssueCode = + | "permission_denied" + | "device_not_found" + | "device_in_use" + | "publish_failed" + | "unsupported" + | "unknown" + +export type VoiceAssistantMicPermissionState = PermissionState | "unsupported" | "unknown" + +export type VoiceAssistantBootstrap = { + assistantName: string + businessName: string + phone: string + phoneUrl: string + email: string + model: typeof XAI_REALTIME_MODEL + voice: typeof XAI_REALTIME_VOICE +} + +export type LiveKitTokenResponse = { + serverUrl: string + participantToken: string + roomName: string + participantIdentity: string + bootstrap: VoiceAssistantBootstrap +} + +export type VoiceAssistantVisitorMetadata = { + source: typeof VOICE_ASSISTANT_SOURCE + pathname: string + pageUrl: string + startedAt: string +} + +export type AssistantContactLeadPayload = { + firstName: string + lastName: string + email: string + phone: string + company?: string + message: string + serviceTextConsent: true + marketingTextConsent?: boolean + consentVersion: string + consentCapturedAt: string + consentSourcePage: string +} + +export type AssistantMachineRequestPayload = { + firstName: string + lastName: string + email: string + phone: string + company: string + employeeCount: string + machineType: string + machineCount: string + message?: string + serviceTextConsent: true + marketingTextConsent?: boolean + consentVersion: string + consentCapturedAt: string + consentSourcePage: string +} diff --git a/livekit-agent/README.md b/livekit-agent/README.md new file mode 100644 index 00000000..6287eb5b --- /dev/null +++ b/livekit-agent/README.md @@ -0,0 +1,57 @@ +# LiveKit Voice Assistant + +This worker runs the Rocky Mountain Vending site voice assistant with LiveKit Agents and xAI realtime voice. + +## Local development + +1. Start the Next.js app: + `pnpm dev` +2. Start the LiveKit agent worker in a second terminal: + `pnpm voice-agent:dev` +3. Open the site and launch the voice bubble. +4. Run the automated regressions: + `pnpm voice-assistant:test:local` +5. Run the fake-microphone voice regression: + `pnpm voice-assistant:test:local:voice` + +## Environment + +The worker loads environment variables from: +- `.env.local` +- `.env.voice-agent.local` (optional override file) + +Required variables: +- `LIVEKIT_URL` +- `LIVEKIT_API_KEY` +- `LIVEKIT_API_SECRET` +- `XAI_API_KEY` +- `XAI_REALTIME_MODEL=grok-4-1-fast-non-reasoning` + +Recommended variable: +- `VOICE_ASSISTANT_SITE_URL` + +## Production + +Run the worker as a separate long-lived Node process: + +`pnpm voice-agent:start` + +Point `VOICE_ASSISTANT_SITE_URL` at the deployed Next.js app so the worker can reuse `/api/contact` and `/api/request-machine`. + +## Verification + +Automated checks: +- `pnpm voice-assistant:test:local` +- `pnpm voice-assistant:test:local:voice` +- `pnpm voice-assistant:test:live` + +Manual spoken smoke checklist: +1. Open the homepage and launch the assistant bubble. +2. Press `Start assistant` and confirm the panel reaches `Text Ready` or `Listening`. +3. Confirm the text box is enabled before microphone access is granted. +4. Press `Enable microphone` and verify the browser permission prompt appears. +5. After granting permission, say a short question such as "What cities in Utah do you serve?" +6. Confirm you see a user transcript, then an assistant reply transcript. +7. Confirm spoken audio playback is audible after `Enable spoken replies` when the browser requires it. +8. Refresh the page and verify reconnect still works. +9. Open `/sign-in` and confirm the assistant stays hidden there. diff --git a/livekit-agent/worker.ts b/livekit-agent/worker.ts new file mode 100644 index 00000000..b742bb28 --- /dev/null +++ b/livekit-agent/worker.ts @@ -0,0 +1,722 @@ +import { fileURLToPath } from "node:url" +import { config as loadEnv } from "dotenv" +import { AutoSubscribe, cli, defineAgent, llm, type JobContext, voice, WorkerOptions } from "@livekit/agents" +import * as xai from "@livekit/agents-plugin-xai" +import { z } from "zod" +import { SMS_CONSENT_VERSION } from "../lib/sms-compliance" +import { + addVoiceTranscriptTurn, + completeVoiceSession, + createVoiceSessionRecord, + getVoicePersistenceServices, + startVoiceRecording, + stopVoiceRecording, + updateVoiceRecording, +} from "../lib/voice-assistant/persistence" +import { buildHandoffMessage, VOICE_ASSISTANT_SYSTEM_PROMPT } from "../lib/voice-assistant/prompt" +import { getVoiceAssistantServerEnv } from "../lib/voice-assistant/server" +import { + VOICE_ASSISTANT_AGENT_NAME, + VOICE_ASSISTANT_SOURCE, + XAI_REALTIME_VOICE, + type AssistantContactLeadPayload, + type AssistantMachineRequestPayload, + type VoiceAssistantVisitorMetadata, +} from "../lib/voice-assistant/types" + +loadEnv({ path: ".env.local" }) +loadEnv({ path: ".env.voice-agent.local", override: true }) + +const env = getVoiceAssistantServerEnv() +const persistenceServices = getVoicePersistenceServices({ + livekitUrl: env.livekitUrl, + livekitApiKey: env.livekitApiKey, + livekitApiSecret: env.livekitApiSecret, +}) + +type WorkerUserData = { + siteUrl: string + pathname: string + pageUrl: string + startedAt: string +} + +type ApiSuccess = { + success: boolean + message?: string + error?: string +} + +type VoiceSessionPersistenceState = { + recordingId: string | null + recordingStatus: "pending" | "starting" | "recording" | "completed" | "failed" + recordingUrl: string | null + sessionId: string | null +} + +function serializeError(error: unknown) { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + } + } + + return { + message: String(error), + } +} + +function truncateText(value: string, maxLength = 160) { + const normalized = value.replace(/\s+/g, " ").trim() + if (normalized.length <= maxLength) { + return normalized + } + + return `${normalized.slice(0, maxLength - 1)}...` +} + +function logWorker(event: string, payload: Record = {}) { + console.info("[voice-agent]", event, payload) +} + +function parseVisitorMetadata(metadata: string | undefined): VoiceAssistantVisitorMetadata { + if (!metadata) { + return { + source: VOICE_ASSISTANT_SOURCE, + pathname: "/", + pageUrl: env.siteUrl, + startedAt: new Date().toISOString(), + } + } + + try { + const parsed = JSON.parse(metadata) as Partial + + return { + source: VOICE_ASSISTANT_SOURCE, + pathname: typeof parsed.pathname === "string" && parsed.pathname ? parsed.pathname : "/", + pageUrl: typeof parsed.pageUrl === "string" && parsed.pageUrl ? parsed.pageUrl : env.siteUrl, + startedAt: typeof parsed.startedAt === "string" && parsed.startedAt ? parsed.startedAt : new Date().toISOString(), + } + } catch { + return { + source: VOICE_ASSISTANT_SOURCE, + pathname: "/", + pageUrl: env.siteUrl, + startedAt: new Date().toISOString(), + } + } +} + +function hasExplicitConfirmation(confirmationMessage: string) { + return /\b(yes|yeah|yep|correct|confirm|confirmed|go ahead|submit|sounds good|please do)\b/i.test( + confirmationMessage, + ) +} + +function assertExplicitConfirmation(confirmationMessage: string) { + if (!hasExplicitConfirmation(confirmationMessage)) { + throw new Error("The visitor has not clearly confirmed the submission yet.") + } +} + +async function postJson(pathname: string, payload: TPayload) { + logWorker("api_request_start", { pathname }) + + const response = await fetch(new URL(pathname, env.siteUrl), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }) + + const data = (await response.json().catch(() => ({}))) as ApiSuccess + + if (!response.ok || !data.success) { + throw new Error(data.error || data.message || `Request to ${pathname} failed.`) + } + + logWorker("api_request_success", { pathname }) + return data +} + +function buildContactPayload(args: AssistantContactLeadPayload, userData: WorkerUserData) { + return { + ...args, + source: VOICE_ASSISTANT_SOURCE, + page: userData.pathname, + timestamp: new Date().toISOString(), + url: userData.pageUrl, + } +} + +function buildMachineRequestPayload(args: AssistantMachineRequestPayload, userData: WorkerUserData) { + return { + ...args, + machineType: args.machineType + .split(",") + .map((value) => value.trim()) + .filter(Boolean) + .join(","), + source: VOICE_ASSISTANT_SOURCE, + page: userData.pathname, + timestamp: new Date().toISOString(), + url: userData.pageUrl, + } +} + +function buildConsentMetadata(userData: WorkerUserData) { + return { + consentVersion: SMS_CONSENT_VERSION, + consentCapturedAt: new Date().toISOString(), + consentSourcePage: userData.pathname, + } +} + +function extractConversationItemText(item: unknown): string { + if (typeof item === "string") { + return item.replace(/\s+/g, " ").trim() + } + + if (!item || typeof item !== "object") { + return "" + } + + if ("text" in item && typeof item.text === "string") { + return item.text.replace(/\s+/g, " ").trim() + } + + if ("transcript" in item && typeof item.transcript === "string") { + return item.transcript.replace(/\s+/g, " ").trim() + } + + if ("content" in item) { + const { content } = item as { content?: unknown } + if (Array.isArray(content)) { + return content + .map((part) => extractConversationItemText(part)) + .filter(Boolean) + .join(" ") + .replace(/\s+/g, " ") + .trim() + } + + if (typeof content === "string") { + return content.replace(/\s+/g, " ").trim() + } + } + + return "" +} + +function createTools() { + return { + submit_contact_lead: llm.tool({ + description: + "Submit a general contact or callback request after the visitor clearly confirms the details.", + parameters: z.object({ + firstName: z.string().min(1).describe("Visitor first name"), + lastName: z.string().min(1).describe("Visitor last name"), + email: z.string().email().describe("Visitor email address"), + phone: z.string().min(7).describe("Visitor phone number"), + company: z.string().optional().describe("Visitor company name if they shared one"), + message: z.string().min(10).describe("Short summary of what the visitor needs"), + serviceTextConsent: z + .literal(true) + .describe("Set to true only if the visitor explicitly agrees to service-related SMS follow-up."), + marketingTextConsent: z + .boolean() + .optional() + .describe("Optional marketing SMS consent when the visitor clearly opts in."), + confirmed: z.literal(true).describe("Set to true only after the visitor explicitly confirms submission."), + confirmationMessage: z + .string() + .min(2) + .describe("The visitor's confirmation words, such as 'yes, go ahead and submit that'."), + }), + execute: async ({ confirmed: _confirmed, confirmationMessage, ...args }, { ctx }) => { + assertExplicitConfirmation(confirmationMessage) + const userData = ctx.userData as WorkerUserData + const payload = buildContactPayload( + { + ...args, + ...buildConsentMetadata(userData), + }, + userData, + ) + + logWorker("tool_start", { + tool: "submit_contact_lead", + pathname: userData.pathname, + email: args.email, + }) + + try { + const result = await postJson("/api/contact", payload) + logWorker("tool_success", { + tool: "submit_contact_lead", + pathname: userData.pathname, + email: args.email, + }) + + return { + success: true, + message: + result.message || + "Thanks, your contact request has been sent to Rocky Mountain Vending." + } + } catch (error) { + console.error("[voice-agent] tool_failure", { + tool: "submit_contact_lead", + pathname: userData.pathname, + email: args.email, + error: serializeError(error), + }) + throw error + } + }, + }), + submit_machine_request: llm.tool({ + description: + "Submit a free machine placement or machine request lead after the visitor explicitly confirms and agrees to service-related follow-up.", + parameters: z.object({ + firstName: z.string().min(1).describe("Visitor first name"), + lastName: z.string().min(1).describe("Visitor last name"), + email: z.string().email().describe("Visitor email address"), + phone: z.string().min(7).describe("Visitor phone number"), + company: z.string().min(1).describe("Business or organization name"), + employeeCount: z + .string() + .min(1) + .describe("Approximate employee count or audience size as a plain number string"), + machineType: z + .string() + .min(1) + .describe("Desired machine type or a comma-separated list like snack, beverage, combo"), + machineCount: z.string().min(1).describe("Desired number of machines as a plain number string"), + message: z.string().optional().describe("Optional visitor notes or placement details"), + serviceTextConsent: z + .literal(true) + .describe("Set to true only if the visitor explicitly agrees to service-related SMS follow-up."), + marketingTextConsent: z + .boolean() + .optional() + .describe("Optional marketing SMS consent when the visitor clearly opts in."), + confirmed: z.literal(true).describe("Set to true only after the visitor explicitly confirms submission."), + confirmationMessage: z + .string() + .min(2) + .describe("The visitor's confirmation words, such as 'yes, please submit it'."), + }), + execute: async ({ confirmed: _confirmed, confirmationMessage, ...args }, { ctx }) => { + assertExplicitConfirmation(confirmationMessage) + const userData = ctx.userData as WorkerUserData + + const payload = buildMachineRequestPayload( + { + ...args, + ...buildConsentMetadata(userData), + }, + userData, + ) + + logWorker("tool_start", { + tool: "submit_machine_request", + pathname: userData.pathname, + email: args.email, + company: args.company, + }) + + try { + const result = await postJson("/api/request-machine", payload) + logWorker("tool_success", { + tool: "submit_machine_request", + pathname: userData.pathname, + email: args.email, + company: args.company, + }) + + return { + success: true, + message: + result.message || + "Thanks, your machine request has been sent to Rocky Mountain Vending." + } + } catch (error) { + console.error("[voice-agent] tool_failure", { + tool: "submit_machine_request", + pathname: userData.pathname, + email: args.email, + company: args.company, + error: serializeError(error), + }) + throw error + } + }, + }), + handoff_to_human: llm.tool({ + description: + "Use this when the visitor wants a human right away or asks for something uncertain, legal, pricing-related, or highly specific.", + parameters: z.object({ + reason: z.string().min(2).describe("Short reason for the human handoff"), + }), + execute: async ({ reason }) => { + logWorker("tool_success", { + tool: "handoff_to_human", + reason: truncateText(reason, 120), + }) + + return { + success: true, + message: `${buildHandoffMessage()} Reason: ${reason}`, + } + }, + }), + } +} + +async function startSession(ctx: JobContext) { + const roomName = ctx.room.name || `rmv-voice-${ctx.workerId}` + + logWorker("job_connect_start", { + room: roomName, + workerId: ctx.workerId, + }) + await ctx.connect(undefined, AutoSubscribe.AUDIO_ONLY) + logWorker("job_connect_ready", { + room: roomName, + workerId: ctx.workerId, + }) + + const participant = await ctx.waitForParticipant() + const visitor = parseVisitorMetadata(participant.metadata) + const userData: WorkerUserData = { + siteUrl: env.siteUrl, + pathname: visitor.pathname, + pageUrl: visitor.pageUrl, + startedAt: visitor.startedAt, + } + const persistenceState: VoiceSessionPersistenceState = { + sessionId: null, + recordingId: null, + recordingStatus: persistenceServices.recording ? "pending" : "failed", + recordingUrl: null, + } + + logWorker("participant_joined", { + identity: participant.identity, + room: ctx.room.name, + pathname: userData.pathname, + pageUrl: userData.pageUrl, + }) + + ctx.room.on("trackPublished", (publication, remoteParticipant) => { + if (remoteParticipant.identity !== participant.identity) { + return + } + + try { + publication.setSubscribed(true) + logWorker("remote_track_subscription_requested", { + participantIdentity: remoteParticipant.identity, + source: publication.source, + kind: publication.kind, + trackSid: publication.sid, + }) + } catch (error) { + console.error("[voice-agent] remote_track_subscription_failed", { + participantIdentity: remoteParticipant.identity, + source: publication.source, + kind: publication.kind, + trackSid: publication.sid, + error: serializeError(error), + }) + } + + logWorker("remote_track_published", { + participantIdentity: remoteParticipant.identity, + source: publication.source, + kind: publication.kind, + trackSid: publication.sid, + }) + }) + + ctx.room.on("trackSubscribed", (_track, publication, remoteParticipant) => { + if (remoteParticipant.identity !== participant.identity) { + return + } + + logWorker("remote_track_subscribed", { + participantIdentity: remoteParticipant.identity, + source: publication.source, + kind: publication.kind, + trackSid: publication.sid, + }) + }) + + try { + const sessionId = await createVoiceSessionRecord(persistenceServices, { + roomName, + participantIdentity: participant.identity, + siteUrl: userData.siteUrl, + pathname: userData.pathname, + pageUrl: userData.pageUrl, + source: VOICE_ASSISTANT_SOURCE, + metadata: JSON.stringify(visitor), + recordingDisclosureAt: Date.now(), + recordingStatus: persistenceServices.recording ? "pending" : undefined, + startedAt: Date.parse(userData.startedAt) || Date.now(), + }) + + persistenceState.sessionId = typeof sessionId === "string" ? sessionId : null + logWorker("voice_session_persisted", { + room: ctx.room.name, + participantIdentity: participant.identity, + persisted: Boolean(persistenceState.sessionId), + }) + } catch (error) { + console.error("[voice-agent] session_persistence_failed", { + room: roomName, + participantIdentity: participant.identity, + error: serializeError(error), + }) + } + + const session = new voice.AgentSession({ + llm: new xai.realtime.RealtimeModel({ + apiKey: env.xaiApiKey, + model: env.realtimeModel, + voice: XAI_REALTIME_VOICE, + }), + userData, + turnHandling: { + interruption: { + enabled: true, + }, + }, + }) + + session.on(voice.AgentSessionEventTypes.AgentStateChanged, (event) => { + logWorker("agent_state_changed", { + oldState: event.oldState, + newState: event.newState, + }) + }) + + session.on(voice.AgentSessionEventTypes.UserStateChanged, (event) => { + logWorker("user_state_changed", { + oldState: event.oldState, + newState: event.newState, + }) + }) + + session.on(voice.AgentSessionEventTypes.UserInputTranscribed, (event) => { + logWorker("user_input_transcribed", { + isFinal: event.isFinal, + language: event.language, + transcript: truncateText(event.transcript, 160), + }) + + if (!event.isFinal || !persistenceState.sessionId) { + return + } + + void addVoiceTranscriptTurn(persistenceServices, { + sessionId: persistenceState.sessionId, + roomName, + participantIdentity: participant.identity, + role: "user", + text: event.transcript, + kind: "transcription", + isFinal: event.isFinal, + language: event.language ?? undefined, + }).catch((error) => { + console.error("[voice-agent] transcript_persist_failed", { + role: "user", + error: serializeError(error), + }) + }) + }) + + session.on(voice.AgentSessionEventTypes.ConversationItemAdded, (event) => { + const role = "role" in event.item ? event.item.role : "unknown" + const text = extractConversationItemText(event.item) + + logWorker("conversation_item_added", { + role, + preview: truncateText(JSON.stringify(event.item), 180), + }) + + if (role !== "assistant" || !text || !persistenceState.sessionId) { + return + } + + void addVoiceTranscriptTurn(persistenceServices, { + sessionId: persistenceState.sessionId, + roomName, + participantIdentity: participant.identity, + role: "assistant", + text, + kind: "response", + source: VOICE_ASSISTANT_SOURCE, + }).catch((error) => { + console.error("[voice-agent] transcript_persist_failed", { + role: "assistant", + error: serializeError(error), + }) + }) + }) + + session.on(voice.AgentSessionEventTypes.FunctionToolsExecuted, (event) => { + logWorker("function_tools_executed", { + toolNames: event.functionCalls.map((call) => call.name), + outputs: event.functionCallOutputs.length, + }) + }) + + session.on(voice.AgentSessionEventTypes.SpeechCreated, (event) => { + logWorker("speech_created", { + source: event.source, + userInitiated: event.userInitiated, + }) + }) + + session.on(voice.AgentSessionEventTypes.MetricsCollected, (event) => { + logWorker("metrics_collected", { + metricType: event.metrics.type, + }) + }) + + session.on(voice.AgentSessionEventTypes.Error, (event) => { + console.error("[voice-agent] session_error", { + source: event.source?.constructor?.name || "unknown", + error: serializeError(event.error), + }) + }) + + session.on(voice.AgentSessionEventTypes.Close, (event) => { + logWorker("session_closed", { + reason: event.reason, + error: event.error ? serializeError(event.error) : null, + }) + + void (async () => { + try { + if (persistenceState.recordingId) { + await stopVoiceRecording(persistenceServices, persistenceState.recordingId) + } + } catch (error) { + console.error("[voice-agent] recording_stop_failed", { + recordingId: persistenceState.recordingId, + error: serializeError(error), + }) + } + + if (!persistenceState.sessionId) { + return + } + + try { + await completeVoiceSession(persistenceServices, { + sessionId: persistenceState.sessionId, + endedAt: Date.now(), + recordingId: persistenceState.recordingId ?? undefined, + recordingStatus: persistenceState.recordingId ? "completed" : persistenceState.recordingStatus, + recordingUrl: persistenceState.recordingUrl ?? undefined, + recordingError: event.error ? serializeError(event.error).message : undefined, + }) + } catch (error) { + console.error("[voice-agent] session_complete_failed", { + sessionId: persistenceState.sessionId, + error: serializeError(error), + }) + } + })() + }) + + const agent = new voice.Agent({ + instructions: VOICE_ASSISTANT_SYSTEM_PROMPT, + tools: createTools(), + }) + + logWorker("session_starting", { + room: roomName, + participantIdentity: participant.identity, + pathname: userData.pathname, + model: env.realtimeModel, + }) + await session.start({ agent, room: ctx.room }) + logWorker("session_started", { + room: roomName, + participantIdentity: participant.identity, + }) + + if (persistenceServices.recording && persistenceState.sessionId) { + try { + persistenceState.recordingStatus = "starting" + await updateVoiceRecording(persistenceServices, { + sessionId: persistenceState.sessionId, + recordingStatus: "starting", + }) + + const recording = await startVoiceRecording(persistenceServices, roomName) + if (recording) { + persistenceState.recordingId = recording.recordingId + persistenceState.recordingStatus = recording.recordingStatus + persistenceState.recordingUrl = recording.recordingUrl + + await updateVoiceRecording(persistenceServices, { + sessionId: persistenceState.sessionId, + recordingStatus: recording.recordingStatus, + recordingId: recording.recordingId, + recordingUrl: recording.recordingUrl, + }) + + logWorker("recording_started", { + room: roomName, + participantIdentity: participant.identity, + recordingId: recording.recordingId, + recordingUrl: recording.recordingUrl, + }) + } + } catch (error) { + persistenceState.recordingStatus = "failed" + + console.error("[voice-agent] recording_start_failed", { + room: roomName, + participantIdentity: participant.identity, + error: serializeError(error), + }) + + await updateVoiceRecording(persistenceServices, { + sessionId: persistenceState.sessionId, + recordingStatus: "failed", + recordingError: error instanceof Error ? error.message : String(error), + }).catch((persistError) => { + console.error("[voice-agent] recording_failure_persist_failed", { + error: serializeError(persistError), + }) + }) + } + } +} + +export default defineAgent({ + entry: async (ctx) => { + await startSession(ctx) + }, +}) + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + cli.runApp( + new WorkerOptions({ + agent: fileURLToPath(import.meta.url), + agentName: VOICE_ASSISTANT_AGENT_NAME, + wsURL: env.livekitUrl, + apiKey: env.livekitApiKey, + apiSecret: env.livekitApiSecret, + }), + ) +} diff --git a/package.json b/package.json index 389a840b..9f644294 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,25 @@ { - "name": "rocky-mountain-vending", + "name": "my-v0-project", "version": "0.1.0", "private": true, "type": "module", "scripts": { "build": "next build", + "deploy:staging:env": "node scripts/deploy-readiness.mjs", + "deploy:staging:preflight": "node scripts/deploy-readiness.mjs --build", + "deploy:staging:smoke": "node scripts/staging-smoke.mjs", + "typecheck": "tsc --noEmit", + "manuals:sync:convex": "tsx scripts/sync-manuals-to-convex.ts", + "manuals:sync:convex:dry": "tsx scripts/sync-manuals-to-convex.ts --dry-run", + "convex:codegen": "node -e \"console.log('Convex generated stubs are committed. Run `npx convex dev` or `npx convex codegen` after configuring a deployment to replace them with typed output.')\"", "dev": "next dev", "lint": "eslint .", + "voice-agent:dev": "tsx livekit-agent/worker.ts dev", + "voice-agent:start": "tsx livekit-agent/worker.ts start", + "voice-assistant:test:local": "node scripts/voice-assistant-regression.mjs --base-url http://127.0.0.1:3000 --label local", + "voice-assistant:test:local:voice": "node scripts/voice-assistant-regression.mjs --base-url http://127.0.0.1:3000 --fake-media --label local-voice", + "voice-assistant:test:live": "node scripts/voice-assistant-regression.mjs --base-url https://rockymountainvending.com --fake-media --label live", "start": "next start", - "test": "tsx --test app/api/contact/route.test.ts", "lighthouse:dev": "node scripts/lighthouse-test.js --dev", "lighthouse:build": "node scripts/lighthouse-test.js", "lighthouse:ci": "lighthouse-ci autorun", @@ -24,9 +35,12 @@ "seo:interactive": "node scripts/seo-internal-link-tool.js interactive" }, "dependencies": { - "@aws-sdk/client-sesv2": "^3.888.0", "@aws-sdk/client-s3": "^3.0.0", "@hookform/resolvers": "^3.10.0", + "@livekit/agents": "^1.2.1", + "@livekit/agents-plugin-xai": "^1.2.1", + "@livekit/components-react": "^2.9.20", + "@livekit/components-styles": "^1.2.0", "@radix-ui/react-accordion": "1.2.2", "@radix-ui/react-alert-dialog": "1.1.4", "@radix-ui/react-aspect-ratio": "1.1.1", @@ -55,14 +69,20 @@ "@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-tooltip": "1.1.6", "@stripe/stripe-js": "^4.0.0", + "@supabase/ssr": "^0.8.0", + "@supabase/supabase-js": "^2.95.3", "@vercel/analytics": "latest", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", + "convex": "^1.34.0", "date-fns": "4.1.0", + "dotenv": "^17.3.1", "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", + "livekit-client": "^2.18.0", + "livekit-server-sdk": "^2.15.0", "lucide-react": "^0.454.0", "next": "16.0.0", "next-themes": "^0.4.6", @@ -76,7 +96,6 @@ "stripe": "^17.0.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", - "usesend-js": "^1.6.3", "vaul": "^0.9.9", "zod": "3.25.76" }, @@ -88,9 +107,10 @@ "@types/react-dom": "^19", "chrome-launcher": "^1.1.0", "lighthouse": "^12.0.0", + "playwright": "^1.58.2", "postcss": "^8.5", "tailwindcss": "^4.1.9", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "tw-animate-css": "1.3.3", "typescript": "^5", "wait-on": "^8.0.1", diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..c14b0e0c2ce665d11843a836d49263ca04215c38 GIT binary patch literal 18048 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 literal 0 HcmV?d00001 diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..e5f5cc198dc08ab1221e67ac16551664785b249c GIT binary patch literal 84603 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

mP>7PW{hq$Gb-?o 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 MOkT7`*f8k-0H+6u)Bpeg literal 0 HcmV?d00001 diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..da1ff266d499d166eebf1c3d0fec4b4b074aacab GIT binary patch literal 478 zcmV<40U`d0P)IIM(BO|I1!+!=w21fiY`2YVu!|L?ci7@(T(e|XIH?e}*SOPRpr-@jn$=Y=cWcY;&S{(AM2nURr+ z;qRTQl)tYx8Z$63GJO5{iLve2 zm%Lb+nZKWVaaj`{jsO4uWng4v#*9uPqLGnsQvL@`7tHWP1Tzd2kc@*bFVNip06?+A UoagCl2><{907*qoM6N<$f@!MwaR2}S literal 0 HcmV?d00001 diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..fe443f0e96b564ccfbbd2a17da74274800317849 GIT binary patch literal 1213 zcmV;u1Va0XP)!H5mq%!Mh#JEmCOJRu%w*=9 z@64RJNC*LoDR@z{!x8{k0s#LjfQ3vLUPzER1L^vCk|2cs76h8tLAN2a0Gl4;W-s5X&Z`*c+(xvraFj0+AU0WaS9RY^gBD&s7M zK+Z$4n0^=o!8rta`$==$J*+iv%OOtLxL$A&LbnHiHai;Wd9%iqJ}IQ<3>epa1Pm)4 zoN{ozC~MxIr+?E4{jCSj#jakVzq}&f1c2$Kj(qo*jY)tp2&#pMbKPGjR$u-+JK3C2f8xriy`G>CNeIu%0-2GVF+h?8YV!|vOI>ZHY#?et zBg9!+aVT=*+C0f(5kmbFb7$#MY{ney8%`rb93KbHL7@t~`|aL3pE`Q`6Dx9y27IpN z9>q&+MysY*tT*P|Js<=itluXE<)hLHOa+M{6p$oTsi*IuzRDvH>nyOV@4QIN{rA%w z0+{o|8qxW5p?c-z0=@c{f%X`eGL$(g0=H$vJDMsVDJd-Z?&BQ;eiy;Buz+vM&-LcQ zpw-u|i11!|&Km}*ky1GZtFj+}UavZFAkftwjsj=iU}LeJdf5bdl92z4Vp%)3AN=QS z?w}^8+mq`M13IjDA}d;sJlc4px%9V@UYnkraXuFS0SIFN=0)o1@}r6XPU1Dp1cGQp zu~Fh&9t$M(lvTeZWz;@^GTI!{^0NC7!CV`Y{UV3}uD-c1)fVl^zGK}1!8x`V!p1rV zyEaNb@S2H9)5nrvX$}B_k5}JX{+K1DJ1d|fDp@HJRso0u#3EJpAwhj->i2kmGyyh7 zaX>adSOi8xHriv$n zObw0ctPhd!{VHoGb9xbI=DC+ctqx?$!2A8q=m^*u~d$rAg4kwoNy*vTHCf?JAB!J<8yXPQOpD+ ze2@oSDH7Jq5IvM(?|-i#!5Xi){DbKi23y{Gw&8WRYt)@=l8qyu{8WAR_YtQ&V+@5l zK#(z^o5gN^jcXDHB7hoY02cwBFg`#4hpxiF6bc6FVDVUMz~nbV<8Q`PVmz@UCU=trY@*pFAE1!85FlYZwW!e2Uok8JSY*LV0AL9K bEUx?o#EfjMKHCTA00000NkvXXu0mjfj!iC) literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..eefd183387bdc5cb47fe3d0a9835d365147516c4 GIT binary patch literal 4003 zcmai0WmFRk+a54rH0XqZ;6_YJ%7Ju`5dsoJl#=c)K~hRkRFG08-3>}ejz*+mbfZ$E zlor@NpZ9rxd_TT(zWc}B=l*k@b6qz8Kms5GFfs!E8VrCZ0RZm(lfnM2SAYNjCn*3R zD*A7o3jzR0C;@;tJzaG=YBuV>T{=w-q`}|#&jm&h#a|CSBwGDl`=p6fMhASfv4r?$ zXFh*U)k`2|J!gtqeaCn&;w{Z(UuA4Hih_co7a^SE?K!{SbU?(G3(@#QA9w0CjvD2B z{2C*s>{{+XI{VCgTKC1T;YfMO@n9r`m6bpY^eR~0-#A@@`CkbHWo8lEjO5P<@{IX# zUn4%m0w87-_1Aid24PG01c-!^KLNY*3q+z_%3~?)jZ3>Lp$lT_^UaqGJ9hE3#GgD; z(aW=Uj+t@PlYbbzsV0be6!yK7v|)<8tc~8(!~^2t{>6hO;_~NZU8V+`6>B#eyEBWP zgA-+0L{{h3au`uA^y1<(LNKXXkK5+EV$7dsD3oV-#=*)};PwLiB&q!O^2jH|t!oNidp`d@pHj(af#1YiO^pB z!<|xMW>|`?)S2DW`sAn%nS{E4D>anlIp>o>ai zEG+>5fzhh3#t*yt|AGk6RMSOPs#r$+A5g>oj$!|S+UMD`BLDzc^e@!AMlP;sH88<` zueEt`FHdB%{t@^cTQ+&%BejBhjo5IMc7iFVTqpNS3VpJdTFJXpjGX%4^9YU+Ovvl? zH=$~9MzLC|-_+p18|Cj~n7a+Ks}@g|7G-?~&8bP9ZV=CHu(sYF!yggLA3fGTlDyO4 ziX+N`EKfF{zG>ePv-AY8lXWlMlO{+6D^dE(4hAdzRtiUizeyW38I|(sKU08TBOr~j zHF7jC;=dcN9U$xzFTWCN^uh%pbDFv#M+0g!2 z`s7t--y2-s7qD}b_kbmhWDS|EWJ5b%nsxAH4Z*9mx)3*&0VK^buMqUFG%RCD9tH`e z@9LXTK8s(ngQql6+A&_0w0lU*-2LqN$X5qroUk-@@`+50(?(_2)pbtk^?U|h2Zb_= zf0I=`dM41@VT%6VpE8h*8Nh0EZ*lwl&0&hpq7Z)n7mNF_=Fp{nR{3d~1MwyZZ>cA3 zJ6&>|&h*s5iJ9kfRNi#(=4K~DrjU*HBZt%qefcr3+(ptsAN=Mc_X7PV2M@o{T9+Ij zLr53l1(5Muc)7!gh0$-LA~`EZ+dYxjC0A2T2Ry!6Os9MjMee^VnK1v5@xWIqN}JQ* zGTm}4z$|^Es@(l?^S!r4NC&O1Ad|b3==X^)0_MGW(*-vhYNFL;*_{F{hzuWVzV>L$ zSJz-1%A1fJg8K(Qe$1sIl%5kLl}^sVQE{75k@4#doYs2Yz&qAo(RV7nG}E1a*2Op* zALL3S(jVM{jI?sIXjM{`nQleHt-s(d39U9u@?v*w9KRwzIuzMIIgpm)Ht3&ptoBbL z=Zpjy5UHd5+-196*5x+XLJIhtRf?S=y6#&>bW}ZVF*PwjaSyyr?ltRTVUW}~lWD|w zNl4&n7FhaYlkV0C_@8o1^yWuKnze<@A3wl8F}^hDS-~k+fgSUb`As(}U=G)ld)`;Ut1E}4eXCGTZ-q^->|%+zCiM4V&&35jxGx__ z)yybcPQStIn-0|FJW=j7Km9UUVQOkGj#d&^ZdSadhtt`RX z45mXqAsqQxTxl$zGV#fR4DVTg?Nb6INkRMQ2$?0#C>>&;bf&{!Jk(JoF#?rNgX+|1*%%1 zWG&p&y8C}{lhCH6EyDaom%D&!5L2^>%LVOpn_t+)f+&1omtagop2WE`94OyL*ZmE) zlX?s3w)XzG)0G-;_1MIs6qhm`*Or-pyr#*M2qcgqU744ePWCJRAAb8s>_q>=Z^oqO zF8~0L(|`FbEzA==d82Qn^|ZA?@ss8~3?qyx28K;WRl}I5x%ARWzF14QeT9EQv zvX6IcDK3r-W;A@>Z#f)3&-B@EW`QY30I9C=Xe6wJQIaxdji?9NzEWaLJQ`#Ua=i$n z-U1gnEf(g7xy?^s*h}o?w#8KN%4PU3V%F#;LoTK_YGF+tZM!aSp(HYM>&T8HR$l7D zT@-SXy1e<5O=H?)F^GDvFhan4fCOSGUA=et=JHBovPt2jsEFn5qTB_H%izGT4UgAt zr%vTyJ(=m42#K*d2JfG`>y!`Bhu{npy-$wW^;g^Y6g1Z#A)HF>rasJYoGFn3{v7mn z{)`;pU9H6`U~vZdMTh0;sGlo#K)}fGN8|X8daoe^PyMjfjcMI1-Mzt8X3|iV*q0|K zio!WBGu1xWUc=n>^;cWbodz1`_=f9jk7wT%&d#}dRUqr^zkYTVy4BUp>t#%KbY9Ft zdQDJp0->PTd=0LCJgG}Y|hz1F&tG5l*(ngFV8r|T-D{> z0cbG~6^f<;yjeD@wcmu^CfcK?REx|CzZ=}udD-&2|4-nOx5y#(P^@_a*uKyE(6-|K zx70$EimG|Mj}qFqA1-boe05ahLJ3w@N)BqJjWwdX!Hya#i${%z^8s;3swE6<;3#In z#_;O7GHxmC6M`!-CTDtRC4#YDXwd(s)NbjnXA|NlIM0#>Ob#?F?G4guH^>6SraCZh z$YL;0XnhRx5jj$P1{})O0dn&T^&Y!a$f5Bm16+CLS#syiSH~c+f|YE`iSo0OPaU@U z?`FN=K)mTTKN+>@$bJTtw+VB-a>q$d)UGRPasc(?2XT9G3%_)-OYDgq=e^(GYSg~3 zt_L1H$_pLkE};;uWzxJ0c9$=Xi5A^D9(DbAyo0<3`kESJrSL+ zKK$a2R3YGj?!|9cfA&lm1-jWct9#$pXVQSZ_R_Gw2F$_5_N4?|olAy0zxSP$g_`Yh zKc9S;YH?c`?LeY(wSj8VX}_O+>qvMV@@O-A4$o`&gwX8a z*t&$iC9e8-_Ku65`kk}M44fM&Xlib+;Z|)@lTB3wKj{7j&YI z+dO&y;zCCJqUUmu>CY;@u1rF$dE55QK!?VtO^+cM6kIU6xO~8bx1dm4SV$$+CHJX8+@Njf|zj9BWz0-|MH(PejrGp4p-CIK4GL@Z}E;+rWR+zHb| z^OVOe=K;&7o*Th(DJ`~&3MGWzI-vNdRVs25z$6?-j2Y%1bjfG<#-+4|*X-RlUqn~g zjqr@coHw4lk&&5*1Rx!Cvn`?3!g4!~nK{c?lvgwqQPwASm^b!{L30lL)X{$p={B;` z5jlS{6hbTzY=QI3HtxG%3fn^VN-Aj5Zg_-v1^1QWhK#Wo>az|L{+Z|u5dL z54W$f;T{p`i;=mgZe7k&ov2N*pYe3e4!+ks-A(?&nm3CPO>d}8IwU8aTY8rF3m6+0 z&cPMdNLqFY=q3^W=lIUoO3Q^#S(GY!%c>KZJVOt; zE|kdsf_Lx(a?#1-<*6{}7F)oGaPp8cZxDtC5&; zr;-agpdBA5_dSkq7;~}@r4cALG*_``VEHh8UF z(jD>BgumCOB^=FdExX@r2(^2;Zt}BQR#+16R-n`TmY3rI$|z}CQWjl4*)8zO>u9XT zx#??+C}NeZ!1FbUC`miyK!6VR6(CMx=?EJ}0P?;wM?LQ#@1oH59F=oVc<@GEsyIKN zL-+v`C6$%hJJN}1JTshBNk z#Tef33jH4cG*`s++{=BGV@P-paeNl9kH}rD%p`tbKI%Ol_F`$0z+ZOFZ9c}V2UKG_aERcZKohgL!bnCye{$G6$ U*Co)obx7@fiI;-@?+@jF0EVWH !readValue(key)) + if (missingKeys.length === 0) { + console.log(`${group.label}: ok`) + continue + } + + failures.push(`${group.label} missing: ${missingKeys.join(", ")}`) + console.log(`${group.label}: missing ${missingKeys.join(", ")}`) + } + + for (const group of OPTIONAL_ENV_GROUPS) { + const missingKeys = group.keys.filter((key) => !readValue(key)) + if (missingKeys.length === 0) { + console.log(`${group.label}: ok`) + continue + } + + warnings.push(`${group.label} missing: ${missingKeys.join(", ")}. ${group.note}`) + console.log(`${group.label}: fallback in use`) + } + + const recordingRequested = readValue("VOICE_RECORDING_ENABLED").toLowerCase() + if (recordingRequested === "true" && !hasVoiceRecordingConfig()) { + failures.push( + "Voice recording is enabled but recording storage is incomplete. Set VOICE_RECORDING_BUCKET plus access key, secret, and endpoint env vars.", + ) + } else if (hasVoiceRecordingConfig()) { + console.log("Voice recordings: storage config present") + } else { + warnings.push("Voice recording storage env vars are not configured yet.") + console.log("Voice recordings: storage config missing") + } + + for (const [key, note] of Object.entries(IGNORED_HANDOFF_ENV)) { + if (readValue(key)) { + warnings.push(`${key} is present but ${note}.`) + } + } + + heading("Build") + + if (args.build) { + const buildResult = runShell("pnpm build", { inherit: true }) + if (buildResult.status !== 0) { + failures.push("pnpm build failed.") + } else { + console.log("pnpm build: ok") + } + } else { + console.log("Build skipped. Re-run with --build for full preflight.") + } + + heading("Summary") + + if (warnings.length > 0) { + console.log("Warnings:") + for (const warning of warnings) { + console.log(`- ${warning}`) + } + } else { + console.log("Warnings: none") + } + + if (failures.length > 0) { + console.log("Failures:") + for (const failure of failures) { + console.log(`- ${failure}`) + } + process.exit(1) + } + + console.log("All staging preflight checks passed.") +} + +main() diff --git a/scripts/sync-manuals-to-convex.ts b/scripts/sync-manuals-to-convex.ts new file mode 100644 index 00000000..e8bfed89 --- /dev/null +++ b/scripts/sync-manuals-to-convex.ts @@ -0,0 +1,130 @@ +import { spawnSync } from "node:child_process" +import { config as loadEnv } from "dotenv" +import { ConvexHttpClient } from "convex/browser" +import { makeFunctionReference } from "convex/server" +import { scanManuals } from "../lib/manuals" +import { buildManualAssetUrl, buildThumbnailAssetUrl, getManualsAssetSource } from "../lib/manuals-storage" + +loadEnv({ path: ".env.local" }) +loadEnv({ path: ".env.staging", override: false }) + +type ManualUpsertInput = { + filename: string + path: string + manufacturer: string + category: string + size?: number + lastModified?: number + searchTerms?: string[] + commonNames?: string[] + thumbnailUrl?: string + manualUrl?: string + hasParts?: boolean +} + +const UPSERT_MANUALS = makeFunctionReference<"mutation">("manuals:upsertMany") + +function parseArgs(argv: string[]) { + const limitFlagIndex = argv.indexOf("--limit") + const limit = limitFlagIndex >= 0 ? Number.parseInt(argv[limitFlagIndex + 1] || "", 10) : undefined + + return { + dryRun: argv.includes("--dry-run"), + skipUpload: argv.includes("--skip-upload"), + limit: Number.isFinite(limit) && limit && limit > 0 ? limit : undefined, + } +} + +function readConvexUrl() { + const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_URL + if (!convexUrl) { + throw new Error("NEXT_PUBLIC_CONVEX_URL is required to sync manuals into Convex.") + } + + return convexUrl +} + +function shouldUploadAssets() { + return Boolean(process.env.CLOUDFLARE_R2_ACCESS_KEY_ID && process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY) +} + +function runAssetUpload() { + const result = spawnSync("node", ["scripts/upload-to-r2.js", "--type", "all", "--incremental"], { + cwd: process.cwd(), + stdio: "inherit", + }) + + if ((result.status ?? 1) !== 0) { + throw new Error("R2 upload step failed.") + } +} + +function normalizeManualForConvex(manual: Awaited>[number]) { + const manualUrl = buildManualAssetUrl(manual.path) + const thumbnailUrl = manual.thumbnailUrl ? buildThumbnailAssetUrl(manual.thumbnailUrl) : undefined + + return { + filename: manual.filename, + path: manual.path, + manufacturer: manual.manufacturer, + category: manual.category, + size: manual.size, + lastModified: manual.lastModified ? manual.lastModified.getTime() : undefined, + searchTerms: manual.searchTerms, + commonNames: manual.commonNames, + thumbnailUrl, + manualUrl, + hasParts: Boolean(manual.searchTerms?.some((term) => term.toLowerCase().includes("parts"))), + } satisfies ManualUpsertInput +} + +async function main() { + const args = parseArgs(process.argv.slice(2)) + + if (!args.skipUpload && shouldUploadAssets()) { + console.log("[manuals-sync] uploading new manuals and thumbnails to object storage") + runAssetUpload() + } else if (!args.skipUpload) { + console.log("[manuals-sync] skipping asset upload because Cloudflare R2 credentials are not configured") + } + + const manuals = await scanManuals() + const selectedManuals = args.limit ? manuals.slice(0, args.limit) : manuals + const importBatch = new Date().toISOString() + const payload = selectedManuals.map((manual) => normalizeManualForConvex(manual)) + + console.log("[manuals-sync] scanned manuals", { + total: manuals.length, + selected: selectedManuals.length, + assetSource: getManualsAssetSource(), + }) + + if (args.dryRun) { + console.log("[manuals-sync] dry run complete") + return + } + + const convex = new ConvexHttpClient(readConvexUrl()) + const batchSize = 100 + let synced = 0 + + for (let index = 0; index < payload.length; index += batchSize) { + const slice = payload.slice(index, index + batchSize) + await convex.mutation(UPSERT_MANUALS, { manuals: slice }) + synced += slice.length + console.log("[manuals-sync] upserted batch", { + synced, + total: payload.length, + }) + } + + console.log("[manuals-sync] completed", { + synced, + importBatch, + }) +} + +main().catch((error) => { + console.error("[manuals-sync] failed", error) + process.exit(1) +}) diff --git a/scripts/upload-to-r2.js b/scripts/upload-to-r2.js index 048a0ad9..5431bc51 100644 --- a/scripts/upload-to-r2.js +++ b/scripts/upload-to-r2.js @@ -22,16 +22,17 @@ import { existsSync } from 'fs'; const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID || 'bd6f76304a840ba11b75f9ced84264f4'; const ENDPOINT = process.env.CLOUDFLARE_R2_ENDPOINT || `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`; -const ACCESS_KEY_ID = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID; -const SECRET_ACCESS_KEY = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY; +const ACCESS_KEY_ID = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY; +const SECRET_ACCESS_KEY = + process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY || process.env.AWS_SECRET_KEY; // Bucket names const MANUALS_BUCKET = process.env.R2_MANUALS_BUCKET || 'vending-vm-manuals'; const THUMBNAILS_BUCKET = process.env.R2_THUMBNAILS_BUCKET || 'vending-vm-thumbnails'; // Source directories (relative to project root) -const MANUALS_SOURCE = join(process.cwd(), '..', 'manuals'); -const THUMBNAILS_SOURCE = join(process.cwd(), '..', 'thumbnails'); +const MANUALS_SOURCE = join(process.cwd(), '..', 'manuals-data', 'manuals'); +const THUMBNAILS_SOURCE = join(process.cwd(), '..', 'manuals-data', 'thumbnails'); // Parse command line arguments const args = process.argv.slice(2); @@ -261,5 +262,3 @@ main().catch(error => { console.error('\n❌ Fatal error:', error); process.exit(1); }); - -