Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"axios": "^1.11.0",
"bootstrap": "^5.3.7",
"file-saver": "^2.0.5",
"jspdf": "^4.2.1",
"papaparse": "^5.5.3",
"react": "^18.3.1",
"react-bootstrap": "^2.10.10",
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/components/ChatPanel/MessageBubble.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";

import ReactMarkdown from "react-markdown";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import { exportQuizToPdf, exportQuizToWord } from "../../utils/quizExporter";

// Strict allowlist for AI-generated markdown content.
//
Expand Down Expand Up @@ -44,6 +45,7 @@ const MODE_BADGE = {
socratic: { label: "Socratic", bg: "rgba(139,92,246,0.15)", color: "#8B5CF6" },
eli5: { label: "Simple", bg: "rgba(34,197,94,0.15)", color: "#22C55E" },
concise: { label: "Concise", bg: "rgba(249,115,22,0.15)", color: "#F97316" },
quiz: { label: "Quiz", bg: "rgba(139,92,246,0.15)", color: "#8B5CF6" },
};

const MessageBubble = ({ msg, darkMode, onOpenSource }) => {
Expand Down Expand Up @@ -109,6 +111,44 @@ const MessageBubble = ({ msg, darkMode, onOpenSource }) => {
<span>{msg.text}</span>
)}

{msg.role === "bot" && !msg.streaming && (msg.mode === "quiz" || msg.text.includes("# Quiz") || msg.text.toLowerCase().includes("quiz:")) && (
<div style={{ marginTop: "14px", display: "flex", gap: "10px" }}>
<button
onClick={() => exportQuizToPdf(msg.text, "quiz")}
style={{
padding: "6px 14px",
borderRadius: "10px",
border: "none",
background: "linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%)",
color: "white",
fontWeight: 600,
fontSize: "12px",
cursor: "pointer",
boxShadow: "0 4px 10px rgba(139, 92, 246, 0.2)",
transition: "all 0.2s ease"
}}
>
📄 Download PDF
</button>
<button
onClick={() => exportQuizToWord(msg.text, "quiz")}
style={{
padding: "6px 14px",
borderRadius: "10px",
border: darkMode ? "1px solid rgba(255,255,255,0.15)" : "1px solid rgba(0,0,0,0.15)",
background: "transparent",
color: darkMode ? "#D1D5DB" : "#4B5563",
fontWeight: 600,
fontSize: "12px",
cursor: "pointer",
transition: "all 0.2s ease"
}}
>
📝 Download Word
</button>
</div>
)}
Comment on lines +114 to 150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard quiz-export predicate against non-string msg.text.

Line [114] calls .includes and .toLowerCase() directly on msg.text; a null/non-string payload will throw during render and break the chat view.

Proposed fix
 const MessageBubble = ({ msg, darkMode, onOpenSource }) => {
+  const messageText = typeof msg.text === "string" ? msg.text : "";
+  const isQuizMessage =
+    msg.role === "bot" &&
+    !msg.streaming &&
+    (msg.mode === "quiz" ||
+      messageText.includes("# Quiz") ||
+      messageText.toLowerCase().includes("quiz:"));

   const getSourceLabel = (source) => source.document || "Source Document";
   const hasOpenablePage = (source) => Boolean(source.page && source.document);
@@
-            <ReactMarkdown rehypePlugins={[[rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}>{msg.text}</ReactMarkdown>
+            <ReactMarkdown rehypePlugins={[[rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}>{messageText}</ReactMarkdown>
@@
-        {msg.role === "bot" && !msg.streaming && (msg.mode === "quiz" || msg.text.includes("# Quiz") || msg.text.toLowerCase().includes("quiz:")) && (
+        {isQuizMessage && (
           <div style={{ marginTop: "14px", display: "flex", gap: "10px" }}>
             <button
-              onClick={() => exportQuizToPdf(msg.text, "quiz")}
+              onClick={() => exportQuizToPdf(messageText, "quiz")}
@@
             <button
-              onClick={() => exportQuizToWord(msg.text, "quiz")}
+              onClick={() => exportQuizToWord(messageText, "quiz")}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/ChatPanel/MessageBubble.jsx` around lines 114 - 150,
The predicate that decides whether to render the quiz-export buttons calls
msg.text.includes and msg.text.toLowerCase() which will throw if msg.text is
null or not a string; update the conditional in MessageBubble.jsx to guard
msg.text by checking typeof msg.text === "string" (e.g. typeof msg.text ===
"string" && (msg.mode === "quiz" || msg.text.includes("# Quiz") ||
msg.text.toLowerCase().includes("quiz:"))), so the
exportQuizToPdf/exportQuizToWord handlers are only shown when msg.text is a
string.


{msg.role === "bot" && msg.mode && msg.mode !== "default" && (() => {
const badge = MODE_BADGE[msg.mode] || MODE_BADGE.default;
return (
Expand Down
Loading
Loading