mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat: added help page and route
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Resend } from 'resend'
|
||||
import { z } from 'zod'
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
// Define schema for validation
|
||||
const helpFormSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
subject: z.string().min(1, 'Subject is required'),
|
||||
message: z.string().min(1, 'Message is required'),
|
||||
type: z.enum(['bug', 'feedback', 'feature_request', 'other']),
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Handle multipart form data
|
||||
const formData = await req.formData()
|
||||
|
||||
// Extract form fields
|
||||
const email = formData.get('email') as string
|
||||
const subject = formData.get('subject') as string
|
||||
const message = formData.get('message') as string
|
||||
const type = formData.get('type') as string
|
||||
|
||||
// Validate the form data
|
||||
const result = helpFormSchema.safeParse({
|
||||
email,
|
||||
subject,
|
||||
message,
|
||||
type,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: result.error.format() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Extract images
|
||||
const images: { filename: string; content: Buffer; contentType: string }[] = []
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key.startsWith('image_') && value instanceof Blob) {
|
||||
const file = value as File
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
images.push({
|
||||
filename: file.name,
|
||||
content: buffer,
|
||||
contentType: file.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare email content
|
||||
let emailText = `
|
||||
Type: ${type}
|
||||
From: ${email}
|
||||
|
||||
${message}
|
||||
`
|
||||
|
||||
if (images.length > 0) {
|
||||
emailText += `\n\n${images.length} image(s) attached.`
|
||||
}
|
||||
|
||||
// Send email using Resend
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: 'Sim Studio <noreply@simstudio.ai>',
|
||||
to: ['help@simstudio.ai'],
|
||||
subject: `[${type.toUpperCase()}] ${subject}`,
|
||||
replyTo: email,
|
||||
text: emailText,
|
||||
attachments: images.map((image) => ({
|
||||
filename: image.filename,
|
||||
content: image.content.toString('base64'),
|
||||
contentType: image.contentType,
|
||||
disposition: 'attachment', // Explicitly set as attachment
|
||||
})),
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('Error sending email:', error)
|
||||
return NextResponse.json({ error: 'Failed to send email' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Send confirmation email to the user
|
||||
await resend.emails
|
||||
.send({
|
||||
from: 'Sim Studio <noreply@simstudio.ai>',
|
||||
to: [email],
|
||||
subject: `Your ${type} request has been received: ${subject}`,
|
||||
text: `
|
||||
Hello,
|
||||
|
||||
Thank you for your ${type} submission. We've received your request and will get back to you as soon as possible.
|
||||
|
||||
Your message:
|
||||
${message}
|
||||
|
||||
${images.length > 0 ? `You attached ${images.length} image(s).` : ''}
|
||||
|
||||
Best regards,
|
||||
The Sim Studio Team
|
||||
`,
|
||||
replyTo: 'help@simstudio.ai',
|
||||
})
|
||||
.catch((err) => {
|
||||
// Log but don't fail if confirmation email fails
|
||||
console.warn('Failed to send confirmation email:', err)
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: true, message: 'Help request submitted successfully' },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error processing help request:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import clsx from 'clsx'
|
||||
import { Plus, ScrollText, Settings } from 'lucide-react'
|
||||
import { HelpCircle, Plus, ScrollText, Settings } from 'lucide-react'
|
||||
import { AgentIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
@@ -102,6 +102,29 @@ export function Sidebar() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Logs</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
asChild
|
||||
className={clsx(
|
||||
'flex !h-9 !w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8',
|
||||
{
|
||||
'bg-accent': pathname === '/w/help',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Link href="/w/help">
|
||||
<HelpCircle className="!h-5 !w-5" />
|
||||
<span className="sr-only">Help & Support</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Help & Support</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import imageCompression from 'browser-image-compression'
|
||||
import { AlertCircle, CheckCircle2, Image as ImageIcon, Upload, X } from 'lucide-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
// Define form schema
|
||||
const formSchema = z.object({
|
||||
email: z.string().email('Please enter a valid email address'),
|
||||
subject: z.string().min(1, 'Subject is required'),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||
type: z.enum(['bug', 'feedback', 'feature_request', 'other'], {
|
||||
required_error: 'Please select a request type',
|
||||
}),
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>
|
||||
|
||||
// Increased maximum upload size to 20MB
|
||||
const MAX_FILE_SIZE = 20 * 1024 * 1024
|
||||
// Target size after compression (2MB)
|
||||
const TARGET_SIZE_MB = 2
|
||||
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']
|
||||
|
||||
interface ImageWithPreview extends File {
|
||||
preview: string
|
||||
}
|
||||
|
||||
export default function HelpForm() {
|
||||
const router = useRouter()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [submitStatus, setSubmitStatus] = useState<'success' | 'error' | null>(null)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [images, setImages] = useState<ImageWithPreview[]>([])
|
||||
const [imageError, setImageError] = useState<string | null>(null)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
type: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
// Clean up object URLs when component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
images.forEach((image) => URL.revokeObjectURL(image.preview))
|
||||
}
|
||||
}, [images])
|
||||
|
||||
const compressImage = async (file: File): Promise<File> => {
|
||||
// Skip compression for small files or GIFs (which don't compress well)
|
||||
if (file.size < TARGET_SIZE_MB * 1024 * 1024 || file.type === 'image/gif') {
|
||||
return file
|
||||
}
|
||||
|
||||
const options = {
|
||||
maxSizeMB: TARGET_SIZE_MB,
|
||||
maxWidthOrHeight: 1920,
|
||||
useWebWorker: true,
|
||||
fileType: file.type,
|
||||
// Ensure we maintain proper file naming and MIME types
|
||||
initialQuality: 0.8,
|
||||
alwaysKeepResolution: true,
|
||||
}
|
||||
|
||||
try {
|
||||
const compressedFile = await imageCompression(file, options)
|
||||
|
||||
// Create a new File object with the original name and type to ensure compatibility
|
||||
return new File([compressedFile], file.name, {
|
||||
type: file.type,
|
||||
lastModified: new Date().getTime(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Image compression failed, using original file:', error)
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setImageError(null)
|
||||
const files = e.target.files
|
||||
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
const newImages: ImageWithPreview[] = []
|
||||
let hasError = false
|
||||
|
||||
for (const file of Array.from(files)) {
|
||||
// Check file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
setImageError(`File ${file.name} is too large. Maximum size is 20MB.`)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Check file type
|
||||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
||||
setImageError(
|
||||
`File ${file.name} has an unsupported format. Please use JPEG, PNG, WebP, or GIF.`
|
||||
)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Compress the image (behind the scenes)
|
||||
const compressedFile = await compressImage(file)
|
||||
|
||||
// Create preview URL
|
||||
const imageWithPreview = Object.assign(compressedFile, {
|
||||
preview: URL.createObjectURL(compressedFile),
|
||||
}) as ImageWithPreview
|
||||
|
||||
newImages.push(imageWithPreview)
|
||||
}
|
||||
|
||||
if (!hasError && newImages.length > 0) {
|
||||
setImages((prev) => [...prev, ...newImages])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing images:', error)
|
||||
setImageError('An error occurred while processing images. Please try again.')
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
|
||||
// Reset the input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeImage = (index: number) => {
|
||||
setImages((prev) => {
|
||||
// Revoke the URL to avoid memory leaks
|
||||
URL.revokeObjectURL(prev[index].preview)
|
||||
return prev.filter((_, i) => i !== index)
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
setIsSubmitting(true)
|
||||
setSubmitStatus(null)
|
||||
|
||||
try {
|
||||
// Create FormData to handle file uploads
|
||||
const formData = new FormData()
|
||||
|
||||
// Add form fields
|
||||
formData.append('email', data.email)
|
||||
formData.append('subject', data.subject)
|
||||
formData.append('message', data.message)
|
||||
formData.append('type', data.type)
|
||||
|
||||
// Add images
|
||||
images.forEach((image, index) => {
|
||||
formData.append(`image_${index}`, image)
|
||||
})
|
||||
|
||||
const response = await fetch('/api/help', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to submit help request')
|
||||
}
|
||||
|
||||
setSubmitStatus('success')
|
||||
reset()
|
||||
|
||||
// Clean up image previews
|
||||
images.forEach((image) => URL.revokeObjectURL(image.preview))
|
||||
setImages([])
|
||||
} catch (error) {
|
||||
console.error('Error submitting help request:', error)
|
||||
setSubmitStatus('error')
|
||||
setErrorMessage(error instanceof Error ? error.message : 'An unknown error occurred')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-10">
|
||||
<Card className="bg-background border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Help & Support</CardTitle>
|
||||
<CardDescription>
|
||||
Submit a bug report, feedback, or feature request. Our team will get back to you as soon
|
||||
as possible.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{submitStatus === 'success' && (
|
||||
<Alert className="mb-6 bg-green-50 text-green-800 border-green-200">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
<AlertTitle>Success!</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your request has been submitted successfully. We'll get back to you as soon as
|
||||
possible.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{submitStatus === 'error' && (
|
||||
<Alert className="mb-6 bg-red-50 text-red-800 border-red-200">
|
||||
<AlertCircle className="h-4 w-4 text-red-600" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
{errorMessage || 'Failed to submit your request. Please try again.'}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="type">Request Type</Label>
|
||||
<Select onValueChange={(value) => setValue('type', value as FormValues['type'])}>
|
||||
<SelectTrigger id="type" className={errors.type ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="Select a request type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bug">Bug Report</SelectItem>
|
||||
<SelectItem value="feedback">Feedback</SelectItem>
|
||||
<SelectItem value="feature_request">Feature Request</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.type && <p className="text-sm text-red-500">{errors.type.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your.email@example.com"
|
||||
className={errors.email ? 'border-red-500' : ''}
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subject">Subject</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
placeholder="Brief description of your request"
|
||||
className={errors.subject ? 'border-red-500' : ''}
|
||||
{...register('subject')}
|
||||
/>
|
||||
{errors.subject && <p className="text-sm text-red-500">{errors.subject.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Please provide details about your request..."
|
||||
className={`min-h-[150px] ${errors.message ? 'border-red-500' : ''}`}
|
||||
{...register('message')}
|
||||
/>
|
||||
{errors.message && <p className="text-sm text-red-500">{errors.message.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="images">Attach Images (Optional)</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="images"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="flex items-center gap-2"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<span className="animate-spin mr-1">⟳</span>
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload Images
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Max 20MB per image. JPEG, PNG, WebP, GIF accepted.
|
||||
</p>
|
||||
</div>
|
||||
{imageError && <p className="text-sm text-red-500 mt-1">{imageError}</p>}
|
||||
|
||||
{images.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label>Attached Images ({images.length})</Label>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||
{images.map((image, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div className="relative aspect-square overflow-hidden rounded-md border bg-muted">
|
||||
<div className="h-full w-full relative">
|
||||
<Image
|
||||
src={image.preview}
|
||||
alt={image.name}
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeImage(index)}
|
||||
className="absolute right-1 top-1 rounded-full bg-foreground/10 p-1 text-white hover:bg-foreground/20 focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Remove</span>
|
||||
</button>
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/50 px-2 py-1 text-xs text-white truncate">
|
||||
{image.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" onClick={() => router.back()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)} disabled={isSubmitting || isProcessing}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Request'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import HelpForm from './components/help-form'
|
||||
|
||||
export default HelpForm
|
||||
Generated
+52
@@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.38.0",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.23.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
@@ -32,6 +33,7 @@
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@webcontainer/api": "^1.5.1-internal.9",
|
||||
"better-auth": "^1.1.18",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
@@ -51,6 +53,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"resend": "^4.1.2",
|
||||
@@ -900,6 +903,18 @@
|
||||
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz",
|
||||
"integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
@@ -3158,6 +3173,12 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -4612,6 +4633,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-image-compression": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz",
|
||||
"integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uzip": "0.20201231.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||
@@ -11268,6 +11298,22 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.54.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz",
|
||||
"integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -13032,6 +13078,12 @@
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uzip": {
|
||||
"version": "0.20201231.0",
|
||||
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
|
||||
"integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.38.0",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.23.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
@@ -44,6 +45,7 @@
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@webcontainer/api": "^1.5.1-internal.9",
|
||||
"better-auth": "^1.1.18",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
@@ -63,6 +65,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"resend": "^4.1.2",
|
||||
|
||||
Reference in New Issue
Block a user