// SOCRATICA CLIENT LOGIC
// Global state
let currentChart = null;
let lastRequestData = null;
// Initialize on DOM load
document.addEventListener("DOMContentLoaded", () => {
// Initialise Lucide icons
if (window.lucide) {
window.lucide.createIcons();
}
// Setup Navigation Tabs
setupNavigation();
// Setup Form Submissions
setupForm();
// Load historical sessions & analytics on start
refreshData();
});
// Navigation Handling
function setupNavigation() {
const navWorkspace = document.getElementById("btn-nav-workspace");
const navAnalytics = document.getElementById("btn-nav-analytics");
const navHistory = document.getElementById("btn-nav-history");
const viewWorkspace = document.getElementById("view-workspace");
const viewAnalytics = document.getElementById("view-analytics");
const viewHistory = document.getElementById("view-history");
const navLinks = [navWorkspace, navAnalytics, navHistory];
const views = [viewWorkspace, viewAnalytics, viewHistory];
function switchTab(targetIndex) {
navLinks.forEach((link, idx) => {
if (idx === targetIndex) {
link.classList.add("active");
views[idx].classList.add("active");
} else {
link.classList.remove("active");
views[idx].classList.remove("active");
}
});
// Refresh when tabs change to keep charts up to date
if (targetIndex === 1 || targetIndex === 2) {
refreshData();
}
}
navWorkspace.addEventListener("click", (e) => { e.preventDefault(); switchTab(0); });
navAnalytics.addEventListener("click", (e) => { e.preventDefault(); switchTab(1); });
navHistory.addEventListener("click", (e) => { e.preventDefault(); switchTab(2); });
// Deep linking via hash
const hash = window.location.hash;
if (hash === "#analytics") switchTab(1);
if (hash === "#history") switchTab(2);
}
// Form Handling
function setupForm() {
const form = document.getElementById("predict-form");
const btnSubmit = document.getElementById("btn-submit");
const btnLoader = document.getElementById("btn-loader");
const btnRegenerate = document.getElementById("btn-regenerate");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const field = document.getElementById("field-select").value;
const problem_text = document.getElementById("problem-text").value;
const use_ai = document.getElementById("toggle-use-ai").checked;
const selected_model = document.querySelector('input[name="selected_model"]:checked').value;
lastRequestData = { field, problem_text, use_ai, selected_model };
await runAnalysis(lastRequestData);
});
btnRegenerate.addEventListener("click", async () => {
if (!lastRequestData) return;
// Read latest state of toggle & selected model (they might have changed before click)
lastRequestData.use_ai = document.getElementById("toggle-use-ai").checked;
lastRequestData.selected_model = document.querySelector('input[name="selected_model"]:checked').value;
// Show local loading state on feedback card only, keep scores visible
const feedbackBody = document.getElementById("feedback-text");
feedbackBody.innerHTML = `
Regenerating response based on ${lastRequestData.selected_model} guidance...
`;
try {
const response = await fetch("/predict", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(lastRequestData)
});
if (!response.ok) throw new Error("Server error");
const data = await response.json();
// Re-render only response block
renderResponse(data.response);
} catch (err) {
console.error(err);
feedbackBody.innerHTML = `Failed to regenerate response. Please try again.
`;
}
});
}
// Running Predict API
async function runAnalysis(reqData) {
const btnSubmit = document.getElementById("btn-submit");
const btnLoader = document.getElementById("btn-loader");
// Loading state ON
btnSubmit.disabled = true;
btnLoader.classList.remove("hidden");
try {
const response = await fetch("/predict", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(reqData)
});
if (!response.ok) {
const errBody = await response.json();
throw new Error(errBody.detail || "Prediction failed");
}
const data = await response.json();
// Display Results
document.getElementById("results-empty").classList.add("hidden");
document.getElementById("results-content").classList.remove("hidden");
// 1. Render Response text
renderResponse(data.response);
// 2. Render Model A (RoBERTa)
renderModelResults("model-a", data.model_a);
// 3. Render Model B (BiLSTM)
renderModelResults("model-b", data.model_b);
// Refresh underlying datasets in background
refreshData();
} catch (err) {
console.error(err);
alert(`Error: ${err.message}`);
} finally {
// Loading state OFF
btnSubmit.disabled = false;
btnLoader.classList.add("hidden");
}
}
// Format markdown paragraphs
function renderResponse(text) {
const body = document.getElementById("feedback-text");
if (!text) {
body.innerHTML = "No response generated.
";
return;
}
// Parse response line breaks and simple styling
const paragraphs = text.split("\n\n");
let html = "";
paragraphs.forEach(p => {
p = p.trim();
if (!p) return;
// Render headers if starts with ### or ##
if (p.startsWith("###")) {
html += `${p.replace("###", "").trim()}
`;
} else if (p.startsWith("##")) {
html += `${p.replace("##", "").trim()}
`;
}
// Render lists
else if (p.startsWith("-") || p.startsWith("*")) {
const items = p.split(/\n[-*]/);
html += "";
items.forEach(item => {
const cleaned = item.replace(/^[-*\s]+/, "").trim();
if (cleaned) html += `- ${cleaned}
`;
});
html += "
";
}
// Normal paragraph
else {
// Support simple bolding "**text**" -> "text"
const parsedBold = p.replace(/\*\*(.*?)\*\*/g, "$1");
html += `${parsedBold.replace(/\n/g, "
")}
`;
}
});
body.innerHTML = html;
}
// Icon Mapping helper
const EMOTION_ICONS = {
"Curious": "compass",
"Confused": "help-circle",
"Frustrated": "alert-triangle",
"Confident": "award",
"Bored": "meh"
};
// Render results inside Model Panel
function renderModelResults(prefix, predData) {
const primary = predData.primary_emotion;
const confidence = predData.primary_confidence;
const all = predData.all_emotions;
const mixed = predData.mixed_emotions;
// Primary Badge
const primaryBadge = document.getElementById(`${prefix}-primary-badge`);
const primaryName = document.getElementById(`${prefix}-primary-name`);
const primaryConf = document.getElementById(`${prefix}-primary-conf`);
primaryName.textContent = primary;
primaryConf.textContent = `${Math.round(confidence * 100)}%`;
// Reset background emotion color classes
primaryBadge.className = "emotion-badge-large";
primaryBadge.classList.add(`emotion-${primary.toLowerCase()}`);
// Update primary icon
const iconName = EMOTION_ICONS[primary] || "smile";
primaryBadge.querySelector("i").setAttribute("data-lucide", iconName);
// Mixed Badges
const mixedContainer = document.getElementById(`${prefix}-mixed-container`);
const mixedSection = document.getElementById(`${prefix}-mixed-section`);
mixedContainer.innerHTML = "";
if (mixed && mixed.length > 0) {
mixedSection.classList.remove("hidden");
mixed.forEach(([emotion, val]) => {
const badge = document.createElement("span");
badge.className = `emotion-badge-small emotion-${emotion.toLowerCase()}`;
const icon = document.createElement("i");
icon.setAttribute("data-lucide", EMOTION_ICONS[emotion] || "smile");
icon.style.width = "13px";
icon.style.height = "13px";
badge.appendChild(icon);
badge.appendChild(document.createTextNode(` ${emotion} `));
const scoreSpan = document.createElement("span");
scoreSpan.className = "score";
scoreSpan.textContent = `${Math.round(val * 100)}%`;
badge.appendChild(scoreSpan);
mixedContainer.appendChild(badge);
});
} else {
mixedSection.classList.add("hidden");
}
// Probability Bars breakdown
const barsContainer = document.getElementById(`${prefix}-bars`);
barsContainer.innerHTML = "";
// Sorted by emotion name to maintain alignment between models
const sortedEmotions = Object.keys(all).sort();
sortedEmotions.forEach(emotion => {
const val = all[emotion];
const barRow = document.createElement("div");
barRow.className = "bar-row";
const barInfo = document.createElement("div");
barInfo.className = "bar-info";
const label = document.createElement("span");
label.className = "bar-label";
const labelIcon = document.createElement("i");
labelIcon.setAttribute("data-lucide", EMOTION_ICONS[emotion] || "smile");
label.appendChild(labelIcon);
label.appendChild(document.createTextNode(emotion));
const valSpan = document.createElement("span");
valSpan.className = "bar-val";
valSpan.textContent = `${Math.round(val * 100)}%`;
barInfo.appendChild(label);
barInfo.appendChild(valSpan);
const track = document.createElement("div");
track.className = "bar-track";
const fill = document.createElement("div");
fill.className = `bar-fill bg-${emotion.toLowerCase()}`;
track.appendChild(fill);
barRow.appendChild(barInfo);
barRow.appendChild(track);
barsContainer.appendChild(barRow);
// Trigger width animation with a small timeout
setTimeout(() => {
fill.style.width = `${Math.round(val * 100)}%`;
}, 50);
});
// Re-create injected lucide icons
if (window.lucide) {
window.lucide.createIcons();
}
}
// Fetch and refresh session logs & charts
async function refreshData() {
try {
const response = await fetch("/history");
if (!response.ok) throw new Error("Could not fetch history");
const logs = await response.json();
// Update history UI components
renderLogsTable(logs);
renderAnalytics(logs);
} catch (err) {
console.error("Error refreshing data:", err);
}
}
// Render logs table
function renderLogsTable(logs) {
const tableBody = document.getElementById("history-table-body");
const emptyState = document.getElementById("table-empty-state");
const countBadge = document.getElementById("log-count-badge");
countBadge.textContent = `${logs.length} session${logs.length === 1 ? "" : "s"} logged`;
if (logs.length === 0) {
tableBody.innerHTML = "";
emptyState.classList.remove("hidden");
return;
}
emptyState.classList.add("hidden");
// Sort descending by timestamp (newest first)
const sortedLogs = [...logs].reverse();
let html = "";
sortedLogs.forEach(row => {
const date = new Date(row.timestamp);
const formattedDate = date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Safely retrieve primary emotions
const modelAPrimary = row.model_a_pred?.primary_emotion || "-";
const modelBPrimary = row.model_b_pred?.primary_emotion || "-";
// Render row cells
html += `
| ${formattedDate} |
${row.field} |
${escapeHTML(row.problem_text)} |
${modelAPrimary} |
${modelBPrimary} |
${row.selected_model} |
${row.final_response.includes("fallback") || row.final_response.includes("It looks like") || row.final_response.includes("It is completely okay") ? "Template Fallback" : "Gemini AI"} |
`;
});
tableBody.innerHTML = html;
}
// Helper to escape HTML characters
function escapeHTML(str) {
if (!str) return "";
return str
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Render Analytics Graph using Chart.js
function renderAnalytics(logs) {
const totalSessions = document.getElementById("stat-total-sessions");
const topEmotion = document.getElementById("stat-top-emotion");
const aiCount = document.getElementById("stat-ai-count");
const chartWrapper = document.querySelector(".chart-wrapper");
const chartCanvas = document.getElementById("emotionChart");
const chartEmpty = document.getElementById("chart-empty-state");
totalSessions.textContent = logs.length;
if (logs.length === 0) {
chartEmpty.classList.remove("hidden");
chartCanvas.classList.add("hidden");
topEmotion.textContent = "-";
aiCount.textContent = "0";
return;
}
chartEmpty.classList.add("hidden");
chartCanvas.classList.remove("hidden");
// Aggregation maps
const emotionCounts = {
"Bored": 0,
"Confident": 0,
"Confused": 0,
"Curious": 0,
"Frustrated": 0
};
let geminiCount = 0;
logs.forEach(row => {
// We aggregate by the emotion of the SELECTED model used to guide the assistant
const selected = row.selected_model === "Model B" ? row.model_b_pred : row.model_a_pred;
const emotion = selected?.primary_emotion;
if (emotion in emotionCounts) {
emotionCounts[emotion]++;
}
// Count AI versus fallback template
// Gemini generated prompt texts aren't matching exact fallback substrings
const isFallback = row.final_response.includes("It looks like") ||
row.final_response.includes("It is completely okay") ||
row.final_response.includes("Fantastic! You're approaching") ||
row.final_response.includes("I love your curiosity") ||
row.final_response.includes("I hear you, and it's completely");
if (!isFallback) {
geminiCount++;
}
});
aiCount.textContent = geminiCount;
// Find most frequent emotion
let maxCount = -1;
let popularEmotion = "-";
for (const [emotion, count] of Object.entries(emotionCounts)) {
if (count > maxCount && count > 0) {
maxCount = count;
popularEmotion = emotion;
}
}
topEmotion.textContent = popularEmotion !== "-" ? popularEmotion : "None";
// Render Chart.js Polar Area Chart
const emotionsList = Object.keys(emotionCounts);
const dataValues = emotionsList.map(e => emotionCounts[e]);
// Mapped custom HSL colors for matching branding style
const bgColors = [
"rgba(100, 116, 139, 0.7)", // Bored - Slate Grey
"rgba(16, 185, 129, 0.7)", // Confident - Emerald Green
"rgba(245, 158, 11, 0.7)", // Confused - Amber Orange
"rgba(99, 102, 241, 0.7)", // Curious - Indigo Purple
"rgba(239, 68, 68, 0.7)" // Frustrated - Coral Red
];
const borderColors = [
"rgba(100, 116, 139, 1)",
"rgba(16, 185, 129, 1)",
"rgba(245, 158, 11, 1)",
"rgba(99, 102, 241, 1)",
"rgba(239, 68, 68, 1)"
];
if (currentChart) {
currentChart.destroy();
}
currentChart = new Chart(chartCanvas, {
type: 'polarArea',
data: {
labels: emotionsList,
datasets: [{
data: dataValues,
backgroundColor: bgColors,
borderColor: borderColors,
borderWidth: 1.5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
grid: {
color: "rgba(0, 0, 0, 0.05)"
},
ticks: {
backdropColor: "transparent",
color: "rgba(0, 0, 0, 0.5)",
stepSize: 1
}
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
font: {
family: "'Plus Jakarta Sans', sans-serif",
weight: 600
},
padding: 15
}
}
}
}
});
}