| <!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> | |