feat(nextjs): Timer added in Each layout convert

This commit is contained in:
shiva raj badu 2025-08-09 20:00:00 +05:45
parent d96ac341e1
commit bb93648a61
No known key found for this signature in database
21 changed files with 580 additions and 208 deletions

View file

@ -38,7 +38,7 @@ RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
# Install dependencies for Next.js
WORKDIR /node_dependencies
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install
RUN npm install --verbose
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome@138.0.7204.94 --install-deps

View file

@ -1,18 +1,15 @@
import React from "react";
import Header from "@/components/Header";
export const AnthropicKeyWarning: React.FC = () => {
export const APIKeyWarning: React.FC = () => {
return (
<div className="min-h-screen font-roboto bg-gradient-to-br from-slate-50 to-slate-100">
<Header />
<div className="flex items-center justify-center aspect-video mx-auto px-6">
<div className="text-center space-y-2 my-6 bg-white p-10 rounded-lg shadow-lg">
<h1 className="text-xl font-bold text-gray-900">
Please put Anthropic Key To Process The Layout
Please add "GOOGLE_API_KEY" to enable template creation via AI.
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
It Only works on Anthropic(Claude-4).
</p>
</div>
</div>
</div>

View file

@ -1,3 +1,5 @@
'use client'
import React from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";

View file

@ -1,3 +1,4 @@
'use client'
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

View file

@ -1,5 +1,6 @@
'use client'
import React from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useDrawingCanvas } from "../../hooks/useDrawingCanvas";
@ -155,36 +156,6 @@ const EachSlide: React.FC<EachSlideProps> = ({
onTouchEnd={handleTouchEnd}
/>
</CardContent>
{/* Action Buttons */}
<div className="p-4 pt-0 flex gap-2">
<Button
onClick={() => {
const newWindow = window.open("", "_blank");
if (newWindow) {
newWindow.document.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slide ${slide.slide_number} - HTML Preview</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="slide-container">
${slide.html}
</div>
</body>
</html>`);
}
}}
variant="outline"
size="sm"
>
Open in new tab
</Button>
</div>
</Card>
);
};

View file

@ -1,3 +1,4 @@
'use client'
import React from "react";
import { AlertCircle, CheckCircle, Edit, Loader2, Repeat2, Trash, Code } from "lucide-react";
import ToolTip from "@/components/ToolTip";

View file

@ -1,8 +1,11 @@
'use client'
import React from "react";
import SlideContent from "../SlideContent";
import { SlideContentDisplayProps } from "../../types";
import { Repeat2 } from "lucide-react";
import Timer from "../Timer";
export const SlideContentDisplay: React.FC<SlideContentDisplayProps> = ({
slide,
@ -33,16 +36,15 @@ export const SlideContentDisplay: React.FC<SlideContentDisplayProps> = ({
if (slide.processing) {
return (
<div className="space-y-4">
<p className="text-base text-blue-600 font-medium">
🔄 Converting to HTML...
</p>
<div className="animate-pulse space-y-3">
<div className="h-6 bg-gray-200 rounded w-2/3"></div>
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
<div className="h-64 bg-gray-200 rounded"></div>
<div className="h-64 bg-gray-200 rounded"></div>
<p className="text-base text-blue-600 font-medium">🔄 Converting to HTML...</p>
<div className="space-y-3">
<Timer duration={160} />
</div>
<div className="animate-pulse space-y-3">
<div className="h-6 bg-gray-200 rounded w-2/3"></div>
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
<div className="h-64 bg-gray-200 rounded"></div>
</div>
</div>
);
}
@ -50,6 +52,12 @@ export const SlideContentDisplay: React.FC<SlideContentDisplayProps> = ({
if (slide.processed && slide.html) {
return (
<div className="relative">
{slide.convertingToReact && (
<div className="mb-4">
<p className="text-sm text-purple-700 font-medium mb-1"> Converting HTML to React...</p>
<Timer duration={90} />
</div>
)}
<div ref={slideDisplayRef} className="relative mx-auto w-full">
<div ref={slideContentRef}>
<SlideContent slide={slide} />
@ -86,27 +94,21 @@ export const SlideContentDisplay: React.FC<SlideContentDisplayProps> = ({
if (slide.error) {
return (
<div className="space-y-4">
<p className="text-base text-red-600 font-medium">
Conversion failed
</p>
<p className="text-base text-red-600 font-medium"> Conversion failed</p>
<div className="text-sm text-gray-700 p-4 bg-red-50 rounded border border-red-200">
{slide.error.includes("image exceeds 5 MB maximum") ? (
<div>
<p className="font-medium text-red-700 mb-2">
Image too large for processing
</p>
<p>
This slide's image exceeds the 5MB limit. Try using a
smaller resolution PPTX file.
</p>
<p className="font-medium text-red-700 mb-2">Image too large for processing</p>
<p>This slide's image exceeds the 5MB limit. Try using a smaller resolution PPTX file.</p>
</div>
) : (
slide.error
)}
</div>
<div className="flex justify-center">
<button className="bg-red-50 flex gap-2 items-center rounded border border-red-200 px-4 py-2 " onClick={() => retrySlide(slide.slide_number)}><Repeat2 className="w-4 h-4" />Retry</button>
<button className="bg-red-50 flex gap-2 items-center rounded border border-red-200 px-4 py-2 " onClick={() => retrySlide(slide.slide_number)}>
<Repeat2 className="w-4 h-4" />Retry
</button>
</div>
</div>
);
@ -114,9 +116,7 @@ export const SlideContentDisplay: React.FC<SlideContentDisplayProps> = ({
return (
<div className="space-y-4">
<p className="text-base text-gray-500">
Waiting in queue to process...
</p>
<p className="text-base text-gray-500"> Waiting in queue to process...</p>
<div className="animate-pulse space-y-3">
<div className="h-6 bg-gray-200 rounded w-2/3"></div>
<div className="h-6 bg-gray-200 rounded w-1/2"></div>

View file

@ -10,6 +10,7 @@ import {
import { Label } from "@/components/ui/label";
import { Upload, FileText, X, Loader2 } from "lucide-react";
import { ProcessedSlide } from "../types";
import Timer from "./Timer";
interface FileUploadSectionProps {
selectedFile: File | null;
@ -96,7 +97,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
</div>
)}
<div className="flex gap-3">
<div className="flex flex-col gap-1 ">
<Button
onClick={processFile}
disabled={isProcessingPptx || slides.some((s) => s.processing)}
@ -108,6 +109,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
? "Select a PPTX file"
: "Process File"}
</Button>
{isProcessingPptx && <Timer duration={90} />}
</div>
</CardContent>
</Card>

View file

@ -1,3 +1,5 @@
'use client'
import React, { memo } from "react";
const SlideContent = memo(({ slide }: { slide: any }) => {

View file

@ -0,0 +1,103 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
interface TimerProps {
duration: number // seconds
}
const Timer = ({ duration }: TimerProps) => {
const [progress, setProgress] = useState<number>(0)
const rafIdRef = useRef<number | null>(null)
const startTimeRef = useRef<number | null>(null)
useEffect(() => {
// Guard against invalid durations
const totalMs = Math.max(0, duration * 1000)
const easeOutCubic = (x: number) => 1 - Math.pow(1 - x, 3)
const easeOutSine = (x: number) => Math.sin((x * Math.PI) / 2)
const tick = (now: number) => {
if (startTimeRef.current === null) startTimeRef.current = now
const elapsed = now - startTimeRef.current
const t = totalMs === 0 ? 1 : Math.min(elapsed / totalMs, 1)
// Piecewise progression:
// - Reach ~75% around 60% of the total duration (faster start)
// - Then ease slowly towards 99% for the remainder
let nextProgress: number
if (t <= 0.6) {
nextProgress = 75 * easeOutCubic(t / 0.6)
} else {
nextProgress = 75 + 24 * easeOutSine((t - 0.6) / 0.4)
}
// Clamp and ensure we never hit 100
nextProgress = Math.min(99, nextProgress)
setProgress(prev => (nextProgress < prev ? prev : nextProgress))
if (t < 1 && nextProgress < 99) {
rafIdRef.current = requestAnimationFrame(tick)
} else {
// End at 99 and stop
setProgress(99)
if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current)
rafIdRef.current = null
}
}
// Initialize animation
setProgress(0)
startTimeRef.current = null
rafIdRef.current = requestAnimationFrame(tick)
return () => {
if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current)
rafIdRef.current = null
startTimeRef.current = null
}
}, [duration])
return (
<div className="w-full space-y-2">
<div className="flex justify-end items-center text-gray-800 text-sm">
<span className="font-inter text-end font-semibold text-xs">{Math.round(progress)}%</span>
</div>
<div
className="w-full rounded-full h-3 overflow-hidden shadow-inner"
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(progress)}
>
<div className="relative h-full rounded-full" style={{
width: `${progress}%`,
backgroundImage: 'linear-gradient(90deg, #9034EA, #5146E5, #9034EA)',
backgroundSize: '200% 100%',
animation: 'gradient 2s linear infinite'
}}>
<div className="absolute inset-0 opacity-25" style={{
backgroundImage:
'linear-gradient(45deg, rgba(255,255,255,.8) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.8) 50%, rgba(255,255,255,.8) 75%, transparent 75%, transparent)',
backgroundSize: '16px 16px',
animation: 'stripes 1s linear infinite'
}} />
</div>
<div className="absolute inset-0" />
</div>
<style jsx>{`
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes stripes {
to { background-position: 16px 0; }
}
`}</style>
</div>
)
}
export default Timer

View file

@ -0,0 +1,17 @@
import { useState, useEffect } from "react";
export const useAPIKeyCheck = () => {
const [hasRequiredKey, setHasAnthropicKey] = useState(false);
const [isRequiredKeyLoading, setIsAnthropicKeyLoading] = useState(true);
useEffect(() => {
fetch("/api/has-required-key")
.then((res) => res.json())
.then((data) => {
setHasAnthropicKey(data.hasKey);
setIsAnthropicKeyLoading(false);
});
}, []);
return { hasRequiredKey, isRequiredKeyLoading };
};

View file

@ -1,17 +0,0 @@
import { useState, useEffect } from "react";
export const useAnthropicKeyCheck = () => {
const [hasAnthropicKey, setHasAnthropicKey] = useState(false);
const [isAnthropicKeyLoading, setIsAnthropicKeyLoading] = useState(true);
useEffect(() => {
fetch("/api/has-anthropic-key")
.then((res) => res.json())
.then((data) => {
setHasAnthropicKey(data.hasKey);
setIsAnthropicKeyLoading(false);
});
}, []);
return { hasAnthropicKey, isAnthropicKeyLoading };
};

View file

@ -7,7 +7,8 @@ import { ProcessedSlide, UploadedFont } from "../types";
export const useLayoutSaving = (
slides: ProcessedSlide[],
UploadedFonts: UploadedFont[],
refetch: () => void
refetch: () => void,
setSlides: React.Dispatch<React.SetStateAction<ProcessedSlide[]>>
) => {
const [isSavingLayout, setIsSavingLayout] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
@ -80,7 +81,7 @@ export const useLayoutSaving = (
try {
// Convert each slide HTML to React component
const reactComponents = [];
const reactComponents: any[] = [];
const presentationId = uuidv4();
// Get all uploaded font URLs
@ -95,6 +96,9 @@ export const useLayoutSaving = (
continue;
}
// Mark current slide as converting to React
setSlides(prev => prev.map((s, idx) => idx === i ? { ...s, convertingToReact: true } : s));
try {
const reactComponent = await convertSlideToReact(slide, presentationId, FontUrls);
reactComponents.push(reactComponent);
@ -112,7 +116,9 @@ export const useLayoutSaving = (
: "An unexpected error occurred",
});
// Continue with other slides even if one fails
continue;
} finally {
// Clear converting flag for this slide
setSlides(prev => prev.map((s, idx) => idx === i ? { ...s, convertingToReact: false } : s));
}
}
@ -169,7 +175,7 @@ export const useLayoutSaving = (
} finally {
setIsSavingLayout(false);
}
}, [slides, UploadedFonts, refetch, closeSaveModal]);
}, [slides, UploadedFonts, refetch, closeSaveModal, setSlides]);
return {
isSavingLayout,

View file

@ -9,20 +9,20 @@ import { useFontManagement } from "./hooks/useFontManagement";
import { useFileUpload } from "./hooks/useFileUpload";
import { useSlideProcessing } from "./hooks/useSlideProcessing";
import { useLayoutSaving } from "./hooks/useLayoutSaving";
import { useAnthropicKeyCheck } from "./hooks/useAnthropicKeyCheck";
import { LoadingSpinner } from "./components/LoadingSpinner";
import { AnthropicKeyWarning } from "./components/AnthropicKeyWarning";
import { FileUploadSection } from "./components/FileUploadSection";
import { SaveLayoutButton } from "./components/SaveLayoutButton";
import { SaveLayoutModal } from "./components/SaveLayoutModal";
import EachSlide from "./components/EachSlide/NewEachSlide";
import { APIKeyWarning } from "./components/APIKeyWarning";
import { useAPIKeyCheck } from "./hooks/useAPIKeyCheck";
const CustomLayoutPage = () => {
const { refetch } = useLayout();
// Custom hooks for different concerns
const { hasAnthropicKey, isAnthropicKeyLoading } = useAnthropicKeyCheck();
const { hasRequiredKey, isRequiredKeyLoading } = useAPIKeyCheck();
const { selectedFile, handleFileSelect, removeFile } = useFileUpload();
const { slides, setSlides, completedSlides } = useCustomLayout();
const { fontsData, UploadedFonts, uploadFont, removeFont, getAllUnsupportedFonts, setFontsData } = useFontManagement();
@ -35,7 +35,8 @@ const CustomLayoutPage = () => {
const { isSavingLayout, isModalOpen, openSaveModal, closeSaveModal, saveLayout } = useLayoutSaving(
slides,
UploadedFonts,
refetch
refetch,
setSlides
);
const handleProcessSlideToHtml = (slide: any) => {
@ -58,13 +59,13 @@ const CustomLayoutPage = () => {
};
// Loading state
if (isAnthropicKeyLoading) {
return <LoadingSpinner message="Checking Anthropic Key..." />;
if (isRequiredKeyLoading) {
return <LoadingSpinner message="Checking API Key..." />;
}
// Anthropic key warning
if (!hasAnthropicKey) {
return <AnthropicKeyWarning />;
if (!hasRequiredKey) {
return <APIKeyWarning />;
}
return (

View file

@ -18,6 +18,7 @@ export interface ProcessedSlide extends SlideData {
processed?: boolean;
error?: string;
modified?: boolean;
convertingToReact?: boolean; // indicates HTML-to-React conversion in progress
}
export interface FontData {

View file

@ -1,19 +1,54 @@
"use client";
import React, { useEffect } from "react";
import React, { useEffect, useState, useRef } from "react";
import { useParams, useRouter } from "next/navigation";
// import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader'
import LoadingStates from "../components/LoadingStates";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Home, Trash2 } from "lucide-react";
import { ArrowLeft, Edit, Home, Trash2 } from "lucide-react";
import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
import { useDrawingCanvas } from "../../custom-layout/hooks/useDrawingCanvas";
import { EditControls } from "../../custom-layout/components/EachSlide/EditControls";
import html2canvas from "html2canvas";
const GroupLayoutPreview = () => {
const params = useParams();
const router = useRouter();
const slug = params.slug as string;
// const isCustom = slug.includes("custom-");
const isCustom = true;
// Custom hooks
const {
canvasRef,
slideDisplayRef,
strokeWidth,
strokeColor,
eraserMode,
isDrawing,
canvasDimensions,
setCanvasDimensions,
didYourDraw,
handleMouseDown,
handleMouseMove,
handleMouseUp,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
handleClearCanvas,
handleEraserModeChange,
handleStrokeColorChange,
handleStrokeWidthChange,
} = useDrawingCanvas();
const slideContentRef = useRef<HTMLDivElement | null>(null);
const { getFullDataByGroup, loading,refetch } = useLayout();
const layoutGroup = getFullDataByGroup(slug);
const [isEditMode, setIsEditMode] = useState(false);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [prompt, setPrompt] = useState("");
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
const existingScript = document.querySelector(
@ -27,6 +62,17 @@ const GroupLayoutPreview = () => {
}
}, [slug]);
// Size canvas to content when entering edit mode
useEffect(() => {
if (isEditMode && slideContentRef.current) {
const rect = slideContentRef.current.getBoundingClientRect();
setCanvasDimensions({
width: Math.max(rect.width, 800),
height: Math.max(rect.height, 600),
});
}
}, [isEditMode, setCanvasDimensions]);
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />;
@ -47,6 +93,130 @@ const GroupLayoutPreview = () => {
router.push("/layout-preview");
}
}
const handleSave = async (
slideDisplayRef: React.RefObject<HTMLDivElement |null>,
didYourDraw: boolean
) => {
if (
!slideContentRef.current ||
!slideDisplayRef.current
)
return;
if (!prompt.trim()) {
alert("Please enter a prompt before saving.");
return;
}
setIsUpdating(true);
try {
// Take screenshot of the slide display area (slide only)
const slideOnly = await html2canvas(slideDisplayRef.current, {
backgroundColor: "#ffffff",
scale: 1,
logging: false,
useCORS: true,
ignoreElements: (element) => {
return element.tagName === "CANVAS";
},
});
let slideWithCanvas;
if (didYourDraw) {
// Take screenshot of the entire slide display area including canvas
slideWithCanvas = await html2canvas(slideDisplayRef.current, {
backgroundColor: "#ffffff",
scale: 1,
logging: false,
useCORS: true,
});
}
const currentUiImageBlob = dataURLToBlob(
slideOnly.toDataURL("image/png")
);
let sketchImageBlob;
if (didYourDraw && slideWithCanvas) {
sketchImageBlob = dataURLToBlob(slideWithCanvas.toDataURL("image/png"));
}
// download the images
const currentUiImageUrl = URL.createObjectURL(currentUiImageBlob);
const sketchImageUrl = didYourDraw ? URL.createObjectURL(sketchImageBlob) : null;
const a = document.createElement("a");
a.href = currentUiImageUrl;
a.download = `slide-current.png`;
a.click();
if (sketchImageUrl) {
const b = document.createElement("a");
b.href = sketchImageUrl;
b.download = `slide-sketch.png`;
b.click();
}
// const formData = new FormData();
// formData.append(
// "current_ui_image",
// currentUiImageBlob,
// `slide--current.png`
// );
// if (didYourDraw && slideWithCanvas && sketchImageBlob) {
// formData.append(
// "sketch_image",
// sketchImageBlob,
// `slide-sketch.png`
// );
// }
// formData.append("html", '');
// formData.append("prompt", prompt);
// const response = await fetch("/api/v1/ppt/html-edit/", {
// method: "POST",
// body: formData,
// });
// if (!response.ok) {
// throw new Error(`API call failed: ${response.statusText}`);
// }
// const data = await response.json();
// Exit edit mode
setIsEditMode(false);
setPrompt("");
} catch (error) {
console.error("Error updating slide:", error);
alert(
`Error updating slide: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
} finally {
setIsUpdating(false);
}
};
const dataURLToBlob = (dataURL: string): Blob => {
const parts = dataURL.split(",");
const contentType = parts[0].match(/:(.*?);/)?.[1] || "image/png";
const raw = window.atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
};
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
@ -72,7 +242,7 @@ const GroupLayoutPreview = () => {
<Home className="w-4 h-4" />
All Groups
</Button>
{slug.includes('custom-') && <button className=" border border-red-200 flex justify-center items-center gap-2 text-red-700 px-4 py-1 rounded-md" onClick={() => {
{isCustom && <button className=" border border-red-200 flex justify-center items-center gap-2 text-red-700 px-4 py-1 rounded-md" onClick={() => {
deleteLayouts();
}}><Trash2 className="w-4 h-4" />Delete</button>}
</div>
@ -90,8 +260,37 @@ const GroupLayoutPreview = () => {
</div>
</header>
{/* Layout Grid */}
<main className="max-w-7xl mx-auto px-6 py-8">
{/* Edit Controls (no HTML editor) */}
{isCustom && (
<EditControls
isEditMode={isEditMode}
prompt={prompt}
isUpdating={isUpdating}
strokeWidth={strokeWidth}
strokeColor={strokeColor}
eraserMode={eraserMode}
onPromptChange={setPrompt}
onSave={() => {
setIsUpdating(true);
setTimeout(() => {
setIsUpdating(false);
setIsEditMode(false);
setSelectedIndex(null);
}, 300);
}}
onCancel={() => {
setIsEditMode(false);
setSelectedIndex(null);
handleClearCanvas();
}}
onStrokeWidthChange={handleStrokeWidthChange}
onStrokeColorChange={handleStrokeColorChange}
onEraserModeChange={handleEraserModeChange}
onClearCanvas={handleClearCanvas}
/>
)}
<div className="space-y-8">
{layoutGroup.map((layout: any, index: number) => {
const {
@ -101,6 +300,8 @@ const GroupLayoutPreview = () => {
fileName,
} = layout;
const isSelected = isCustom && isEditMode && selectedIndex === index;
return (
<Card
key={`${layoutGroup[0].groupName}-${index}`}
@ -123,16 +324,53 @@ const GroupLayoutPreview = () => {
</div>
</div>
<div className="text-right">
<div className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-700">
Layout #{index + 1}
</div>
{isCustom && (
<button
className="border flex items-center gap-2 border-blue-400 bg-blue-50 px-4 py-1 rounded-md text-blue-700"
onClick={() => {
setIsEditMode(true);
setSelectedIndex(index);
}}
>
<Edit className="w-4 h-4" />Edit
</button>
)}
</div>
</div>
</div>
{/* Layout Content */}
<div className="bg-gray-50 aspect-video max-w-[1280px] w-full">
<LayoutComponent data={sampleData} />
<div ref={isSelected ? slideDisplayRef : undefined} className="relative mx-auto w-full">
<div
ref={isSelected ? slideContentRef : undefined}
className="bg-gray-50 aspect-video max-w-[1280px] w-full"
>
<LayoutComponent data={sampleData} />
{isSelected && (
<canvas
ref={canvasRef!}
width={canvasDimensions.width}
height={canvasDimensions.height}
style={{
position: "absolute",
top: 0,
left: 0,
zIndex: 30,
cursor: eraserMode ? "grab" : "crosshair",
pointerEvents: "auto",
touchAction: "none",
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onContextMenu={(e) => e.preventDefault()}
/>
)}
</div>
</div>
</Card>
);

View file

@ -3,6 +3,6 @@ import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
const hasKey = process.env.ANTHROPIC_API_KEY !== "";
const hasKey = process.env.GOOGLE_API_KEY !== "";
return NextResponse.json({ hasKey });
}

View file

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View file

@ -45,7 +45,7 @@
"lucide-react": "^0.447.0",
"marked": "^15.0.11",
"mermaid": "^11.9.0",
"next": "15.4.5",
"next": "^14.2.14",
"next-themes": "^0.4.6",
"prismjs": "^1.30.0",
"puppeteer": "^24.13.0",
@ -67,8 +67,8 @@
"@types/node": "^20",
"@types/prismjs": "^1.26.5",
"@types/puppeteer": "^5.4.7",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.13",
"cypress": "^14.3.3",
@ -1425,15 +1425,15 @@
}
},
"node_modules/@next/env": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.5.tgz",
"integrity": "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.31.tgz",
"integrity": "sha512-X8VxxYL6VuezrG82h0pUA1V+DuTSJp7Nv15bxq3ivrFqZLjx81rfeHMWOE9T0jm1n3DtHGv8gdn6B0T0kr0D3Q==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.5.tgz",
"integrity": "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.31.tgz",
"integrity": "sha512-dTHKfaFO/xMJ3kzhXYgf64VtV6MMwDs2viedDOdP+ezd0zWMOQZkxcwOfdcQeQCpouTr9b+xOqMCUXxgLizl8Q==",
"cpu": [
"arm64"
],
@ -1447,9 +1447,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.5.tgz",
"integrity": "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.31.tgz",
"integrity": "sha512-iSavebQgeMukUAfjfW8Fi2Iz01t95yxRl2w2wCzjD91h5In9la99QIDKcKSYPfqLjCgwz3JpIWxLG6LM/sxL4g==",
"cpu": [
"x64"
],
@ -1463,9 +1463,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.5.tgz",
"integrity": "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.31.tgz",
"integrity": "sha512-XJb3/LURg1u1SdQoopG6jDL2otxGKChH2UYnUTcby4izjM0il7ylBY5TIA7myhvHj9lG5pn9F2nR2s3i8X9awQ==",
"cpu": [
"arm64"
],
@ -1479,9 +1479,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.5.tgz",
"integrity": "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.31.tgz",
"integrity": "sha512-IInDAcchNCu3BzocdqdCv1bKCmUVO/bKJHnBFTeq3svfaWpOPewaLJ2Lu3GL4yV76c/86ZvpBbG/JJ1lVIs5MA==",
"cpu": [
"arm64"
],
@ -1495,9 +1495,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.5.tgz",
"integrity": "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.31.tgz",
"integrity": "sha512-YTChJL5/9e4NXPKW+OJzsQa42RiWUNbE+k+ReHvA+lwXk+bvzTsVQboNcezWOuCD+p/J+ntxKOB/81o0MenBhw==",
"cpu": [
"x64"
],
@ -1511,9 +1511,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.5.tgz",
"integrity": "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.31.tgz",
"integrity": "sha512-A0JmD1y4q/9ufOGEAhoa60Sof++X10PEoiWOH0gZ2isufWZeV03NnyRlRmJpRQWGIbRkJUmBo9I3Qz5C10vx4w==",
"cpu": [
"x64"
],
@ -1527,9 +1527,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.5.tgz",
"integrity": "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.31.tgz",
"integrity": "sha512-nowJ5GbMeDOMzbTm29YqrdrD6lTM8qn2wnZfGpYMY7SZODYYpaJHH1FJXE1l1zWICHR+WfIMytlTDBHu10jb8A==",
"cpu": [
"arm64"
],
@ -1542,10 +1542,26 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.31.tgz",
"integrity": "sha512-pk9Bu4K0015anTS1OS9d/SpS0UtRObC+xe93fwnm7Gvqbv/W1ZbzhK4nvc96RURIQOux3P/bBH316xz8wjGSsA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.5.tgz",
"integrity": "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.31.tgz",
"integrity": "sha512-LwFZd4JFnMHGceItR9+jtlMm8lGLU/IPkgjBBgYmdYSfalbHCiDpjMYtgDQ2wtwiAOSJOCyFI4m8PikrsDyA6Q==",
"cpu": [
"x64"
],
@ -2823,13 +2839,20 @@
"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",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/typography": {
@ -3593,9 +3616,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
"integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
"version": "20.19.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz",
"integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@ -3609,6 +3632,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/puppeteer": {
"version": "5.4.7",
"resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.7.tgz",
@ -3620,23 +3650,24 @@
}
},
"node_modules/@types/react": {
"version": "19.1.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.1.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
"@types/react": "^18.0.0"
}
},
"node_modules/@types/sinonjs__fake-timers": {
@ -3951,9 +3982,9 @@
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz",
"integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz",
"integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==",
"license": "Apache-2.0",
"optional": true
},
@ -4152,6 +4183,17 @@
"node": "*"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/cachedir": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
@ -4212,9 +4254,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001731",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"version": "1.0.30001733",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz",
"integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==",
"funding": [
{
"type": "opencollective",
@ -4679,9 +4721,9 @@
"license": "MIT"
},
"node_modules/cypress": {
"version": "14.5.3",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.3.tgz",
"integrity": "sha512-syLwKjDeMg77FRRx68bytLdlqHXDT4yBVh0/PPkcgesChYDjUZbwxLqMXuryYKzAyJsPsQHUDW1YU74/IYEUIA==",
"version": "14.5.4",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.4.tgz",
"integrity": "sha512-0Dhm4qc9VatOcI1GiFGVt8osgpPdqJLHzRwcAB5MSD/CAAts3oybvPUPawHyvJZUd8osADqZe/xzMsZ8sDTjXw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -5346,9 +5388,9 @@
"license": "MIT"
},
"node_modules/devtools-protocol": {
"version": "0.0.1464554",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz",
"integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==",
"version": "0.0.1475386",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz",
"integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==",
"license": "BSD-3-Clause"
},
"node_modules/didyoumean": {
@ -6107,7 +6149,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/hachure-fill": {
@ -7143,40 +7184,41 @@
}
},
"node_modules/next": {
"version": "15.4.5",
"resolved": "https://registry.npmjs.org/next/-/next-15.4.5.tgz",
"integrity": "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g==",
"version": "14.2.31",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.31.tgz",
"integrity": "sha512-Wyw1m4t8PhqG+or5a1U/Deb888YApC4rAez9bGhHkTsfwAy4SWKVro0GhEx4sox1856IbLhvhce2hAA6o8vkog==",
"license": "MIT",
"dependencies": {
"@next/env": "15.4.5",
"@swc/helpers": "0.5.15",
"@next/env": "14.2.31",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.4.5",
"@next/swc-darwin-x64": "15.4.5",
"@next/swc-linux-arm64-gnu": "15.4.5",
"@next/swc-linux-arm64-musl": "15.4.5",
"@next/swc-linux-x64-gnu": "15.4.5",
"@next/swc-linux-x64-musl": "15.4.5",
"@next/swc-win32-arm64-msvc": "15.4.5",
"@next/swc-win32-x64-msvc": "15.4.5",
"sharp": "^0.34.3"
"@next/swc-darwin-arm64": "14.2.31",
"@next/swc-darwin-x64": "14.2.31",
"@next/swc-linux-arm64-gnu": "14.2.31",
"@next/swc-linux-arm64-musl": "14.2.31",
"@next/swc-linux-x64-gnu": "14.2.31",
"@next/swc-linux-x64-musl": "14.2.31",
"@next/swc-win32-arm64-msvc": "14.2.31",
"@next/swc-win32-ia32-msvc": "14.2.31",
"@next/swc-win32-x64-msvc": "14.2.31"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.51.1",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
@ -7186,9 +7228,6 @@
"@playwright/test": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"sass": {
"optional": true
}
@ -7797,9 +7836,9 @@
}
},
"node_modules/prosemirror-model": {
"version": "1.25.2",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.2.tgz",
"integrity": "sha512-BVypCAJ4SL6jOiTsDffP3Wp6wD69lRhI4zg/iT8JXjp3ccZFiq5WyguxvMKmdKFC3prhaig7wSr8dneDToHE1Q==",
"version": "1.25.3",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.3.tgz",
"integrity": "sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
@ -7948,17 +7987,17 @@
}
},
"node_modules/puppeteer": {
"version": "24.15.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.15.0.tgz",
"integrity": "sha512-HPSOTw+DFsU/5s2TUUWEum9WjFbyjmvFDuGHtj2X4YUz2AzOzvKMkT3+A3FR+E+ZefiX/h3kyLyXzWJWx/eMLQ==",
"version": "24.16.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.16.0.tgz",
"integrity": "sha512-5qxFGOpdAzYexoPwKPEF4L/IYKYOFE1MxWsqcp7K33HySM8N8S/yZwSQCaV0rzmJsTLX5LxU4zt65+ceNiVDgQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.6",
"chromium-bidi": "7.2.0",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1464554",
"puppeteer-core": "24.15.0",
"devtools-protocol": "0.0.1475386",
"puppeteer-core": "24.16.0",
"typed-query-selector": "^2.12.0"
},
"bin": {
@ -7969,15 +8008,15 @@
}
},
"node_modules/puppeteer-core": {
"version": "24.15.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.15.0.tgz",
"integrity": "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg==",
"version": "24.16.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.16.0.tgz",
"integrity": "sha512-tZ0tJiOYaDGTRzzr2giDpf8O/55JsoqkrafS1Xu4H6S8oP4eeL6RbZzY9OzjShSf5EQvx/zAc55QKpDqzXos/Q==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.6",
"chromium-bidi": "7.2.0",
"debug": "^4.4.1",
"devtools-protocol": "0.0.1464554",
"devtools-protocol": "0.0.1475386",
"typed-query-selector": "^2.12.0",
"ws": "^8.18.3"
},
@ -8747,6 +8786,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/streamx": {
"version": "2.22.1",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz",
@ -8825,9 +8872,9 @@
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
@ -8836,7 +8883,7 @@
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
@ -9207,9 +9254,9 @@
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
@ -9669,9 +9716,9 @@
}
},
"node_modules/zod": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.15.tgz",
"integrity": "sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ==",
"version": "4.0.16",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.16.tgz",
"integrity": "sha512-Djo/cM339grjI7/HmN+ixYO2FzEMcWr/On50UlQ/RjrWK1I/hPpWhpC76heCptnRFpH0LMwrEbUY50HDc0V8wg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View file

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev ",
"build": "next build",
"start": "next start",
"lint": "next lint"
@ -47,7 +47,7 @@
"lucide-react": "^0.447.0",
"marked": "^15.0.11",
"mermaid": "^11.9.0",
"next": "15.4.5",
"next": "^14.2.14",
"next-themes": "^0.4.6",
"prismjs": "^1.30.0",
"puppeteer": "^24.13.0",
@ -69,8 +69,8 @@
"@types/node": "^20",
"@types/prismjs": "^1.26.5",
"@types/puppeteer": "^5.4.7",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.13",
"cypress": "^14.3.3",