@@ -75,6 +76,59 @@ Session + + + Cram Mode + Build your exam game plan + + + + Exam name + + + + Time left + + Select time + Tonight + 2 days + 3 days + 1 week + + + + Exam material + + + + Notes (optional) + + + Please fill exam name, time left, and exam material. + + Back + Generate Cram Plan + + + + + + + Cram Mode Results + Your plan + + Start Over + + Building your cram plan... + No cram plan yet. Submit your intake to get started. + + diff --git a/src/renderer.js b/src/renderer.js index fa230bc..64a1db2 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -1,5 +1,6 @@ const root = document.querySelector(".window-shell"); const themeIconToggle = document.querySelector("#theme-icon-toggle"); +const openCramModeButton = document.querySelector("#open-cram-mode"); const shrinkWindow = document.querySelector("#shrink-window"); const stopSessionButton = document.querySelector("#stop-session"); const closeWindow = document.querySelector("#close-window"); @@ -16,6 +17,22 @@ const chatAttachMenu = document.querySelector("#chat-attach-menu"); const attachScreenshotButton = document.querySelector("#attach-screenshot-button"); const attachClipboardButton = document.querySelector("#attach-clipboard-button"); const resizeHandle = document.querySelector("#resize-handle"); +const homeSessionView = document.querySelector("#home-session-view"); +const cramIntakeView = document.querySelector("#cram-intake-view"); +const cramResultsView = document.querySelector("#cram-results-view"); +const cramForm = document.querySelector("#cram-form"); +const cramExamName = document.querySelector("#cram-exam-name"); +const cramTimeLeft = document.querySelector("#cram-time-left"); +const cramMaterial = document.querySelector("#cram-material"); +const cramNotes = document.querySelector("#cram-notes"); +const cramCancelButton = document.querySelector("#cram-cancel"); +const cramSubmitButton = document.querySelector("#cram-submit"); +const cramStartOverButton = document.querySelector("#cram-start-over"); +const cramResultsTitle = document.querySelector("#cram-results-title"); +const cramResultsLoading = document.querySelector("#cram-results-loading"); +const cramResultsSections = document.querySelector("#cram-results-sections"); +const cramIntakeEmptyState = document.querySelector("#cram-intake-empty-state"); +const cramResultsEmptyState = document.querySelector("#cram-results-empty-state"); const ACTION_LABELS = { chat: "Ask SideKlick", @@ -30,6 +47,8 @@ const ACTION_LABELS = { }; const STREAM_CHUNK_SIZE = 3; const STREAM_INTERVAL_MS = 22; +const CRAM_MATERIAL_PREVIEW_LINES = 3; +const CRAM_LOADING_DELAY_MS = 700; const THUMBS_UP = "\uD83D\uDC4D"; const THUMBS_DOWN = "\uD83D\uDC4E"; @@ -39,6 +58,7 @@ let screenshotDataUrl = null; let clipboardAttachmentText = null; let attachmentStatusRow = null; let fitTextFrame = null; +let isCramLoading = false; function fitTextToBox(element, minimumFontSize = 11) { if (!(element instanceof HTMLElement)) { @@ -283,6 +303,166 @@ function setMode(mode) { root.dataset.mode = mode; } +function setCramLoading(loading) { + isCramLoading = loading; + if (cramResultsLoading) { + cramResultsLoading.hidden = !loading; + } + if (cramSubmitButton) { + cramSubmitButton.disabled = loading; + } + if (cramCancelButton) { + cramCancelButton.disabled = loading; + } +} + +function showCramIntake() { + if (homeSessionView) { + homeSessionView.hidden = true; + } + if (cramResultsView) { + cramResultsView.hidden = true; + } + if (cramResultsEmptyState) { + cramResultsEmptyState.hidden = true; + } + if (cramResultsSections) { + cramResultsSections.replaceChildren(); + } + if (cramIntakeView) { + cramIntakeView.hidden = false; + } + if (cramIntakeEmptyState) { + cramIntakeEmptyState.hidden = true; + } +} + +function showCramHome() { + if (homeSessionView) { + homeSessionView.hidden = false; + } + if (cramIntakeView) { + cramIntakeView.hidden = true; + } + if (cramResultsView) { + cramResultsView.hidden = true; + } + if (cramIntakeEmptyState) { + cramIntakeEmptyState.hidden = true; + } + setCramLoading(false); +} + +function createCramMockPlan({ examName, timeLeft, material, notes }) { + const materialLines = material + .split(/\n+/) + .map((line) => line.trim()) + .filter(Boolean) + .slice(0, CRAM_MATERIAL_PREVIEW_LINES); + const firstMaterialLine = materialLines[0] || "high-yield exam topics"; + + return { + title: `${examName} • ${timeLeft}`, + sections: [ + { + heading: "Study First", + items: [ + `Start with ${firstMaterialLine}.`, + "Review your highest-weight topic before switching contexts.", + ], + }, + { + heading: "Study Next", + items: [ + materialLines[1] || "Work through supporting concepts and examples.", + "Summarize what to memorize on a single sheet.", + ], + }, + { + heading: "Skip If Needed", + items: [materialLines[2] || "Low-yield details that rarely show up directly."], + }, + { + heading: "Likely Questions", + items: [ + `Define the core idea behind ${examName}.`, + "Explain a common comparison or tradeoff in one paragraph.", + "Solve one representative problem from memory.", + ], + }, + { + heading: "Quick Self-Test", + items: [ + "Can you explain the top 3 concepts out loud without notes?", + "Can you answer one likely question in under 2 minutes?", + ], + }, + { + heading: "Tonight's Plan", + items: [ + `Block 1 (25 min): Focused review of ${firstMaterialLine}.`, + "Block 2 (25 min): Practice and correction.", + notes + ? `Final 10 min: Re-read your note — ${notes}.` + : "Final 10 min: Write a fast confidence checklist for tomorrow.", + ], + }, + ], + }; +} + +function renderCramResults(plan) { + if (!cramResultsView || !cramResultsSections || !cramResultsEmptyState) { + return; + } + + if (homeSessionView) { + homeSessionView.hidden = true; + } + if (cramIntakeView) { + cramIntakeView.hidden = true; + } + cramResultsView.hidden = false; + + if (!plan?.sections?.length) { + cramResultsSections.replaceChildren(); + cramResultsEmptyState.hidden = false; + return; + } + + cramResultsEmptyState.hidden = true; + if (cramResultsTitle) { + cramResultsTitle.textContent = plan.title || "Your plan"; + } + + const sections = plan.sections.map((section) => { + const sectionElement = document.createElement("section"); + sectionElement.className = "cram-result-section"; + + const heading = document.createElement("h3"); + heading.textContent = section.heading; + + const list = document.createElement("ul"); + const items = Array.isArray(section.items) ? section.items : []; + if (items.length === 0) { + const emptyItem = document.createElement("li"); + emptyItem.textContent = "No items yet."; + list.appendChild(emptyItem); + } else { + for (const item of items) { + const listItem = document.createElement("li"); + listItem.textContent = item; + list.appendChild(listItem); + } + } + + sectionElement.append(heading, list); + return sectionElement; + }); + + cramResultsSections.replaceChildren(...sections); +} + function createFeedbackRow(interactionId) { const feedbackRow = document.createElement("div"); feedbackRow.className = "chat-feedback-row"; @@ -881,6 +1061,81 @@ compactStarButton.addEventListener("click", async () => { applyThemeState(result); }); +if (openCramModeButton) { + openCramModeButton.addEventListener("click", () => { + showCramIntake(); + if (cramExamName instanceof HTMLElement) { + cramExamName.focus(); + } + }); +} + +if (cramCancelButton) { + cramCancelButton.addEventListener("click", () => { + if (!isCramLoading) { + showCramHome(); + } + }); +} + +if (cramStartOverButton) { + cramStartOverButton.addEventListener("click", () => { + showCramIntake(); + if (cramMaterial instanceof HTMLElement) { + cramMaterial.focus(); + } + }); +} + +if (cramForm) { + cramForm.addEventListener("submit", async (event) => { + event.preventDefault(); + if (isCramLoading) { + return; + } + + const examName = typeof cramExamName?.value === "string" ? cramExamName.value.trim() : ""; + const timeLeft = typeof cramTimeLeft?.value === "string" ? cramTimeLeft.value.trim() : ""; + const material = typeof cramMaterial?.value === "string" ? cramMaterial.value.trim() : ""; + const notes = typeof cramNotes?.value === "string" ? cramNotes.value.trim() : ""; + + if (!examName || !timeLeft || !material) { + if (cramIntakeEmptyState) { + cramIntakeEmptyState.hidden = false; + } + return; + } + + if (cramIntakeEmptyState) { + cramIntakeEmptyState.hidden = true; + } + + setCramLoading(true); + if (cramResultsView) { + cramResultsView.hidden = false; + } + if (cramIntakeView) { + cramIntakeView.hidden = true; + } + if (homeSessionView) { + homeSessionView.hidden = true; + } + + await new Promise((resolve) => { + window.setTimeout(resolve, CRAM_LOADING_DELAY_MS); + }); + + const plan = createCramMockPlan({ + examName, + timeLeft, + material, + notes, + }); + renderCramResults(plan); + setCramLoading(false); + }); +} + chatInput.addEventListener("paste", async (event) => { const items = Array.from(event.clipboardData?.items || []); const imageItem = items.find((item) => item.type.startsWith("image/")); @@ -1027,6 +1282,7 @@ window.addEventListener("DOMContentLoaded", async () => { applySession(session); attachResizeHandle(resizeHandle); renderAttachmentStatus(); + showCramHome(); }); window.addEventListener("resize", scheduleFitText); diff --git a/src/styles.css b/src/styles.css index 0d0d68a..da63818 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2994,6 +2994,129 @@ button { min-width: 104px; } +.cram-flow-card { + height: 100%; + min-height: 0; + padding: 14px; + overflow: auto; +} + +.cram-flow-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.cram-flow-title { + margin: 4px 0 0; + font-family: var(--font-display); + font-size: 1.1rem; + letter-spacing: -0.02em; +} + +.cram-form { + display: grid; + gap: 12px; + margin-top: 8px; +} + +.cram-field { + display: grid; + gap: 6px; + font-size: 0.88rem; +} + +.cram-field > span { + color: var(--dark-muted); +} + +.window-shell[data-tone="light"] .cram-field > span { + color: var(--light-muted); +} + +.cram-field input, +.cram-field select, +.cram-field textarea { + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 12px; + background: rgba(255, 255, 255, 0.04); + color: inherit; + font: inherit; + padding: 10px 12px; + outline: none; +} + +.window-shell[data-tone="light"] .cram-field input, +.window-shell[data-tone="light"] .cram-field select, +.window-shell[data-tone="light"] .cram-field textarea { + border-color: rgba(36, 72, 122, 0.14); + background: rgba(255, 255, 255, 0.66); +} + +.cram-field textarea { + resize: vertical; +} + +.cram-field input:focus, +.cram-field select:focus, +.cram-field textarea:focus { + border-color: rgba(143, 211, 255, 0.42); + box-shadow: 0 0 0 3px rgba(143, 211, 255, 0.12); +} + +.cram-empty-state, +.cram-loading-copy { + margin: 0; + font-size: 0.84rem; + color: var(--dark-muted); +} + +.window-shell[data-tone="light"] .cram-empty-state, +.window-shell[data-tone="light"] .cram-loading-copy { + color: var(--light-muted); +} + +.cram-form-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.cram-results-header { + margin-bottom: 8px; +} + +.cram-results-sections { + display: grid; + gap: 10px; +} + +.cram-result-section { + padding: 12px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); +} + +.window-shell[data-tone="light"] .cram-result-section { + border-color: rgba(36, 72, 122, 0.14); + background: rgba(255, 255, 255, 0.68); +} + +.cram-result-section h3 { + margin: 0 0 8px; + font-size: 0.95rem; +} + +.cram-result-section ul { + margin: 0; + padding-left: 18px; + display: grid; + gap: 6px; +} + .chat-attachment-status-row { display: flex; flex-wrap: wrap; diff --git a/tests/renderer.incoming-payload.test.js b/tests/renderer.incoming-payload.test.js index a510ba3..5134836 100644 --- a/tests/renderer.incoming-payload.test.js +++ b/tests/renderer.incoming-payload.test.js @@ -10,15 +10,49 @@ function createDom() { + + - - + + + + + + + + + + + + + + + + + + Select time + Tonight + + + + + + + + + + + + + + + @@ -217,3 +251,80 @@ test("manual chat submit shows the typed message instead of the action label", a dom.window.close(); }); + +test("cram mode intake submits and renders all mocked result sections", async () => { + const dom = createDom(); + installAnimationFrameStub(dom); + + global.window = dom.window; + global.document = dom.window.document; + global.HTMLElement = dom.window.HTMLElement; + global.Event = dom.window.Event; + global.FileReader = class FakeFileReader {}; + + window.overlayApi = { + getWindowBounds: async () => ({ width: 100, height: 100 }), + resizeWindow: async () => ({}), + setThemeSource: async () => ({ shouldUseDarkColors: false }), + minimizeToDock: async () => ({}), + stopSession: async () => ({}), + closeWindow: async () => ({}), + expandWindow: async () => ({}), + assist: async () => ({ interactionId: 1, answer: "ok", nextStep: "ok" }), + submitFeedback: async () => ({}), + onThemeChanged: () => {}, + onWindowMode: () => {}, + onSessionChanged: () => {}, + onIncomingPayload: () => {}, + captureScreenshotAttachment: async () => null, + readClipboardAttachment: async () => null, + getCurrentSession: async () => ({ + classId: 7, + className: "AP Biology", + sessionName: "Meiosis Review", + sessionNotes: "Need help with vocab", + }), + }; + + const rendererPath = path.join(__dirname, "..", "src", "renderer.js"); + delete require.cache[require.resolve(rendererPath)]; + require(rendererPath); + + await new Promise((resolve) => { + window.dispatchEvent(new dom.window.Event("DOMContentLoaded")); + setTimeout(resolve, 0); + }); + + document.querySelector("#open-cram-mode").click(); + document.querySelector("#cram-exam-name").value = "Calculus Final"; + document.querySelector("#cram-time-left").value = "Tonight"; + document.querySelector("#cram-material").value = + "Derivatives\nIntegrals\nOptimization"; + document.querySelector("#cram-notes").value = "I always miss sign mistakes"; + document + .querySelector("#cram-form") + .dispatchEvent(new dom.window.Event("submit", { bubbles: true, cancelable: true })); + + await waitFor(() => { + const headings = [ + ...document.querySelectorAll("#cram-results-sections .cram-result-section h3"), + ].map((node) => node.textContent); + assert.deepEqual(headings, [ + "Study First", + "Study Next", + "Skip If Needed", + "Likely Questions", + "Quick Self-Test", + "Tonight's Plan", + ]); + }, 2500); + + assert.equal(document.querySelector("#cram-results-view").hidden, false); + assert.equal(document.querySelector("#home-session-view").hidden, true); + assert.equal( + document.querySelector("#cram-results-title").textContent, + "Calculus Final • Tonight", + ); + + dom.window.close(); +});