Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
56 changes: 55 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ <h1 id="session-name-label">Session</h1>
</div>
</div>
<div class="window-actions no-drag">
<button id="open-cram-mode" class="ghost-button" type="button">Cram Mode</button>
<button id="shrink-window" class="icon-button" type="button" aria-label="Pin overlay">
<svg class="icon-svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 6h10v2H7zM7 11h10v2H7zM7 16h10v2H7z"></path>
Expand All @@ -44,7 +45,7 @@ <h1 id="session-name-label">Session</h1>
</header>

<section class="content">
<section class="session-layout no-drag">
<section id="home-session-view" class="session-layout no-drag">
<section class="chat-shell session-chat-shell">
<div id="chat-thread" class="chat-thread">
<article class="chat-message assistant">
Expand Down Expand Up @@ -75,6 +76,59 @@ <h1 id="session-name-label">Session</h1>
</form>
</section>
</section>
<section id="cram-intake-view" class="chat-shell cram-flow-card no-drag" hidden>
<div class="cram-flow-header">
<p class="eyebrow">Cram Mode</p>
<h2 class="cram-flow-title">Build your exam game plan</h2>
</div>
<form id="cram-form" class="cram-form">
<label class="cram-field">
<span>Exam name</span>
<input id="cram-exam-name" name="examName" type="text" placeholder="e.g. Organic Chemistry Midterm" required />
</label>
<label class="cram-field">
<span>Time left</span>
<select id="cram-time-left" name="timeLeft" required>
<option value="">Select time</option>
<option value="Tonight">Tonight</option>
<option value="2 days">2 days</option>
<option value="3 days">3 days</option>
<option value="1 week">1 week</option>
</select>
</label>
<label class="cram-field">
<span>Exam material</span>
<textarea
id="cram-material"
name="material"
rows="6"
placeholder="Paste slides, topics, formulas, or a rough study guide..."
required
></textarea>
</label>
<label class="cram-field">
<span>Notes (optional)</span>
<textarea id="cram-notes" name="notes" rows="3" placeholder="Anything to prioritize or avoid?"></textarea>
</label>
<p id="cram-intake-empty-state" class="cram-empty-state" hidden>Please fill exam name, time left, and exam material.</p>
<div class="cram-form-actions">
<button id="cram-cancel" class="ghost-button" type="button">Back</button>
<button id="cram-submit" class="continue-button" type="submit">Generate Cram Plan</button>
</div>
</form>
</section>
<section id="cram-results-view" class="chat-shell cram-flow-card no-drag" hidden>
<div class="cram-flow-header cram-results-header">
<div>
<p class="eyebrow">Cram Mode Results</p>
<h2 id="cram-results-title" class="cram-flow-title">Your plan</h2>
</div>
<button id="cram-start-over" class="ghost-button" type="button">Start Over</button>
</div>
<p id="cram-results-loading" class="cram-loading-copy" hidden>Building your cram plan...</p>
<p id="cram-results-empty-state" class="cram-empty-state" hidden>No cram plan yet. Submit your intake to get started.</p>
<div id="cram-results-sections" class="cram-results-sections"></div>
</section>
</section>

<section class="compact-strip no-drag">
Expand Down
256 changes: 256 additions & 0 deletions src/renderer.js
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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",
Expand All @@ -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";

Expand All @@ -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)) {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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/"));
Expand Down Expand Up @@ -1027,6 +1282,7 @@ window.addEventListener("DOMContentLoaded", async () => {
applySession(session);
attachResizeHandle(resizeHandle);
renderAttachmentStatus();
showCramHome();
});

window.addEventListener("resize", scheduleFitText);
Loading