prompt-analyser/frontend/app.js

305 lines
10 KiB
JavaScript

/**
* Prompt Analyzer — Frontend Logic
*/
const API_BASE = window.location.origin;
let currentAnalysisId = null;
let currentResult = null;
// ── Character count ───────────────────────────────────────────
const promptInput = document.getElementById('prompt-input');
const charCount = document.getElementById('char-count');
promptInput.addEventListener('input', () => {
const len = promptInput.value.length;
charCount.textContent = `${len.toLocaleString()} characters`;
});
// ── Analyze ───────────────────────────────────────────────────
async function analyzePrompt() {
const prompt = promptInput.value.trim();
if (!prompt) {
showToast('Please enter a prompt to analyze', 'warning');
return;
}
const context = document.getElementById('context-input').value.trim() || null;
const projectId = document.getElementById('project-input').value.trim() || null;
const btn = document.getElementById('analyze-btn');
btn.disabled = true;
hideResults();
hideError();
showLoading();
try {
const response = await fetch(`${API_BASE}/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
context,
project_id: projectId,
}),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${response.status}`);
}
const result = await response.json();
currentResult = result;
currentAnalysisId = result.analysis_id;
hideLoading();
renderResults(result);
} catch (error) {
hideLoading();
showError(error.message);
} finally {
btn.disabled = false;
}
}
// ── Render Results ────────────────────────────────────────────
function renderResults(result) {
const section = document.getElementById('results-section');
section.classList.remove('hidden');
// Overall score
const score = result.overall_score;
const scoreCard = document.getElementById('overall-score-card');
const scoreRing = document.getElementById('score-ring');
const scoreValue = document.getElementById('overall-score');
const scoreLabel = document.getElementById('overall-label');
// Remove old class
scoreCard.className = 'overall-score-card';
if (score >= 85) {
scoreCard.classList.add('score-excellent');
scoreLabel.textContent = 'Excellent — This prompt is well-crafted';
} else if (score >= 65) {
scoreCard.classList.add('score-good');
scoreLabel.textContent = 'Good — Minor improvements possible';
} else if (score >= 40) {
scoreCard.classList.add('score-fair');
scoreLabel.textContent = 'Fair — Several issues to address';
} else {
scoreCard.classList.add('score-poor');
scoreLabel.textContent = 'Poor — Major improvements needed';
}
// Animate ring
const circumference = 2 * Math.PI * 52; // r=52
const offset = circumference - (score / 100) * circumference;
scoreRing.style.strokeDashoffset = offset;
// Animate number
animateNumber(scoreValue, score);
// Dimension scores
const dimensions = ['clarity', 'token_efficiency', 'goal_alignment', 'structure', 'vagueness_index'];
dimensions.forEach(dim => {
const data = result.scores[dim];
const numEl = document.querySelector(`[data-dimension="${dim}"]`);
const reasoningEl = document.getElementById(`reasoning-${dim}`);
animateNumber(numEl, data.score);
reasoningEl.textContent = data.reasoning;
// Color the score
const card = document.getElementById(`score-${dim}`);
card.style.borderColor = getScoreColor(data.score);
numEl.style.color = getScoreColor(data.score);
});
// Mistakes
const mistakesList = document.getElementById('mistakes-list');
const mistakeCount = document.getElementById('mistake-count');
const mistakes = result.mistakes || [];
mistakeCount.textContent = `${mistakes.length} issue${mistakes.length !== 1 ? 's' : ''}`;
mistakeCount.className = mistakes.length === 0 ? 'badge badge-success' : 'badge badge-error';
if (mistakes.length === 0) {
mistakesList.innerHTML = `
<div style="text-align:center; padding:20px; color:var(--accent-green);">
<span class="material-symbols-outlined" style="font-size:32px;">check_circle</span>
<p style="margin-top:8px;">No issues found — great prompt!</p>
</div>
`;
} else {
mistakesList.innerHTML = mistakes.map(m => `
<div class="mistake-item">
<div class="mistake-icon">
<span class="material-symbols-outlined">${getMistakeIcon(m.type)}</span>
</div>
<div class="mistake-content">
<div class="mistake-type">${formatMistakeType(m.type)}</div>
${m.text ? `<div class="mistake-text">"${escapeHtml(m.text)}"</div>` : ''}
<div class="mistake-suggestion">${escapeHtml(m.suggestion)}</div>
</div>
</div>
`).join('');
}
// Rewrite comparison
document.getElementById('original-text').textContent = result.original_prompt;
document.getElementById('rewritten-text').textContent = result.rewritten_prompt;
const tc = result.token_comparison;
document.getElementById('original-tokens').textContent = `${tc.original_tokens} tokens`;
document.getElementById('rewritten-tokens').textContent = `${tc.rewritten_tokens} tokens`;
document.getElementById('savings-text').textContent = `${tc.savings_percent}% saved`;
// Scroll to results
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// ── Rewrite Choice ────────────────────────────────────────────
async function useRewrite() {
if (!currentResult) return;
// Paste rewritten prompt into the input textarea
promptInput.value = currentResult.rewritten_prompt;
charCount.textContent = `${promptInput.value.length.toLocaleString()} characters`;
// Also copy to clipboard
try {
await navigator.clipboard.writeText(currentResult.rewritten_prompt);
showToast('Rewritten prompt pasted into input & copied to clipboard!', 'success');
} catch {
showToast('Rewritten prompt pasted into input!', 'success');
}
// Scroll back to prompt input
promptInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
promptInput.focus();
if (currentAnalysisId) {
fetch(`${API_BASE}/rewrite-choice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ analysis_id: currentAnalysisId, used_rewrite: true }),
}).catch(() => { });
}
}
async function keepOriginal() {
if (!currentResult) return;
try {
await navigator.clipboard.writeText(currentResult.original_prompt);
showToast('Original prompt copied to clipboard!', 'success');
} catch {
showToast('Could not copy to clipboard', 'warning');
}
if (currentAnalysisId) {
fetch(`${API_BASE}/rewrite-choice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ analysis_id: currentAnalysisId, used_rewrite: false }),
}).catch(() => { });
}
}
// ── Helpers ───────────────────────────────────────────────────
function animateNumber(el, target) {
let current = 0;
const duration = 1200;
const stepTime = 16;
const steps = duration / stepTime;
const increment = target / steps;
const timer = setInterval(() => {
current += increment;
if (current >= target) {
current = target;
clearInterval(timer);
}
el.textContent = Math.round(current);
}, stepTime);
}
function getScoreColor(score) {
if (score >= 85) return 'var(--accent-green)';
if (score >= 65) return 'var(--accent-blue)';
if (score >= 40) return 'var(--accent-amber)';
return 'var(--accent-red)';
}
function getMistakeIcon(type) {
const icons = {
vague_instruction: 'blur_on',
missing_context: 'help_outline',
redundancy: 'content_copy',
contradiction: 'sync_problem',
poor_formatting: 'format_align_left',
missing_output_format: 'output',
unclear_scope: 'unfold_more',
overly_complex: 'device_hub',
};
return icons[type] || 'warning';
}
function formatMistakeType(type) {
return type.replace(/_/g, ' ');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showLoading() {
document.getElementById('loading-section').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-section').classList.add('hidden');
}
function hideResults() {
document.getElementById('results-section').classList.add('hidden');
}
function showError(message) {
document.getElementById('error-message').textContent = message;
document.getElementById('error-section').classList.remove('hidden');
}
function hideError() {
document.getElementById('error-section').classList.add('hidden');
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
const icon = document.getElementById('toast-icon');
const msg = document.getElementById('toast-message');
icon.textContent = type === 'success' ? 'check_circle' : 'warning';
icon.style.color = type === 'success' ? 'var(--accent-green)' : 'var(--accent-amber)';
msg.textContent = message;
toast.classList.remove('hidden');
setTimeout(() => toast.classList.add('hidden'), 3000);
}
// Allow Ctrl+Enter to submit
promptInput.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
analyzePrompt();
}
});