diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bae10a67..2494bab0 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -16,6 +16,6 @@ dependencies = [ [dependency-groups] dev = [ "black>=25.9.0", - "pyright>=1.1.407", + "pyright[nodejs]>=1.1.407", "ruff>=0.14.3", ] diff --git a/backend/src/routers/pages.py b/backend/src/routers/pages.py index da6b87b8..2f44d4fa 100644 --- a/backend/src/routers/pages.py +++ b/backend/src/routers/pages.py @@ -129,12 +129,35 @@ async def create_page_with_gpt( print("Content", content) print("Props Block") - props = page.props if isinstance(page.props, dict) else {} - props_dict = ( - json.loads(props) if isinstance(props, str) else props - ) # Ensure it's a dictionary + raw_props = page.props + if isinstance(raw_props, str): + try: + props_dict = json.loads(raw_props) + except Exception: + props_dict = {} + elif isinstance(raw_props, dict): + props_dict = raw_props + else: + props_dict = {} + + props = dict(props_dict) print(json.dumps(props_dict, indent=2)) - question_text = props_dict.get("question", "No question found") + content_lines = ( + [c for c in content if isinstance(c, str)] if isinstance(content, list) else [] + ) + content_question = next( + (line.strip() for line in content_lines if "?" in line and line.strip()), + "", + ) + question_from_page = ( + page.questions[0].question + if page.questions and getattr(page.questions[0], "question", "") + else "" + ) + + question_text = ( + props_dict.get("question") or question_from_page or content_question or "" + ) print("Question", question_text) follow_up_question = props_dict.get("followUpQuestion", "") follow_up_answers = props_dict.get("followUpAnswers", []) @@ -158,6 +181,18 @@ async def create_page_with_gpt( ), "", ) + if ( + not answer_options + and page.questions + and getattr(page.questions[0], "options", None) + ): + answer_options = page.questions[0].options + if ( + not correct_answer + and page.questions + and getattr(page.questions[0], "answer", None) + ): + correct_answer = page.questions[0].answer print("Answer options ", answer_options) print("Correct Answer ", correct_answer) statements = props_dict.get("statements", []) @@ -168,8 +203,15 @@ async def create_page_with_gpt( condition: str = props_dict.get("condition", "") print("Logical ", condition) + existing_hints = props_dict.get("gptHints") + stale_hints = isinstance(existing_hints, list) and any( + isinstance(h, dict) + and "no question found" in str(h.get("statement", "")).lower() + for h in existing_hints + ) + # Check if GPT hints already exist - if not props_dict.get("gptHints"): + if not existing_hints or stale_hints: # Only generate hints if they are not already present gptHints = await generate_gpt_hints( bookId, @@ -188,18 +230,22 @@ async def create_page_with_gpt( code, ) - props["gptHints"] = gptHints - - page_return = await db.page.update( - where={"id": page.id}, - data={ - "pageNumber": pageId, - "content": Json(content), - "image": page.image if page.image else "/images/blank.png", - "props": Json(props), - }, - include={"book": False}, - ) + if gptHints: + props["gptHints"] = gptHints + + page_return = await db.page.update( + where={"id": page.id}, + data={ + "pageNumber": pageId, + "content": Json(content), + "image": page.image if page.image else "/images/blank.png", + "props": Json(props), + }, + include={"book": False}, + ) + else: + print("Warning: generate_gpt_hints returned empty list, not persisting.") + page_return = page else: print("GPT hints already exist. Skipping generation.") page_return = page @@ -225,49 +271,71 @@ async def generate_gpt_hints( ) -> list[dict]: system_message = """ - You are Hint Generator helping young children (ages 5-7) learn advanced programming concepts in a fun and understandable way. These children are using interactive activity books from the CodeKids platform. + You are Hint Generator helping young children (ages 5-7) learn advanced programming concepts in a fun and understandable way. These children are using interactive activity books from the CodeKids platform. - Your job is to help them answer questions from these books, without directly giving away the answer. Instead, guide them with simple,concise and progressive hints that build on their understanding. + Your job is to help them answer questions from these books, without directly giving away the answer. Instead, guide them with simple, concise and progressive hints that build on their understanding. - Focus on: - - Providing step-by-step hints, starting from simple conceptual reminders and building toward deeper insight specific to the problem topic and content. - - Using content-appropriate analogies (e.g., boxes, toys, animals, simple real-world examples). - - Encouraging critical thinking. - - Reinforcing correct patterns without using complex terms. - - Keeping a positive and supportive tone. + Focus on: + - Providing step-by-step hints, starting from simple conceptual reminders and building toward deeper insight specific to the problem topic and content. + - Using content-appropriate analogies (e.g., boxes, toys, animals, simple real-world examples). + - Encouraging critical thinking. + - Reinforcing correct patterns without using complex terms. + - Keeping a positive and supportive tone. - Each hint should: - 1. Be one step closer to helping them solve the question. - 2. Encourage curiosity and exploration. - 3. Include minimum 2 and a maximum of 4 hints + Each hint should: + 1. Be one step closer to helping them solve the question. + 2. Encourage curiosity and exploration. + 3. Include minimum 2 and a maximum of 4 hints. - Never give the direct answer unless explicitly asked. Keep the experience playful, supportive, and confidence-building. - """ + Never give the direct answer unless explicitly asked. Keep the experience playful, supportive, and confidence-building. + """ user_message = f""" Book ID: {bookId} Page ID: {pageId} - Topic:{title} + Topic: {title} Content: {content} - Code:{code} - Question:{question} or {condition} - Options:{options} or {statements} - Answer:{correct_answer} or{ans} + Code: {code} + Question: {question} or {condition} + Options: {options} or {statements} + Answer: {correct_answer} or {ans} Follow-Up Question: {follow_up_question} Follow-Up Options: {follow_up_options} Follow-Up Answer: {follow_up_correct_answer} - - Generate your output in this format: + Generate your output in this format: [ - {{ "statement": "First hint goes here." }}, - {{ "statement": "Second hint goes here." }}, - {{ "statement": "Third hint goes here." }} + {{ "statement": "First hint goes here." }}, + {{ "statement": "Second hint goes here." }}, + {{ "statement": "Third hint goes here." }} ] Return ONLY this list. Do not explain anything else. + """ -""" + print( + "OPENAI_API_KEY =", + settings.OPENAI_API_KEY[:10] if settings.OPENAI_API_KEY else "EMPTY", + ) + print("DEV_MODE =", settings.DEV_MODE) + + if settings.DEV_MODE or settings.OPENAI_API_KEY in ("openai-api-key", ""): + print("USING MOCK HINTS") + safe_question = question.strip() if question else "" + question_hint = ( + f"Look carefully at the question: '{safe_question}'. Which part helps most?" + if safe_question + else "Read the task instructions carefully and focus on the exact thing being asked." + ) + return [ + { + "statement": f"Think about what '{title}' means. What is the main idea of this page?" + }, + {"statement": question_hint}, + {"statement": "Try removing the wrong choices first."}, + ] + + print("CALLING OPENAI NOW") try: response = await client.chat.completions.create( @@ -280,11 +348,14 @@ async def generate_gpt_hints( ) gpt_text = response.choices[0].message.content or "" + print("RAW OPENAI RESPONSE:", gpt_text) + hints = parse_gpt_hints(gpt_text) + print("PARSED HINTS:", hints) return hints except Exception as e: - print("GPT-4 API Error:", e) + print("GPT API Error:", e) return [] diff --git a/backend/uv.lock b/backend/uv.lock index cb70b637..efc05f53 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -104,7 +104,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "black" }, - { name = "pyright" }, + { name = "pyright", extra = ["nodejs"] }, { name = "ruff" }, ] @@ -122,7 +122,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=25.9.0" }, - { name = "pyright", specifier = ">=1.1.407" }, + { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.407" }, { name = "ruff", specifier = ">=0.14.3" }, ] @@ -732,6 +732,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] +[[package]] +name = "nodejs-wheel-binaries" +version = "24.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/87/e5755ad739daafce2e152ab609293d65e6c663b399e28a4bbcd0f4af1f45/nodejs_wheel_binaries-24.14.1.tar.gz", hash = "sha256:d00ae0c86d7e1bfa798e8f8ad282db751af157cdcaa1208a1b9a2cf2a85ac821", size = 8056, upload-time = "2026-03-31T14:07:27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/b7/9765d9a5d3b95475829ef5965d4a4f6f4badb034ee4e18c2d5f8b9b65d6f/nodejs_wheel_binaries-24.14.1-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9e856ba0f2d3d2659869e6e0f4cae6874faeeeca7f879131a88451356373ac4", size = 54945603, upload-time = "2026-03-31T14:06:58.526Z" }, + { url = "https://files.pythonhosted.org/packages/6f/15/bc2fa51ee31ce597b2af1905081e5a5add07fe0cf619bfa531d7df2f1f1b/nodejs_wheel_binaries-24.14.1-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:634f57829ebfdfe95d096f32a50c5cdd3a6c72a94dcf2b92a8bef9868cccb13e", size = 55119951, upload-time = "2026-03-31T14:07:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/a6/dd/92ff0831262af4bbb5473d4e7964fd27afb0901a2690a6ff7bc3d220d97f/nodejs_wheel_binaries-24.14.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:404b563467129e6a0ea7006a38b3d8af0ebfbc340b31a6a0af2c59ea3af90b7c", size = 59487620, upload-time = "2026-03-31T14:07:06.198Z" }, + { url = "https://files.pythonhosted.org/packages/45/36/bbbee3adf6afd00944e5a86ebd64987dea90bd347090155a4989dc3e8594/nodejs_wheel_binaries-24.14.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:7863c62f8a3946b727831f71375a9ae00205b3258478476034b49c3a1d57ac12", size = 59986044, upload-time = "2026-03-31T14:07:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/05/16/119e4168bf7ed17ad7961d122701c75ac86135fa243a958b960a3f1b7055/nodejs_wheel_binaries-24.14.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a3f64daa1235fa6a83c778ded98d5fe4e74979ca54aa2ffb807f0805c57c3abe", size = 61489823, upload-time = "2026-03-31T14:07:13.378Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a6/d581996827b9d1133094dc347f1c4e3d2a70557973ce7a427a03337b1427/nodejs_wheel_binaries-24.14.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:810a48ce096925ead0690f7d143e48fb902ebfc9212097e8f6cb3ac6cbe8f314", size = 62069740, upload-time = "2026-03-31T14:07:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/396d1a48cbf3d5899461bda12fa97ae4010d7e4013e7f28230cb04af5818/nodejs_wheel_binaries-24.14.1-py2.py3-none-win_amd64.whl", hash = "sha256:7a087b6a727fb9242d1cc83c8b121711bd0e9686408d27de48b34b23dfb26ac5", size = 41400067, upload-time = "2026-03-31T14:07:20.513Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/adb21cf549934579e98934531e7f9b038d583fc9c2dd4b82ea01cc31bdd2/nodejs_wheel_binaries-24.14.1-py2.py3-none-win_arm64.whl", hash = "sha256:978fdfe76624c48111ab99ed0f99f9d4c1c682e420b0212ac9e1daee52f20283", size = 39096873, upload-time = "2026-03-31T14:07:23.977Z" }, +] + [[package]] name = "openai" version = "2.6.1" @@ -1106,6 +1122,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, ] +[package.optional-dependencies] +nodejs = [ + { name = "nodejs-wheel-binaries" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" diff --git a/frontend/src/components/HokieBirdMap.tsx b/frontend/src/components/HokieBirdMap.tsx index 7278a587..26d14bb0 100644 --- a/frontend/src/components/HokieBirdMap.tsx +++ b/frontend/src/components/HokieBirdMap.tsx @@ -41,6 +41,7 @@ export function HokieBirdMap({ function handleOnDragStatement(e: React.DragEvent, statement: string) { e.dataTransfer.setData("statement", statement); + e.dataTransfer.effectAllowed = "copyMove"; } function handleDragOver(e: React.DragEvent) { @@ -123,6 +124,16 @@ export function HokieBirdMap({ setProcedures(newProcedures); } + function clearProcedure(index: number) { + const newProcedures = [...procedures]; + newProcedures[index] = ""; + setProcedures(newProcedures); + if (errorProcedure === index) { + setErrorProcedure(null); + setMessage(null); + } + } + function resetAll() { setProcedures(blankProcedures); setMessage(null); @@ -156,11 +167,23 @@ export function HokieBirdMap({ disabled={!isFireFox && !props.type} value={statement} onChange={(e) => setProcedure(index, e.target.value)} + onDrop={(e) => handleOnDropStatement(e, index)} + onDragOver={(e) => handleDragOver(e)} /> {errorProcedure === index && (