| | <!DOCTYPE html>
|
| | <html lang="en">
|
| | <head>
|
| | <meta charset="UTF-8" />
|
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| | <title>Interconnected Subject Explorer</title>
|
| | <script src="https://cdn.tailwindcss.com"></script>
|
| | <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
| | <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
| | <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| | <style>
|
| | html.dark { background: #1a2233; }
|
| | ::selection { background: #fbbf24; color: #1a2233; }
|
| | .fade-in { animation: fadeIn .5s; }
|
| | @keyframes fadeIn { from { opacity:0; transform: translateY(24px);} to {opacity:1; transform:none;} }
|
| | .glass {
|
| | background: rgba(255,255,255,0.82);
|
| | backdrop-filter: blur(7px) saturate(1.2);
|
| | }
|
| | html.dark .glass {
|
| | background: rgba(28,32,46,0.90);
|
| | color: #f1f5f9;
|
| | }
|
| | .scenario-active {
|
| | outline: 3px solid #6366f1;
|
| | box-shadow: 0 2px 24px 0 #6366f140;
|
| | z-index: 10;
|
| | }
|
| | </style>
|
| | </head>
|
| | <body class="bg-slate-100 dark:bg-slate-900">
|
| | <div id="root"></div>
|
| | <script type="text/babel">
|
| | const { useState, useMemo, useEffect, useCallback } = React;
|
| |
|
| | const SUBJECTS = [
|
| | { key: "math", name: "Mathematics", icon: "➗", color: "bg-indigo-500", accent: "from-indigo-400 to-fuchsia-500" },
|
| | { key: "science", name: "Science", icon: "🔬", color: "bg-green-600", accent: "from-green-400 to-cyan-500" },
|
| | { key: "history", name: "History", icon: "📜", color: "bg-yellow-600", accent: "from-yellow-400 to-orange-400" },
|
| | { key: "art", name: "Art", icon: "🎨", color: "bg-pink-500", accent: "from-pink-400 to-rose-400" },
|
| | { key: "geography", name: "Geography", icon: "🗺️", color: "bg-blue-500", accent: "from-blue-400 to-green-300" },
|
| | { key: "cs", name: "Computer Science", icon: "💻", color: "bg-slate-700", accent: "from-slate-600 to-emerald-400" },
|
| | { key: "economics", name: "Economics", icon: "💹", color: "bg-orange-600", accent: "from-orange-400 to-yellow-400" },
|
| | { key: "literature", name: "Literature", icon: "📚", color: "bg-red-500", accent: "from-red-400 to-yellow-300" }
|
| | ];
|
| |
|
| | function darkModeInit() {
|
| | const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
| | if (mql.matches) document.documentElement.classList.add('dark');
|
| | }
|
| | darkModeInit();
|
| |
|
| | function App() {
|
| | const [apiKey, setApiKey] = useState("");
|
| | const [currentSubject, setCurrentSubject] = useState(SUBJECTS[0].key);
|
| | const [aiLoading, setAiLoading] = useState(false);
|
| | const [aiError, setAiError] = useState("");
|
| | const [aiData, setAiData] = useState({});
|
| | const [inputPrompt, setInputPrompt] = useState("");
|
| | const [activeScenario, setActiveScenario] = useState(null);
|
| | const [projectNotes, setProjectNotes] = useState("");
|
| | const [pbLoading, setPbLoading] = useState(false);
|
| | const [pbError, setPbError] = useState("");
|
| | const [pbResult, setPbResult] = useState("");
|
| | // Per-subject "What I'm studying this week"
|
| | const [subjectNotes, setSubjectNotes] = useState({});
|
| |
|
| | const subjectMeta = useMemo(() => SUBJECTS.find(s=>s.key===currentSubject), [currentSubject]);
|
| | const currentSubjectNote = subjectNotes[currentSubject] || "";
|
| |
|
| | // AI request handler (for ideas/scenarios)
|
| | const fetchAIContent = useCallback(async () => {
|
| | if (!apiKey || !apiKey.startsWith("sk-")) { setAiError("Please enter a valid OpenAI API key."); return; }
|
| | setAiLoading(true); setAiError(""); setActiveScenario(null);
|
| | const extraContext = [
|
| | inputPrompt ? inputPrompt : null,
|
| | currentSubjectNote ? `This week, the student is studying: "${currentSubjectNote}" in ${subjectMeta.name}.` : null
|
| | ].filter(Boolean).join(" ");
|
| | const prompt = `
|
| | You are an educational AI agent. For the subject "${subjectMeta.name}", perform the following:
|
| | 1. Generate five engaging, critical-thinking ideas or inquiry questions for students, encouraging them to explore and analyze complex concepts within this subject. Each should be a concise, thought-provoking prompt.
|
| | 2. Propose nine creative real-life scenarios, each showing how "${subjectMeta.name}" connects with at least one other school subject (from: Mathematics, Science, History, Art, Geography, Computer Science, Economics, Literature), and describe a real-world application for each. Clearly specify which subjects are involved.
|
| |
|
| | ${extraContext ? "User context: " + extraContext : ""}
|
| |
|
| | Format the result as valid JSON with two top-level keys:
|
| | - "ideas": array of strings (five items).
|
| | - "scenarios": array of objects, each with:
|
| | - "title": string,
|
| | - "subjects": array of strings (subject names),
|
| | - "application": string (practical real-life connection).
|
| |
|
| | Output ONLY the JSON, no commentary.
|
| | `.trim();
|
| | try {
|
| | const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
| | method: "POST",
|
| | headers: {
|
| | "Content-Type": "application/json",
|
| | Authorization: `Bearer ${apiKey}`
|
| | },
|
| | body: JSON.stringify({
|
| | model: "gpt-4o-mini",
|
| | messages: [
|
| | { role: "system", content: "You are a helpful educational agent. Only return valid JSON, no markdown or commentary." },
|
| | { role: "user", content: prompt }
|
| | ],
|
| | temperature: 0.74,
|
| | max_tokens: 1200
|
| | })
|
| | });
|
| | if (!res.ok) throw new Error(`API error (${res.status})`);
|
| | const data = await res.json();
|
| | const txt = data.choices[0].message.content;
|
| | const start = txt.indexOf("{"), end = txt.lastIndexOf("}");
|
| | const jsonString = (start !== -1 && end !== -1) ? txt.slice(start, end+1) : "";
|
| | const result = JSON.parse(jsonString);
|
| | setAiData(prev => ({...prev, [currentSubject]: result}));
|
| | } catch (e) {
|
| | setAiError("Failed to load AI ideas: " + (e.message || "Unknown error"));
|
| | } finally { setAiLoading(false); }
|
| | }, [apiKey, currentSubject, subjectMeta, inputPrompt, currentSubjectNote]);
|
| |
|
| | useEffect(() => {
|
| | setAiError(""); setActiveScenario(null); setProjectNotes("");
|
| | setPbError(""); setPbResult(""); setPbLoading(false);
|
| | }, [currentSubject]);
|
| |
|
| | useEffect(()=>{
|
| | function toggle(e){
|
| | if(e.ctrlKey && e.key==="k"){
|
| | document.documentElement.classList.toggle("dark");
|
| | e.preventDefault();
|
| | }
|
| | }
|
| | window.addEventListener("keydown", toggle);
|
| | return ()=>window.removeEventListener("keydown",toggle);
|
| | },[]);
|
| |
|
| | // Canvas content for each subject
|
| | const canvasContent = useMemo(() => {
|
| | const d = aiData[currentSubject];
|
| | return (
|
| | <div className="fade-in px-1 sm:px-8 pt-1 pb-4">
|
| | {/* User custom input for the subject */}
|
| | <div className="mb-6">
|
| | <label className="block font-semibold text-indigo-800 dark:text-indigo-200 mb-1">
|
| | What are you studying in {subjectMeta.name} this week?
|
| | </label>
|
| | <input
|
| | type="text"
|
| | className="w-full px-3 py-2 rounded-lg border bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow text-base focus:ring-2 focus:ring-indigo-400"
|
| | placeholder={`E.g., Algebraic fractions, Newton's Laws, Ancient Egypt, Poetry, ...`}
|
| | value={currentSubjectNote}
|
| | onChange={e=>{
|
| | setSubjectNotes(notes => ({
|
| | ...notes,
|
| | [currentSubject]: e.target.value
|
| | }));
|
| | }}
|
| | aria-label={`What are you studying in ${subjectMeta.name} this week?`}
|
| | />
|
| | </div>
|
| | {/* Ideas */}
|
| | {d && (
|
| | <div className="mb-7">
|
| | <h3 className="text-2xl font-semibold mb-2 tracking-tight text-indigo-900 dark:text-indigo-200">
|
| | Ideas to Explore (Critical Thinking)
|
| | </h3>
|
| | <ul className="grid gap-2">
|
| | {d.ideas.map((idea, i) =>
|
| | <li key={i}
|
| | className="rounded-xl px-4 py-2 bg-gradient-to-br from-white via-indigo-50/60 to-indigo-100 dark:from-slate-800 dark:via-slate-700 dark:to-indigo-900/40 shadow-sm border-l-4 border-indigo-400 dark:border-indigo-500">
|
| | {idea}
|
| | </li>
|
| | )}
|
| | </ul>
|
| | </div>
|
| | )}
|
| | {/* Scenarios */}
|
| | {d && (
|
| | <div>
|
| | <h3 className="text-2xl font-semibold mb-2 tracking-tight text-emerald-900 dark:text-emerald-200">
|
| | Interconnected Scenarios (Real-Life Applications)
|
| | </h3>
|
| | <ul className="grid gap-3">
|
| | {d.scenarios.map((sc, i) =>
|
| | <li key={i}
|
| | className={
|
| | "glass rounded-xl px-5 py-4 shadow transition hover:scale-[1.025] cursor-pointer " +
|
| | (activeScenario === i ? "scenario-active" : "")
|
| | }
|
| | onClick={()=>{setActiveScenario(i); setProjectNotes(""); setPbResult(""); setPbError(""); setPbLoading(false);}}
|
| | tabIndex={0}
|
| | aria-label={`Activate scenario ${i+1}: ${sc.title}`}>
|
| | <div className="flex items-center mb-1">
|
| | <span className="font-bold text-lg">{sc.title}</span>
|
| | <span className="ml-3 flex flex-wrap gap-1 text-xs">
|
| | {sc.subjects.map((s,j) =>
|
| | <span key={j}
|
| | className={`inline-block rounded px-2 py-0.5 font-semibold ${{
|
| | Mathematics:"bg-indigo-200 text-indigo-900 dark:bg-indigo-700 dark:text-indigo-100",
|
| | Science:"bg-green-200 text-green-900 dark:bg-green-700 dark:text-green-100",
|
| | History:"bg-yellow-200 text-yellow-900 dark:bg-yellow-700 dark:text-yellow-100",
|
| | Art:"bg-pink-200 text-pink-900 dark:bg-pink-700 dark:text-pink-100",
|
| | Geography:"bg-blue-200 text-blue-900 dark:bg-blue-700 dark:text-blue-100",
|
| | "Computer Science":"bg-slate-300 text-slate-900 dark:bg-slate-700 dark:text-slate-100",
|
| | Economics:"bg-orange-200 text-orange-900 dark:bg-orange-700 dark:text-orange-100",
|
| | Literature:"bg-red-200 text-red-900 dark:bg-red-700 dark:text-red-100"
|
| | }[s]||"bg-slate-200 text-slate-700 dark:bg-slate-600 dark:text-slate-200"}`}>{s}</span>
|
| | )}
|
| | </span>
|
| | </div>
|
| | <div className="pl-1 text-base leading-snug text-slate-800 dark:text-slate-200">{sc.application}</div>
|
| | </li>
|
| | )}
|
| | </ul>
|
| | </div>
|
| | )}
|
| | {!d && (
|
| | <div className="text-lg text-center mt-12 text-slate-500 dark:text-slate-300">
|
| | <div className="animate-pulse">Ask AI for critical thinking ideas and scenarios!</div>
|
| | </div>
|
| | )}
|
| | </div>
|
| | );
|
| | }, [aiData, currentSubject, activeScenario, subjectMeta, subjectNotes, currentSubjectNote]);
|
| |
|
| | // Project-Based Learning section (shows when a scenario is active)
|
| | function ProjectPanel() {
|
| | const d = aiData[currentSubject];
|
| | if (!d || activeScenario==null || !d.scenarios[activeScenario]) return null;
|
| | const sc = d.scenarios[activeScenario];
|
| |
|
| | // Handler for project-based learning AI request
|
| | async function askAIForProject() {
|
| | if (!apiKey || !apiKey.startsWith("sk-")) { setPbError("Please enter a valid OpenAI API key."); return; }
|
| | setPbLoading(true); setPbError(""); setPbResult("");
|
| | const thisWeekNote = subjectNotes[currentSubject] ? `This week, the student is studying: "${subjectNotes[currentSubject]}" in ${subjectMeta.name}.` : "";
|
| | const prompt = `
|
| | A student has chosen the following scenario for a project-based learning activity:
|
| | Title: ${sc.title}
|
| | Subjects Involved: ${sc.subjects.join(", ")}
|
| | Real-Life Application: ${sc.application}
|
| | ${thisWeekNote}
|
| |
|
| | Create a detailed, multi-step project-based learning activity that:
|
| | - Guides the student through a meaningful investigation or creation process related to this scenario.
|
| | - Specifies a driving question, expected outcomes, and assessment ideas.
|
| | - Incorporates collaboration, creativity, real-world research, and presentation or product.
|
| | - Is practical for students in middle or high school.
|
| |
|
| | Format your answer as clear markdown with sections: **Driving Question**, **Project Steps**, **Expected Outcomes**, **Assessment Ideas**.
|
| | `.trim();
|
| | try {
|
| | const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
| | method: "POST",
|
| | headers: {
|
| | "Content-Type": "application/json",
|
| | Authorization: `Bearer ${apiKey}`
|
| | },
|
| | body: JSON.stringify({
|
| | model: "gpt-4o-mini",
|
| | messages: [
|
| | { role: "system", content: "You are an expert in project-based learning. Return clear, structured markdown only." },
|
| | { role: "user", content: prompt }
|
| | ],
|
| | temperature: 0.72,
|
| | max_tokens: 650
|
| | })
|
| | });
|
| | if (!res.ok) throw new Error(`API error (${res.status})`);
|
| | const data = await res.json();
|
| | let answer = data.choices[0].message.content.trim();
|
| | if (answer.startsWith("```")) answer = answer.replace(/^```[a-z]*\s*/,'').replace(/```$/,'');
|
| | setPbResult(answer);
|
| | } catch (e) {
|
| | setPbError("Failed to load project activity: " + (e.message || "Unknown error"));
|
| | } finally { setPbLoading(false); }
|
| | }
|
| |
|
| | return (
|
| | <div className="fade-in mt-8 mb-6 max-w-2xl mx-auto glass p-6 rounded-3xl border border-indigo-300 dark:border-indigo-700 shadow-xl">
|
| | <h3 className="text-2xl font-bold mb-2 text-fuchsia-700 dark:text-fuchsia-200">Project-Based Learning: <span className="text-indigo-900 dark:text-indigo-200">{sc.title}</span></h3>
|
| | <div className="mb-2 text-base font-semibold text-emerald-700 dark:text-emerald-200">
|
| | Subjects Involved:
|
| | <span className="ml-2">{sc.subjects.join(", ")}</span>
|
| | </div>
|
| | <div className="mb-2 text-base">
|
| | <span className="font-semibold text-indigo-700 dark:text-indigo-300">Real-Life Connection:</span> {sc.application}
|
| | </div>
|
| | <div className="mb-4 text-slate-700 dark:text-slate-300">
|
| | <span className="font-semibold">Project Steps & Tips:</span>
|
| | <ol className="list-decimal ml-6 mt-1 space-y-1">
|
| | <li>Define the main question/problem you want to solve, inspired by the scenario.</li>
|
| | <li>Research how the connected subjects apply to the scenario—collect data, examples, or cases.</li>
|
| | <li>Design an experiment, model, creative product, or presentation that integrates concepts from each subject.</li>
|
| | <li>Document your findings with evidence, analysis, and reflections.</li>
|
| | <li>Share your project with peers or your teacher—get feedback and discuss real-world impacts.</li>
|
| | </ol>
|
| | </div>
|
| | <textarea
|
| | className="w-full min-h-[80px] rounded-lg border px-3 py-2 bg-white/90 dark:bg-slate-800 border-indigo-200 dark:border-indigo-500 shadow text-base mb-3"
|
| | placeholder="Use this space to brainstorm ideas, outline your project, or write your project plan..."
|
| | value={projectNotes}
|
| | onChange={e=>setProjectNotes(e.target.value)}
|
| | />
|
| | <div className="flex flex-wrap gap-3 mt-1 mb-3">
|
| | <button
|
| | className="px-4 py-1 rounded-lg font-bold text-white bg-fuchsia-600 hover:bg-fuchsia-700 shadow transition"
|
| | onClick={()=>setProjectNotes("")}
|
| | >Clear Notes</button>
|
| | <button
|
| | className="px-4 py-1 rounded-lg font-bold bg-indigo-200 text-indigo-900 dark:bg-indigo-700 dark:text-indigo-100 shadow"
|
| | onClick={()=>setActiveScenario(null)}
|
| | >Close Project</button>
|
| | <button
|
| | className="px-4 py-1 rounded-lg font-bold bg-emerald-500 text-white hover:bg-emerald-600 shadow transition"
|
| | onClick={askAIForProject}
|
| | disabled={pbLoading}
|
| | aria-label="Ask AI for a detailed project-based learning activity"
|
| | >
|
| | {pbLoading ? <span className="animate-pulse">Thinking...</span> : "Ask AI for a Project Plan"}
|
| | </button>
|
| | </div>
|
| | {pbError && <div className="mb-2 px-3 py-2 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 font-semibold text-center">{pbError}</div>}
|
| | {pbResult && (
|
| | <div className="prose prose-indigo dark:prose-invert max-w-full mt-4 border-t pt-4">
|
| | <MarkdownRenderer markdown={pbResult}/>
|
| | </div>
|
| | )}
|
| | </div>
|
| | );
|
| | }
|
| |
|
| | function MarkdownRenderer({markdown}) {
|
| | let html = markdown
|
| | .replace(/(^|\n)### (.*)/g, '$1<h3>$2</h3>')
|
| | .replace(/(^|\n)## (.*)/g, '$1<h2>$2</h2>')
|
| | .replace(/(^|\n)# (.*)/g, '$1<h1>$2</h1>')
|
| | .replace(/\*\*(.*?)\*\*/g, '<b>$1</b>')
|
| | .replace(/\*(.*?)\*/g, '<i>$1</i>')
|
| | .replace(/^\s*-\s+(.*)$/gm, '<ul><li>$1</li></ul>')
|
| | .replace(/^\s*\d+\.\s+(.*)$/gm, '<ol><li>$1</li></ol>')
|
| | .replace(/\n{2,}/g, '<br/><br/>');
|
| | html = html.replace(/<\/ul>\s*<ul>/g, '');
|
| | html = html.replace(/<\/ol>\s*<ol>/g, '');
|
| | return <div dangerouslySetInnerHTML={{__html: html}} />;
|
| | }
|
| |
|
| | function SubjectTabs() {
|
| | return (
|
| | <div className="flex flex-wrap gap-2 mb-4 sm:mb-6">
|
| | {SUBJECTS.map(s =>
|
| | <button key={s.key}
|
| | className={
|
| | "group flex items-center px-4 py-2 rounded-2xl shadow-sm border-2 focus:outline-none text-lg font-semibold tracking-tight transition-all duration-150 " +
|
| | (currentSubject===s.key
|
| | ? `${s.color} bg-gradient-to-r ${s.accent} border-transparent text-white scale-105`
|
| | : "bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-slate-200 hover:scale-105 hover:border-indigo-400 dark:hover:border-indigo-300")
|
| | }
|
| | onClick={() => setCurrentSubject(s.key)}
|
| | aria-label={`Switch to ${s.name}`}
|
| | >
|
| | <span className="mr-2 text-2xl">{s.icon}</span> {s.name}
|
| | </button>
|
| | )}
|
| | </div>
|
| | );
|
| | }
|
| |
|
| | return (
|
| | <div className="min-h-screen flex flex-col items-center pb-16 bg-gradient-to-br from-indigo-100 via-slate-100 to-pink-50 dark:from-slate-900 dark:via-slate-900 dark:to-indigo-950 transition-colors duration-300">
|
| | <header className="w-full max-w-4xl mx-auto pt-10 pb-4">
|
| | <h1 className="text-3xl sm:text-4xl md:text-5xl font-extrabold mb-2 text-slate-800 dark:text-white tracking-tight text-center">
|
| | Interconnected Subject Explorer
|
| | </h1>
|
| | <p className="text-lg sm:text-xl text-center max-w-2xl mx-auto text-slate-600 dark:text-slate-300">
|
| | Explore how your school subjects connect and find real-world meaning—with interactive canvases and instant AI-powered scenarios.
|
| | </p>
|
| | </header>
|
| | <main className="w-full max-w-4xl flex-1 fade-in">
|
| | <section className="rounded-3xl glass shadow-lg p-4 sm:p-8">
|
| | <div className="flex flex-wrap items-center gap-3 mb-6">
|
| | <input
|
| | type="password"
|
| | className="flex-1 px-4 py-2 rounded-lg border bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow text-base focus:ring-2 focus:ring-indigo-400"
|
| | style={{minWidth:160, maxWidth:290}}
|
| | placeholder="OpenAI API key (sk-...)" value={apiKey}
|
| | onChange={e=>setApiKey(e.target.value)}
|
| | aria-label="OpenAI API Key"
|
| | />
|
| | <input
|
| | type="text"
|
| | className="flex-1 px-4 py-2 rounded-lg border bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow text-base focus:ring-2 focus:ring-indigo-400"
|
| | style={{minWidth:160, maxWidth:300}}
|
| | placeholder="Add an extra prompt (optional)" value={inputPrompt}
|
| | onChange={e=>setInputPrompt(e.target.value)}
|
| | aria-label="Additional prompt for AI"
|
| | />
|
| | <button
|
| | className={`px-6 py-2 rounded-xl font-bold text-lg shadow transition-all
|
| | bg-indigo-600 hover:bg-indigo-700 text-white ${aiLoading ? "opacity-50" : ""}`}
|
| | disabled={aiLoading || !apiKey}
|
| | onClick={fetchAIContent}
|
| | aria-label="Generate ideas and scenarios with AI"
|
| | >
|
| | {aiLoading
|
| | ? <span className="animate-pulse flex items-center gap-2">
|
| | <svg className="w-5 h-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
| | <circle className="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
|
| | <path className="opacity-90" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
|
| | </svg>
|
| | Thinking...
|
| | </span>
|
| | : "Ask AI"
|
| | }
|
| | </button>
|
| | </div>
|
| | <SubjectTabs/>
|
| | <div className="min-h-[320px]">{canvasContent}</div>
|
| | {aiError && <div className="mt-6 px-3 py-2 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 font-semibold text-center">{aiError}</div>}
|
| | {ProjectPanel()}
|
| | </section>
|
| | </main>
|
| | <footer className="w-full max-w-3xl text-center mt-10 mb-6 text-slate-500 dark:text-slate-400 text-xs">
|
| | <div>
|
| | <span className="font-bold">Tip:</span> Press <kbd>Ctrl</kbd>+<kbd>K</kbd> to toggle dark mode. <span className="ml-2">No API keys are stored.</span>
|
| | </div>
|
| | <div className="mt-1">
|
| | Made for educational exploration. © {new Date().getFullYear()}
|
| | </div>
|
| | </footer>
|
| | </div>
|
| | );
|
| | }
|
| |
|
| | ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
| | </script>
|
| | </body>
|
| | </html>
|
| |
|