diff --git a/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/context.md b/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/context.md new file mode 100644 index 0000000..e99a923 --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/context.md @@ -0,0 +1,35 @@ +# Context: SmolTerms Project Landing Page + +## Requirements +- Static landing page at `website/` directory +- Vanilla HTML + Tailwind CSS (CDN) + minimal JS +- No build system required +- Light pastel/offwhite color palette with darker contrast buttons +- Responsive (mobile 375px, desktop 1280px) + +## Sections Required +1. Hero/Header - project name, tagline +2. What it does - privacy policy analyzer, 5 dimensions +3. Motivation - why privacy policies need simplification +4. How it works - extension flow explanation +5. Install - Firefox/Chrome buttons (placeholder URLs) +6. Scoring explanation - 5 dimensions + 4 risk levels with colors +7. Try It (placeholder) - `
` for URL analysis (task-03) +8. Footer - GitHub repo, license + +## Design Decisions +- **Tailwind CSS via CDN** - user preference, no build step needed +- **Color palette**: Offwhite backgrounds (#f8f7f4 / #faf9f6), darker offwhite buttons (#e8e5df / #d4d0c8) +- **Risk level colors from design doc**: Green (8-10), Yellow (5-0-7.9), Orange (3-4.9), Red (1-2.9) +- **Font**: Inter via Google Fonts for clean modern look + +## Scoring System (from design doc) +| Dimension | What it measures | +|-----------|-----------------| +| Data Collection | How much personal data is collected | +| Data Sharing | Whether data is shared/sold to third parties | +| User Rights | Ability to access, delete, export data | +| Retention | How long data is kept | +| Security | Security measures, encryption, breach notification | + +Risk levels: Low (8-10, green), Moderate (5-7.9, yellow), High (3-4.9, orange), Critical (1-2.9, red) diff --git a/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/plan.md b/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/plan.md new file mode 100644 index 0000000..90e7a00 --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/plan.md @@ -0,0 +1,33 @@ +# Plan: SmolTerms Project Landing Page + +## Test Strategy +Since this is a static HTML/CSS/JS page with no build system, traditional unit tests don't apply. Validation: +- Manual browser check: all sections render correctly +- HTML structure verification: `
` placeholder exists +- Responsive: verify layout at 375px and 1280px viewports +- All required sections present in HTML + +## Implementation Plan + +### Files to Create +1. `website/index.html` - Main page with all sections, Tailwind CDN +2. `website/script.js` - Smooth scrolling, mobile nav toggle +3. `website/README.md` - Local dev and GitHub Pages deployment instructions + +### Design Approach +- Tailwind CSS via CDN (no build step) +- Google Fonts: Inter +- Color palette: offwhite backgrounds, darker offwhite/warm gray buttons +- Risk level colors used as accents in scoring section +- Clean, minimal layout with generous whitespace + +### Section Breakdown +1. **Nav** - Fixed top, logo + nav links +2. **Hero** - Large heading, tagline, CTA buttons +3. **What it does** - Brief feature overview +4. **Motivation** - Why SmolTerms exists +5. **How it works** - 3-step visual flow +6. **Install** - Browser extension download buttons +7. **Scoring** - 5 dimensions grid + risk level legend +8. **Try It** - Placeholder section for URL analysis +9. **Footer** - Links, license diff --git a/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/progress.md b/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/progress.md new file mode 100644 index 0000000..4464106 --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/project-landing-page/progress.md @@ -0,0 +1,21 @@ +# Progress: SmolTerms Project Landing Page + +## Setup +- [x] Create documentation directory +- [x] Read task file and design document +- [x] Create context.md + +## Implementation +- [x] Create `website/` directory structure +- [x] Build `index.html` with all sections (hero, features, motivation, how-it-works, install, scoring, try-it, footer) +- [x] Style with Tailwind CSS (CDN) - pastel/offwhite palette with custom cream color scale +- [x] Add `script.js` for smooth scrolling and mobile nav toggle +- [x] Create `website/README.md` with local dev and GitHub Pages deploy instructions +- [x] Validate: all acceptance criteria met (sections, scoring, responsive classes, try-it placeholder) +- [ ] Commit + +## Decisions +- Used Tailwind CDN with custom config (cream color palette, risk level colors) +- Inter font via Google Fonts for clean modern look +- SVG icons inline (no external icon library dependency) +- Firefox/Chrome install buttons use placeholder `#` URLs diff --git a/.agents/scratchpad/2026-02-15-smolterms/website-url-analysis/context.md b/.agents/scratchpad/2026-02-15-smolterms/website-url-analysis/context.md new file mode 100644 index 0000000..4ad5fd6 --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/website-url-analysis/context.md @@ -0,0 +1,75 @@ +# Context: Website URL Analysis Feature + +## Project Structure +- `website/index.html` - Landing page using Tailwind CSS CDN, Inter font, vanilla JS +- `website/script.js` - Mobile menu toggle + smooth scrolling (28 lines) +- No `styles.css` - all styling via Tailwind utility classes inline +- No build system - plain static files served directly + +## Requirements Summary + +### Functional +1. Replace the `
` placeholder (line 279-290) with: + - URL input field with placeholder text + - "Analyze" submit button + - Results container (hidden by default) +2. Form submission flow: + - Client-side URL validation (basic format check) + - Loading state (spinner, disabled button, progress text) + - POST to `{API_BASE_URL}/api/v1/analyze-url` with `{ "url": "..." }` + - Render results or errors +3. Results display: overall score + risk badge, 5 dimension scores, key concerns list, summary, cached indicator +4. Error handling: network errors, fetch failures (suggest extension), rate limiting (429), invalid URL, non-policy content +5. Configurable API base URL (default `http://localhost:8080`) +6. Smooth scroll to results after analysis + +### Backend API Contract +- **Request:** `POST /api/v1/analyze-url` with `{ "url": "..." }` +- **Success Response (200):** `AnalysisResult` struct: + ```json + { + "url": "...", + "overall_score": 7.2, + "risk_level": "moderate", // "low"|"moderate"|"high"|"critical"|"not_policy" + "dimensions": { + "data_collection": { "score": 7.0, "summary": "..." }, + "data_sharing": { "score": 6.5, "summary": "..." }, + "user_rights": { "score": 8.0, "summary": "..." }, + "retention": { "score": 7.5, "summary": "..." }, + "security": { "score": 7.0, "summary": "..." } + }, + "key_concerns": ["concern1", "concern2"], + "summary": "...", + "cached": false, + "analyzed_at": "2026-02-15T..." + } + ``` +- **Error Response:** `{ "error": "..." }` with appropriate HTTP status codes + - 400: Invalid request / empty URL + - 429: Rate limited (5 req/min/IP) + - 502: Fetch failure / analysis failure + - 504: Analysis timeout + +### Risk Level Colors (from Tailwind config) +- Low (8-10): `risk-low` (#4ade80 green) +- Moderate (5-7.9): `risk-moderate` (#facc15 yellow) +- High (3-4.9): `risk-high` (#fb923c orange) +- Critical (1-2.9): `risk-critical` (#f87171 red) + +## Existing Patterns +- Website uses Tailwind CSS CDN - no custom CSS file, all utility classes +- Existing sections use `max-w-3xl` or `max-w-5xl` with `mx-auto`, `px-6`, `py-20` +- Cards use `bg-cream-100 rounded-xl p-5/p-6 border border-cream-300` pattern +- Colors from custom cream palette (cream-50 through cream-900) +- Risk colors already defined in Tailwind config +- JS is vanilla, no framework, no modules +- Try-it section currently has `max-w-3xl mx-auto text-center` layout + +## Implementation Path +1. Edit `website/index.html` - Replace try-it placeholder with form HTML + results container +2. Edit `website/script.js` - Add all JS logic (validation, API calls, rendering, error handling) +3. Add inline ` + + + + + + + +
+
+

+ Understand Privacy Policies
in Seconds +

+

+ SmolTerms is a browser extension that reads the fine print so you don't have to. Get clear privacy scores across 5 dimensions before you click "I agree." +

+ +
+
+ + +
+
+

What SmolTerms Does

+

+ One click to analyze any privacy policy. SmolTerms breaks down complex legal language into clear, actionable scores. +

+
+
+
+ + + +
+

Privacy Scoring

+

+ Get scores across 5 privacy dimensions — data collection, sharing, user rights, retention, and security — rated 1 to 10. +

+
+
+
+ + + +
+

AI-Powered Analysis

+

+ Uses a RAG pipeline and LLMs to read and understand privacy policies the way a privacy expert would. +

+
+
+
+ + + +
+

Key Concerns

+

+ Highlights the most important privacy concerns so you know exactly what to watch out for. +

+
+
+
+
+ + +
+
+

Why SmolTerms Exists

+

+ The average privacy policy is over 4,000 words long. It would take roughly 18 minutes to read — and most people encounter dozens of them every year. Nobody reads them, but everyone agrees to them. +

+

+ SmolTerms exists because you deserve to know what you're agreeing to without spending hours reading legal jargon. One click, clear scores, and the key concerns — that's it. +

+
+
+ + +
+
+

How It Works

+
+
+
+ 1 +
+

Visit a Page

+

+ Navigate to any website's privacy policy or terms of service page. +

+
+
+
+ 2 +
+

Click the Extension

+

+ Click the SmolTerms icon in your browser toolbar. The extension extracts the page content and sends it for analysis. +

+
+
+
+ 3 +
+

Get Your Scores

+

+ Within seconds, see privacy scores across 5 dimensions, key concerns, and an overall risk level. +

+
+
+
+
+ + +
+
+

Get SmolTerms

+

Available for Firefox and Chrome. Free and open source.

+ +

Extension store links will be available once published.

+
+
+ + +
+
+

Privacy Scoring System

+

+ Every policy is scored across 5 dimensions on a scale of 1 to 10, where higher is better for your privacy. +

+ + +
+
+

Data Collection

+

How much personal data is collected, what types, and whether collection is proportionate to the service.

+
+
+

Data Sharing

+

Whether data is shared or sold to third parties and under what conditions.

+
+
+

User Rights

+

Your ability to access, delete, and export your data, plus opt-out mechanisms.

+
+
+

Retention

+

How long your data is kept and whether retention periods are clearly defined.

+
+
+

Security

+

Security measures mentioned including encryption, breach notification policies, and data protection practices.

+
+
+ + +

Risk Levels

+
+
+
+
+ Low Risk + 8.0 – 10.0 +
+
+
+
+
+ Moderate + 5.0 – 7.9 +
+
+
+
+
+ High Risk + 3.0 – 4.9 +
+
+
+
+
+ Critical + 1.0 – 2.9 +
+
+
+
+
+ + +
+
+

Analyze a Privacy Policy

+

+ Don't want to install the extension? Paste a privacy policy URL below and get the same analysis. +

+ + +
+ + +
+ + + + + + + + + + + + +
+
+ + +
+
+
+ SmolTerms + + GitHub + +
+

+ Open source under the MIT License. +

+
+
+ + + + diff --git a/website/script.js b/website/script.js new file mode 100644 index 0000000..a280d85 --- /dev/null +++ b/website/script.js @@ -0,0 +1,315 @@ +// Mobile menu toggle +const menuBtn = document.getElementById('mobile-menu-btn'); +const mobileMenu = document.getElementById('mobile-menu'); + +menuBtn.addEventListener('click', () => { + mobileMenu.classList.toggle('hidden'); +}); + +// Close mobile menu when a link is clicked +mobileMenu.querySelectorAll('a').forEach(link => { + link.addEventListener('click', () => { + mobileMenu.classList.add('hidden'); + }); +}); + +// Smooth scrolling for anchor links +document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', (e) => { + const targetId = anchor.getAttribute('href'); + if (targetId === '#') return; + + const target = document.querySelector(targetId); + if (target) { + e.preventDefault(); + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); +}); + +// --------------------------------------------------------------------------- +// URL Analysis Feature +// --------------------------------------------------------------------------- + +const API_BASE = 'http://localhost:8080'; + +// DOM references +const analyzeForm = document.getElementById('analyze-form'); +const urlInput = document.getElementById('url-input'); +const analyzeBtn = document.getElementById('analyze-btn'); +const validationMsg = document.getElementById('validation-msg'); +const loadingState = document.getElementById('loading-state'); +const errorState = document.getElementById('error-state'); +const errorMessage = document.getElementById('error-message'); +const errorSuggestion = document.getElementById('error-suggestion'); +const resultsState = document.getElementById('results-state'); +const overallScore = document.getElementById('overall-score'); +const riskBadge = document.getElementById('risk-badge'); +const cachedBadge = document.getElementById('cached-badge'); +const dimensionScores = document.getElementById('dimension-scores'); +const summaryText = document.getElementById('summary-text'); +const concernsList = document.getElementById('concerns-list'); +const analyzeAnotherBtn = document.getElementById('analyze-another-btn'); + +// Dimension key -> display label +const DIMENSION_LABELS = { + data_collection: 'Data Collection', + data_sharing: 'Data Sharing', + user_rights: 'User Rights', + retention: 'Retention', + security: 'Security', +}; + +// Risk level -> Tailwind classes for the badge +const RISK_STYLES = { + low: { bg: 'bg-risk-low', text: 'text-green-900', label: 'Low Risk' }, + moderate: { bg: 'bg-risk-moderate', text: 'text-yellow-900', label: 'Moderate Risk' }, + high: { bg: 'bg-risk-high', text: 'text-orange-900', label: 'High Risk' }, + critical: { bg: 'bg-risk-critical', text: 'text-red-900', label: 'Critical Risk' }, +}; + +// Score -> Tailwind text color for dimension score numbers +function getScoreColor(score) { + if (score >= 8) return 'text-green-600'; + if (score >= 5) return 'text-yellow-600'; + if (score >= 3) return 'text-orange-500'; + return 'text-red-500'; +} + +// --------------------------------------------------------------------------- +// State management +// --------------------------------------------------------------------------- + +function resetState() { + validationMsg.classList.add('hidden'); + validationMsg.textContent = ''; + loadingState.classList.add('hidden'); + errorState.classList.add('hidden'); + resultsState.classList.add('hidden'); + errorSuggestion.classList.add('hidden'); +} + +function showLoading() { + resetState(); + loadingState.classList.remove('hidden'); + analyzeBtn.disabled = true; + analyzeBtn.textContent = 'Analyzing...'; +} + +function hideLoading() { + loadingState.classList.add('hidden'); + analyzeBtn.disabled = false; + analyzeBtn.textContent = 'Analyze'; +} + +function showValidation(message) { + resetState(); + validationMsg.textContent = message; + validationMsg.classList.remove('hidden'); +} + +function showError(message, suggestion) { + hideLoading(); + errorState.classList.remove('hidden'); + errorMessage.textContent = message; + if (suggestion) { + errorSuggestion.textContent = suggestion; + errorSuggestion.classList.remove('hidden'); + } + scrollToElement(errorState); +} + +function scrollToElement(el) { + // Small delay to ensure DOM is painted before scrolling + setTimeout(() => { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, 100); +} + +// --------------------------------------------------------------------------- +// URL validation +// --------------------------------------------------------------------------- + +function isValidURL(str) { + try { + const url = new URL(str); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } +} + +// --------------------------------------------------------------------------- +// API call +// --------------------------------------------------------------------------- + +async function analyzeURL(url) { + const response = await fetch(`${API_BASE}/api/v1/analyze-url`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }), + }); + + const data = await response.json(); + + if (!response.ok) { + const error = new Error(data.error || 'An unexpected error occurred'); + error.status = response.status; + throw error; + } + + return data; +} + +// --------------------------------------------------------------------------- +// Results rendering +// --------------------------------------------------------------------------- + +function renderResults(data) { + hideLoading(); + + // Handle not_policy as a special case + if (data.risk_level === 'not_policy') { + showError( + "This page doesn't appear to be a privacy policy.", + 'Try submitting a direct link to a privacy policy or terms of service page.' + ); + return; + } + + // Overall score + overallScore.textContent = data.overall_score.toFixed(1); + overallScore.className = `text-4xl font-bold ${getScoreColor(data.overall_score)}`; + + // Risk badge + const risk = RISK_STYLES[data.risk_level] || RISK_STYLES.moderate; + riskBadge.textContent = risk.label; + riskBadge.className = `px-4 py-1.5 rounded-full text-sm font-semibold ${risk.bg} ${risk.text}`; + + // Cached badge + if (data.cached) { + cachedBadge.classList.remove('hidden'); + } else { + cachedBadge.classList.add('hidden'); + } + + // Dimension scores + dimensionScores.innerHTML = ''; + const dimensionOrder = ['data_collection', 'data_sharing', 'user_rights', 'retention', 'security']; + for (const key of dimensionOrder) { + const dim = data.dimensions[key]; + if (!dim) continue; + + const card = document.createElement('div'); + card.className = 'bg-cream-100 rounded-xl p-5 border border-cream-300 text-left'; + card.innerHTML = ` +
+

${DIMENSION_LABELS[key] || key}

+ ${dim.score.toFixed(1)} +
+

${escapeHTML(dim.summary)}

+ `; + dimensionScores.appendChild(card); + } + + // Summary + summaryText.textContent = data.summary || ''; + + // Key concerns + concernsList.innerHTML = ''; + if (data.key_concerns && data.key_concerns.length > 0) { + for (const concern of data.key_concerns) { + const li = document.createElement('li'); + li.className = 'flex items-start gap-2 text-sm text-cream-700'; + li.innerHTML = ` + + + + ${escapeHTML(concern)} + `; + concernsList.appendChild(li); + } + } else { + concernsList.innerHTML = '
  • No major concerns identified.
  • '; + } + + resultsState.classList.remove('hidden'); + scrollToElement(document.getElementById('results-header')); +} + +// Escape HTML to prevent XSS from API response content +function escapeHTML(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +// --------------------------------------------------------------------------- +// Error handling +// --------------------------------------------------------------------------- + +function handleError(err) { + if (err.status === 429) { + showError('Too many requests. Please wait a moment and try again.'); + return; + } + + if (err.status === 502) { + showError( + err.message || 'Failed to fetch the page for analysis.', + 'Some websites block automated access. Try using the SmolTerms browser extension instead for full compatibility.' + ); + return; + } + + if (err.status === 504) { + showError('Analysis timed out. The page may be too large. Please try again.'); + return; + } + + // Network error (no status) or other unexpected errors + if (!err.status) { + showError('Could not reach the analysis server. Please check that the backend is running and try again.'); + return; + } + + // Generic error with backend message + showError(err.message || 'An unexpected error occurred. Please try again.'); +} + +// --------------------------------------------------------------------------- +// Form submission +// --------------------------------------------------------------------------- + +analyzeForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const url = urlInput.value.trim(); + + if (!url) { + showValidation('Please enter a URL.'); + return; + } + + if (!isValidURL(url)) { + showValidation('Please enter a valid URL (e.g., https://example.com/privacy).'); + return; + } + + showLoading(); + + try { + const result = await analyzeURL(url); + renderResults(result); + } catch (err) { + handleError(err); + } +}); + +// "Analyze another" resets the form to initial state +analyzeAnotherBtn.addEventListener('click', () => { + resetState(); + urlInput.value = ''; + urlInput.focus(); + document.getElementById('try-it').scrollIntoView({ behavior: 'smooth', block: 'start' }); +});