Hma47's picture
Upload 4 files
0c140a6 verified
<!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. &copy; {new Date().getFullYear()}
</div>
</footer>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>