From b252276f066ae3aa75c43cd75046fa85fabfa973 Mon Sep 17 00:00:00 2001 From: v0 Date: Tue, 26 Aug 2025 07:05:49 +0000 Subject: [PATCH 1/4] feat: add dev auto-login route for admin access Create development-only auto-login for admin privileges. Co-authored-by: Steve Kabore <26856433+githubtic@users.noreply.github.com> --- .gitignore | 27 + LICENSE | 21 - app/admin/investors/page.tsx | 162 + app/admin/layout.tsx | 33 + app/admin/overview/page.tsx | 85 + app/admin/page.tsx | 225 ++ app/admin/patterns/new/page.tsx | 555 +++ app/admin/patterns/page.tsx | 105 + app/api/audio/analyze/route.ts | 77 + app/api/audio/execute-action/route.ts | 69 + app/api/auth/bypass-confirmation/route.ts | 54 + app/api/auth/dev-login/route.ts | 85 + app/api/python/run-script/route.ts | 92 + app/api/waveform/[filename]/route.ts | 48 + app/dashboard/layout.tsx | 31 + app/dashboard/page.tsx | 76 + app/dashboard/pattern-detector/page.tsx | 530 +++ app/dev-login/page.tsx | 95 + app/forgot-password/page.tsx | 95 + app/globals.css | 90 + app/layout.tsx | 32 + app/login/page.tsx | 258 ++ app/page.tsx | 191 + app/register/page.tsx | 170 + app/reset-password/page.tsx | 129 + components.json | 21 + components/admin/admin-layout.tsx | 113 + components/admin/dashboard-stats.tsx | 152 + components/admin/recent-activity.tsx | 219 ++ components/device-connector.tsx | 390 ++ components/event-matcher.tsx | 358 ++ components/investment-form.tsx | 175 + components/layout/app-layout.tsx | 94 + components/layout/app-sidebar.tsx | 272 ++ components/layout/dashboard-header.tsx | 152 + components/pattern-detector.tsx | 458 +++ components/sound-wave-labeler.tsx | 496 +++ components/theme-provider.tsx | 11 + components/ui/alert.tsx | 59 + components/ui/avatar.tsx | 50 + components/ui/badge.tsx | 36 + components/ui/button.tsx | 56 + components/ui/card.tsx | 79 + components/ui/charts.tsx | 250 ++ components/ui/dropdown-menu.tsx | 200 + components/ui/input.tsx | 22 + components/ui/label.tsx | 26 + components/ui/progress.tsx | 28 + components/ui/scroll-area.tsx | 48 + components/ui/select.tsx | 160 + components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 + components/ui/sidebar.tsx | 763 ++++ components/ui/skeleton.tsx | 15 + components/ui/slider.tsx | 28 + components/ui/switch.tsx | 29 + components/ui/table.tsx | 117 + components/ui/tabs.tsx | 55 + components/ui/textarea.tsx | 22 + components/ui/toast.tsx | 129 + components/ui/toaster.tsx | 35 + components/ui/tooltip.tsx | 30 + hooks/use-mobile.tsx | 25 + hooks/use-toast.ts | 194 + lib/auth-utils.ts | 84 + lib/auth.ts | 73 + lib/database.types | 255 ++ lib/database.types.ts | 255 ++ lib/sound-pattern-matcher.ts | 140 + lib/supabase/client.ts | 42 + lib/supabase/server.ts | 21 + lib/utils.ts | 6 + next.config.mjs | 14 + package.json | 78 + pnpm-lock.yaml | 4002 +++++++++++++++++++++ postcss.config.mjs | 8 + public/placeholder-logo.png | Bin 0 -> 568 bytes public/placeholder-logo.svg | 1 + public/placeholder-user.jpg | Bin 0 -> 1635 bytes public/placeholder.jpg | Bin 0 -> 1064 bytes public/placeholder.svg | 1 + styles/globals.css | 90 + tailwind.config.js | 77 + tsconfig.json | 27 + 84 files changed, 14026 insertions(+), 21 deletions(-) create mode 100644 .gitignore delete mode 100644 LICENSE create mode 100644 app/admin/investors/page.tsx create mode 100644 app/admin/layout.tsx create mode 100644 app/admin/overview/page.tsx create mode 100644 app/admin/page.tsx create mode 100644 app/admin/patterns/new/page.tsx create mode 100644 app/admin/patterns/page.tsx create mode 100644 app/api/audio/analyze/route.ts create mode 100644 app/api/audio/execute-action/route.ts create mode 100644 app/api/auth/bypass-confirmation/route.ts create mode 100644 app/api/auth/dev-login/route.ts create mode 100644 app/api/python/run-script/route.ts create mode 100644 app/api/waveform/[filename]/route.ts create mode 100644 app/dashboard/layout.tsx create mode 100644 app/dashboard/page.tsx create mode 100644 app/dashboard/pattern-detector/page.tsx create mode 100644 app/dev-login/page.tsx create mode 100644 app/forgot-password/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/login/page.tsx create mode 100644 app/page.tsx create mode 100644 app/register/page.tsx create mode 100644 app/reset-password/page.tsx create mode 100644 components.json create mode 100644 components/admin/admin-layout.tsx create mode 100644 components/admin/dashboard-stats.tsx create mode 100644 components/admin/recent-activity.tsx create mode 100644 components/device-connector.tsx create mode 100644 components/event-matcher.tsx create mode 100644 components/investment-form.tsx create mode 100644 components/layout/app-layout.tsx create mode 100644 components/layout/app-sidebar.tsx create mode 100644 components/layout/dashboard-header.tsx create mode 100644 components/pattern-detector.tsx create mode 100644 components/sound-wave-labeler.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/charts.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 hooks/use-mobile.tsx create mode 100644 hooks/use-toast.ts create mode 100644 lib/auth-utils.ts create mode 100644 lib/auth.ts create mode 100644 lib/database.types create mode 100644 lib/database.types.ts create mode 100644 lib/sound-pattern-matcher.ts create mode 100644 lib/supabase/client.ts create mode 100644 lib/supabase/server.ts create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 public/placeholder-logo.png create mode 100644 public/placeholder-logo.svg create mode 100644 public/placeholder-user.jpg create mode 100644 public/placeholder.jpg create mode 100644 public/placeholder.svg create mode 100644 styles/globals.css create mode 100644 tailwind.config.js create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c5ee0da..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Steve Kabore - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/app/admin/investors/page.tsx b/app/admin/investors/page.tsx new file mode 100644 index 0000000..1495d8c --- /dev/null +++ b/app/admin/investors/page.tsx @@ -0,0 +1,162 @@ +import { Suspense } from "react" +import { redirect } from "next/navigation" +import Link from "next/link" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { isAdmin } from "@/lib/auth" +import { createServerSupabaseClient } from "@/lib/supabase/server" +import AdminLayout from "@/components/admin/admin-layout" + +async function InvestorsContent() { + // Check if user is admin + const admin = await isAdmin() + if (!admin) { + redirect("/login") + } + + const supabase = createServerSupabaseClient() + + // Fetch investors + const { data: investors } = await supabase.from("investors").select("*").order("created_at", { ascending: false }) + + return ( +
+
+
+

Investors

+

Manage investor information and track investment status

+
+
+ + + +
+
+ + + + Investor List + All investors who have expressed interest in SonicReactor + + +
+ + + + + + + + + + + + + + {investors?.map((investor) => ( + + + + + + + + + + ))} + {(!investors || investors.length === 0) && ( + + + + )} + +
NameEmailPhoneAmountStatusDateActions
{investor.name}{investor.email}{investor.phone || "N/A"}${investor.amount.toLocaleString()} + + {investor.status.charAt(0).toUpperCase() + investor.status.slice(1)} + + {new Date(investor.created_at).toLocaleDateString()} + + + +
+ No investors found +
+
+
+
+ + + + Investment Summary + Overview of investment interest by status + + +
+
+
Pending
+
+ $ + {investors + ?.filter((i) => i.status === "pending") + .reduce((sum, i) => sum + i.amount, 0) + .toLocaleString() || "0"} +
+
+ {investors?.filter((i) => i.status === "pending").length || 0} investors +
+
+ +
+
Approved
+
+ $ + {investors + ?.filter((i) => i.status === "approved") + .reduce((sum, i) => sum + i.amount, 0) + .toLocaleString() || "0"} +
+
+ {investors?.filter((i) => i.status === "approved").length || 0} investors +
+
+ +
+
Rejected
+
+ $ + {investors + ?.filter((i) => i.status === "rejected") + .reduce((sum, i) => sum + i.amount, 0) + .toLocaleString() || "0"} +
+
+ {investors?.filter((i) => i.status === "rejected").length || 0} investors +
+
+
+
+
+
+ ) +} + +export default function InvestorsPage() { + return ( + + Loading investors...}> + + + + ) +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..ccd8f7c --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,33 @@ +import type React from "react" +import { Suspense } from "react" +import { redirect } from "next/navigation" +import { getCurrentUser, isAdmin } from "@/lib/auth" +import { DashboardHeader } from "@/components/layout/dashboard-header" + +async function AdminLayoutContent({ children }: { children: React.ReactNode }) { + // Check if user is authenticated and is admin + const user = await getCurrentUser() + if (!user) { + redirect("/login") + } + + const userIsAdmin = await isAdmin() + if (!userIsAdmin) { + redirect("/dashboard") + } + + return ( +
+ +
{children}
+
+ ) +} + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return ( + Loading admin panel...}> + {children} + + ) +} diff --git a/app/admin/overview/page.tsx b/app/admin/overview/page.tsx new file mode 100644 index 0000000..465a6cc --- /dev/null +++ b/app/admin/overview/page.tsx @@ -0,0 +1,85 @@ +import type { Metadata } from "next" +import { DashboardStats } from "@/components/admin/dashboard-stats" +import { RecentActivity } from "@/components/admin/recent-activity" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { ArrowRight, Users, AudioWaveformIcon as Waveform, Cpu, Settings } from "lucide-react" +import Link from "next/link" + +export const metadata: Metadata = { + title: "Admin Overview | SonicReactor", + description: "System overview and statistics for SonicReactor administrators", +} + +export default function AdminOverviewPage() { + return ( +
+
+
+

System Overview

+

Monitor system performance and recent activity

+
+
+ +
+ + +
+ + +
+ + + Quick Actions + Common administrative tasks + + +
+ + + + +

Manage Investors

+

View and manage investor accounts

+
+
+ + + + + + +

Sound Patterns

+

Manage sound pattern library

+
+
+ + + + + + +

Device Management

+

Monitor and configure devices

+
+
+ +
+ +
+ + + +
+
+
+
+
+
+
+ ) +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..6ba6236 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,225 @@ +import { Suspense } from "react" +import { redirect } from "next/navigation" +import Link from "next/link" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { LineChart, PieChart } from "@/components/ui/charts" +import { isAdmin } from "@/lib/auth" +import { createServerSupabaseClient } from "@/lib/supabase/server" +import AdminLayout from "@/components/admin/admin-layout" + +async function AdminDashboardContent() { + // Check if user is admin + const admin = await isAdmin() + if (!admin) { + redirect("/login") + } + + const supabase = createServerSupabaseClient() + + // Fetch dashboard data + const { data: investorsCount } = await supabase.from("investors").select("*", { count: "exact", head: true }) + + const { data: patternsCount } = await supabase.from("sound_patterns").select("*", { count: "exact", head: true }) + + const { data: devicesCount } = await supabase.from("devices").select("*", { count: "exact", head: true }) + + const { data: detectionsCount } = await supabase.from("detection_history").select("*", { count: "exact", head: true }) + + // Fetch recent detections + const { data: recentDetections } = await supabase + .from("detection_history") + .select(` + id, + confidence, + action_executed, + detected_at, + sound_patterns (label), + devices (name) + `) + .order("detected_at", { ascending: false }) + .limit(5) + + return ( +
+
+
+

Admin Dashboard

+

Overview of your SonicReactor system and analytics

+
+
+ + + +
+
+ +
+ + + Total Investors + + + + + + + +
{investorsCount?.count || 0}
+

+10% from last month

+
+
+ + + Sound Patterns + + + + + +
{patternsCount?.count || 0}
+

+12 new patterns this week

+
+
+ + + Connected Devices + + + + + + +
{devicesCount?.count || 0}
+

+3 devices since last week

+
+
+ + + Pattern Detections + + + + + +
{detectionsCount?.count || 0}
+

+24% from last month

+
+
+
+ +
+ + + Detection Analytics + Pattern detection frequency over the past 30 days + + +
+ +
+
+
+ + + Pattern Distribution + Breakdown of detected sound patterns + + +
+ +
+
+
+
+ + + + Recent Detections + Latest sound pattern detections across your devices + + +
+ + + + + + + + + + + + {recentDetections?.map((detection) => ( + + + + + + + + ))} + {(!recentDetections || recentDetections.length === 0) && ( + + + + )} + +
PatternDeviceConfidenceAction ExecutedDetected At
{detection.sound_patterns?.label || "Unknown"}{detection.devices?.name || "Unknown"} + {detection.confidence ? `${(detection.confidence * 100).toFixed(1)}%` : "N/A"} + {detection.action_executed ? "Yes" : "No"}{new Date(detection.detected_at).toLocaleString()}
+ No recent detections found +
+
+
+
+
+ ) +} + +export default function AdminDashboard() { + return ( + + Loading dashboard...}> + + + + ) +} diff --git a/app/admin/patterns/new/page.tsx b/app/admin/patterns/new/page.tsx new file mode 100644 index 0000000..e765af8 --- /dev/null +++ b/app/admin/patterns/new/page.tsx @@ -0,0 +1,555 @@ +"use client" + +import type React from "react" + +import { useState, useRef } from "react" +import { useRouter } from "next/navigation" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Switch } from "@/components/ui/switch" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Mic, Upload, Save, Play, Square } from "lucide-react" +import { supabase } from "@/lib/supabase/client" +import AdminLayout from "@/components/admin/admin-layout" + +export default function NewPatternPage() { + const router = useRouter() + const [activeTab, setActiveTab] = useState("record") + const [label, setLabel] = useState("") + const [description, setDescription] = useState("") + const [processCode, setProcessCode] = useState("") + const [isActive, setIsActive] = useState(true) + const [isRecording, setIsRecording] = useState(false) + const [isPlaying, setIsPlaying] = useState(false) + const [audioBlob, setAudioBlob] = useState(null) + const [audioUrl, setAudioUrl] = useState(null) + const [uploadedFile, setUploadedFile] = useState(null) + const [error, setError] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + const [recordingDuration, setRecordingDuration] = useState(0) + + const canvasRef = useRef(null) + const mediaRecorderRef = useRef(null) + const audioContextRef = useRef(null) + const analyserRef = useRef(null) + const dataArrayRef = useRef(null) + const audioChunksRef = useRef([]) + const recordingTimerRef = useRef(null) + const audioElementRef = useRef(null) + const fileInputRef = useRef(null) + + // Initialize audio element for playback + if (typeof window !== "undefined" && !audioElementRef.current) { + audioElementRef.current = new Audio() + } + + const startRecording = async () => { + try { + // Reset previous recording data + audioChunksRef.current = [] + setAudioBlob(null) + setAudioUrl(null) + setRecordingDuration(0) + + // Check if MediaDevices API is supported + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + throw new Error("Media devices API not supported in this browser") + } + + // Try to get the audio stream + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + + // Set up audio context and analyser + audioContextRef.current = new AudioContext() + analyserRef.current = audioContextRef.current.createAnalyser() + analyserRef.current.fftSize = 256 + + const source = audioContextRef.current.createMediaStreamSource(stream) + source.connect(analyserRef.current) + + const bufferLength = analyserRef.current.frequencyBinCount + dataArrayRef.current = new Uint8Array(bufferLength) + + // Set up media recorder with audio/webm MIME type + mediaRecorderRef.current = new MediaRecorder(stream, { + mimeType: "audio/webm;codecs=opus", + }) + + // Set up event handlers for the media recorder + mediaRecorderRef.current.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunksRef.current.push(event.data) + } + } + + mediaRecorderRef.current.onstop = () => { + // Create a blob from all the chunks + const audioBlob = new Blob(audioChunksRef.current, { type: "audio/webm" }) + setAudioBlob(audioBlob) + + // Create a URL for the blob + const audioUrl = URL.createObjectURL(audioBlob) + setAudioUrl(audioUrl) + + // Set up the audio element for playback + if (audioElementRef.current) { + audioElementRef.current.src = audioUrl + } + } + + // Start recording + mediaRecorderRef.current.start(100) // Collect data every 100ms + + setIsRecording(true) + + // Start visualizing + visualize() + + // Set up a timer to track recording duration + let duration = 0 + recordingTimerRef.current = setInterval(() => { + duration += 0.1 + setRecordingDuration(duration) + }, 100) + } catch (error) { + console.error("Error accessing microphone:", error) + setError(error instanceof Error ? error.message : "Failed to access microphone") + } + } + + const stopRecording = () => { + if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") { + mediaRecorderRef.current.stop() + + // Stop all tracks in the stream + mediaRecorderRef.current.stream.getTracks().forEach((track) => track.stop()) + + setIsRecording(false) + + if (recordingTimerRef.current) { + clearInterval(recordingTimerRef.current) + } + } + } + + const playAudio = () => { + if (audioElementRef.current && (audioUrl || uploadedFile)) { + audioElementRef.current.play() + setIsPlaying(true) + + audioElementRef.current.onended = () => { + setIsPlaying(false) + } + } + } + + const stopAudio = () => { + if (audioElementRef.current) { + audioElementRef.current.pause() + audioElementRef.current.currentTime = 0 + setIsPlaying(false) + } + } + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + // Check if file is an audio file + if (!file.type.startsWith("audio/")) { + setError("Please upload an audio file") + return + } + + setUploadedFile(file) + + // Create a URL for the file + const fileUrl = URL.createObjectURL(file) + setAudioUrl(fileUrl) + + // Set up the audio element for playback + if (audioElementRef.current) { + audioElementRef.current.src = fileUrl + + // Get duration when metadata is loaded + audioElementRef.current.onloadedmetadata = () => { + if (audioElementRef.current) { + setRecordingDuration(audioElementRef.current.duration) + } + } + } + + // Clear any previous recording + setAudioBlob(null) + } + } + + const triggerFileInput = () => { + fileInputRef.current?.click() + } + + const visualize = () => { + if (!canvasRef.current || !analyserRef.current || !dataArrayRef.current) return + + const canvas = canvasRef.current + const canvasCtx = canvas.getContext("2d") + if (!canvasCtx) return + + const width = canvas.width + const height = canvas.height + + const draw = () => { + requestAnimationFrame(draw) + + analyserRef.current!.getByteTimeDomainData(dataArrayRef.current!) + + canvasCtx.fillStyle = "rgb(240, 240, 240)" + canvasCtx.fillRect(0, 0, width, height) + + canvasCtx.lineWidth = 2 + canvasCtx.strokeStyle = "rgb(0, 125, 255)" + canvasCtx.beginPath() + + const sliceWidth = width / dataArrayRef.current!.length + let x = 0 + + for (let i = 0; i < dataArrayRef.current!.length; i++) { + const v = dataArrayRef.current![i] / 128.0 + const y = (v * height) / 2 + + if (i === 0) { + canvasCtx.moveTo(x, y) + } else { + canvasCtx.lineTo(x, y) + } + + x += sliceWidth + } + + canvasCtx.lineTo(width, height / 2) + canvasCtx.stroke() + } + + draw() + } + + const renderStaticWaveform = () => { + if (!canvasRef.current) return + + const canvas = canvasRef.current + const canvasCtx = canvas.getContext("2d") + if (!canvasCtx) return + + const width = canvas.width + const height = canvas.height + + // Clear the canvas + canvasCtx.fillStyle = "rgb(240, 240, 240)" + canvasCtx.fillRect(0, 0, width, height) + + // Draw a flat line + canvasCtx.lineWidth = 2 + canvasCtx.strokeStyle = "rgb(180, 180, 180)" + canvasCtx.beginPath() + canvasCtx.moveTo(0, height / 2) + canvasCtx.lineTo(width, height / 2) + canvasCtx.stroke() + + // Add text + canvasCtx.font = "14px Arial" + canvasCtx.fillStyle = "rgb(100, 100, 100)" + canvasCtx.textAlign = "center" + + if (activeTab === "record") { + canvasCtx.fillText('Click "Start Recording" to record a sound pattern', width / 2, height / 2 - 20) + } else { + canvasCtx.fillText("Upload an audio file to visualize the waveform", width / 2, height / 2 - 20) + } + } + + // Initialize the canvas with static visualization + if (typeof window !== "undefined" && canvasRef.current) { + renderStaticWaveform() + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + + if (!label) { + setError("Pattern label is required") + return + } + + if (!audioBlob && !uploadedFile) { + setError("Please record or upload an audio file") + return + } + + setIsSubmitting(true) + + try { + // Get current user + const { + data: { user }, + } = await supabase.auth.getUser() + + if (!user) { + throw new Error("You must be logged in to create a pattern") + } + + // Upload the audio file to storage + const audioFile = audioBlob || uploadedFile + const fileExt = audioBlob ? "webm" : uploadedFile?.name.split(".").pop() || "wav" + const filePath = `patterns/${user.id}/${Date.now()}.${fileExt}` + + const { data: uploadData, error: uploadError } = await supabase.storage.from("audio").upload(filePath, audioFile!) + + if (uploadError) { + throw uploadError + } + + // Get the public URL for the uploaded file + const { + data: { publicUrl }, + } = supabase.storage.from("audio").getPublicUrl(filePath) + + // Create the sound pattern record + const { data: pattern, error: patternError } = await supabase.from("sound_patterns").insert({ + label, + description, + process_code: processCode || null, + audio_url: publicUrl, + duration: recordingDuration, + is_active: isActive, + created_by: user.id, + }) + + if (patternError) { + throw patternError + } + + // Generate a waveform image (in a real implementation) + // This would call a server function to generate a waveform image + + // Success! Redirect to the patterns list + router.push("/admin/patterns") + } catch (error) { + console.error("Error creating pattern:", error) + setError(error instanceof Error ? error.message : "Failed to create pattern") + setIsSubmitting(false) + } + } + + return ( + +
+
+

Add New Sound Pattern

+

Record or upload a sound pattern for detection

+
+ +
+
+ + + Pattern Information + Enter details about the sound pattern + + +
+ + setLabel(e.target.value)} + required + /> +
+ +
+ +