Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Citation Formatter</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" /> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #ffffff; | |
| --surface: #f8f9fb; | |
| --border: #e2e6ed; | |
| --border-strong: #c8cdd6; | |
| --text: #0d1117; | |
| --text-muted: #5a6270; | |
| --text-faint: #9ba3af; | |
| --accent: #2563eb; | |
| --accent-light: #eff4ff; | |
| --accent-dark: #1d4ed8; | |
| --success: #059669; | |
| --danger: #dc2626; | |
| --sans: 'Space Grotesk', system-ui, sans-serif; | |
| --mono: 'JetBrains Mono', monospace; | |
| } | |
| body { | |
| font-family: var(--sans); | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 0 1.5rem 4rem; | |
| } | |
| .top-bar { | |
| width: 100%; | |
| max-width: 700px; | |
| border-bottom: 1px solid var(--border); | |
| padding: 1.25rem 0; | |
| margin-bottom: 3rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .logo-mark { | |
| width: 28px; | |
| height: 28px; | |
| background: var(--accent); | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .logo-mark svg { width: 14px; height: 14px; fill: #fff; } | |
| .logo-text { | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| color: var(--text); | |
| } | |
| .logo-text span { color: var(--accent); } | |
| header { | |
| width: 100%; | |
| max-width: 700px; | |
| margin-bottom: 2.5rem; | |
| } | |
| h1 { | |
| font-size: clamp(2rem, 5vw, 3rem); | |
| font-weight: 700; | |
| letter-spacing: -0.04em; | |
| line-height: 1.05; | |
| color: var(--text); | |
| margin-bottom: 0.75rem; | |
| } | |
| h1 .accent { color: var(--accent); } | |
| .subtitle { | |
| font-size: 1rem; | |
| color: var(--text-muted); | |
| font-weight: 400; | |
| line-height: 1.5; | |
| } | |
| .main { | |
| width: 100%; | |
| max-width: 700px; | |
| } | |
| .key-section { | |
| margin-bottom: 1.75rem; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| .key-toggle { | |
| width: 100%; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0.7rem 1rem; | |
| background: var(--surface); | |
| border: none; | |
| cursor: pointer; | |
| font-family: var(--mono); | |
| font-size: 0.72rem; | |
| color: var(--text-muted); | |
| text-align: left; | |
| letter-spacing: 0.02em; | |
| } | |
| .key-toggle:hover { background: #f0f2f5; } | |
| .key-toggle .arrow { font-size: 0.6rem; transition: transform 0.2s; color: var(--text-faint); } | |
| .key-toggle.open .arrow { transform: rotate(180deg); } | |
| .key-body { | |
| display: none; | |
| padding: 1rem; | |
| background: #fff; | |
| border-top: 1px solid var(--border); | |
| } | |
| .key-body.open { display: block; } | |
| .key-hint { | |
| font-size: 0.8rem; | |
| color: var(--text-faint); | |
| line-height: 1.6; | |
| margin-bottom: 0.75rem; | |
| } | |
| .key-hint a { color: var(--accent); text-decoration: none; } | |
| .key-hint a:hover { text-decoration: underline; } | |
| .key-row { display: flex; gap: 0.5rem; } | |
| .key-input { | |
| flex: 1; | |
| height: 38px; | |
| font-family: var(--mono); | |
| font-size: 0.8rem; | |
| padding: 0 0.75rem; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| color: var(--text); | |
| outline: none; | |
| } | |
| .key-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); } | |
| .key-save-btn { | |
| font-family: var(--sans); | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| height: 38px; | |
| padding: 0 1.1rem; | |
| background: var(--accent); | |
| color: #fff; | |
| border: none; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: background 0.15s; | |
| } | |
| .key-save-btn:hover { background: var(--accent-dark); } | |
| .key-status { font-size: 0.75rem; margin-top: 0.5rem; min-height: 1rem; font-family: var(--mono); } | |
| .key-status.ok { color: var(--success); } | |
| .key-status.err { color: var(--danger); } | |
| .field-label { | |
| font-size: 0.72rem; | |
| font-weight: 600; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--text-faint); | |
| margin-bottom: 0.5rem; | |
| display: block; | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 120px; | |
| resize: vertical; | |
| font-family: var(--mono); | |
| font-size: 0.85rem; | |
| line-height: 1.7; | |
| padding: 0.85rem 1rem; | |
| background: var(--bg); | |
| border: 1.5px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text); | |
| outline: none; | |
| transition: border-color 0.15s, box-shadow 0.15s; | |
| } | |
| textarea:focus { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px rgba(37,99,235,0.1); | |
| } | |
| textarea::placeholder { color: var(--text-faint); } | |
| .controls { | |
| display: flex; | |
| gap: 0.75rem; | |
| margin-top: 1rem; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .style-pills { | |
| display: flex; | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| flex: 1; | |
| } | |
| .pill { | |
| font-family: var(--sans); | |
| font-size: 0.78rem; | |
| font-weight: 500; | |
| padding: 6px 14px; | |
| border-radius: 100px; | |
| border: 1.5px solid var(--border); | |
| background: transparent; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| } | |
| .pill:hover { border-color: var(--accent); color: var(--accent); } | |
| .pill.active { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #fff; | |
| } | |
| button#format-btn { | |
| font-family: var(--sans); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| padding: 0 1.5rem; | |
| height: 40px; | |
| background: var(--text); | |
| color: #fff; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: background 0.15s, opacity 0.15s; | |
| white-space: nowrap; | |
| flex-shrink: 0; | |
| } | |
| button#format-btn:hover { background: #1a2333; } | |
| button#format-btn:disabled { opacity: 0.4; cursor: default; } | |
| .error-msg { | |
| display: none; | |
| font-size: 0.8rem; | |
| color: var(--danger); | |
| margin-top: 0.75rem; | |
| padding: 0.6rem 0.9rem; | |
| background: #fef2f2; | |
| border: 1px solid #fecaca; | |
| border-radius: 6px; | |
| font-family: var(--mono); | |
| } | |
| .result-area { display: none; margin-top: 2rem; } | |
| .result-area.visible { display: block; } | |
| .result-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 0.75rem; | |
| } | |
| .style-badge { | |
| font-size: 0.7rem; | |
| font-weight: 700; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| background: var(--accent-light); | |
| padding: 4px 10px; | |
| border-radius: 100px; | |
| } | |
| button#copy-btn { | |
| font-family: var(--sans); | |
| font-size: 0.78rem; | |
| font-weight: 500; | |
| padding: 5px 12px; | |
| background: transparent; | |
| border: 1.5px solid var(--border); | |
| border-radius: 6px; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| } | |
| button#copy-btn:hover { border-color: var(--accent); color: var(--accent); } | |
| .result-citation { | |
| font-family: var(--mono); | |
| font-size: 0.875rem; | |
| line-height: 1.8; | |
| color: var(--text); | |
| background: var(--surface); | |
| border: 1.5px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1.1rem 1.25rem 1.1rem 1.5rem; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| position: relative; | |
| } | |
| .result-citation::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 4px; height: 100%; | |
| background: var(--accent); | |
| border-radius: 8px 0 0 8px; | |
| } | |
| .notes { | |
| margin-top: 0.75rem; | |
| font-size: 0.82rem; | |
| color: var(--text-muted); | |
| line-height: 1.6; | |
| display: flex; | |
| gap: 8px; | |
| align-items: flex-start; | |
| } | |
| .notes-icon { | |
| font-size: 0.7rem; | |
| background: #fef9c3; | |
| color: #854d0e; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-weight: 700; | |
| font-family: var(--mono); | |
| flex-shrink: 0; | |
| margin-top: 2px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 13px; | |
| height: 13px; | |
| border: 2px solid rgba(255,255,255,0.35); | |
| border-top-color: #fff; | |
| border-radius: 50%; | |
| animation: spin 0.6s linear infinite; | |
| vertical-align: middle; | |
| margin-right: 6px; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| footer { | |
| margin-top: 3rem; | |
| width: 100%; | |
| max-width: 700px; | |
| padding-top: 1.5rem; | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.75rem; | |
| color: var(--text-faint); | |
| font-family: var(--mono); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="top-bar"> | |
| <div class="logo-mark"> | |
| <svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M2 2h4v10H2zm6 0h4v4h-4zm0 6h4v4h-4z"/></svg> | |
| </div> | |
| <span class="logo-text">Citation <span>Formatter</span></span> | |
| </div> | |
| <header> | |
| <h1>Clean up any<br><span class="accent">citation,</span> instantly.</h1> | |
| <p class="subtitle">Paste a messy reference — get it back formatted and ready to use.</p> | |
| </header> | |
| <div class="main"> | |
| <div class="key-section"> | |
| <button class="key-toggle" id="key-toggle" onclick="toggleKey()"> | |
| <span id="key-toggle-label">🔑 API KEY — CLICK TO SET</span> | |
| <span class="arrow">▼</span> | |
| </button> | |
| <div class="key-body" id="key-body"> | |
| <p class="key-hint">Stored only in your browser's localStorage — never in the code. Get a key at <a href="https://console.anthropic.com" target="_blank">console.anthropic.com</a>.</p> | |
| <div class="key-row"> | |
| <input class="key-input" type="password" id="key-input" placeholder="sk-ant-..." autocomplete="off" /> | |
| <button class="key-save-btn" onclick="saveKey()">Save</button> | |
| </div> | |
| <p class="key-status" id="key-status"></p> | |
| </div> | |
| </div> | |
| <label class="field-label" for="citation-input">Paste your citation</label> | |
| <textarea | |
| id="citation-input" | |
| placeholder="e.g. smith j 2019 attachment theory new york basic books or a DOI, a half-remembered reference, a messy export..." | |
| ></textarea> | |
| <div class="controls"> | |
| <div class="style-pills" id="style-pills"> | |
| <button class="pill active" onclick="selectPill(this, 'APA 7th edition')">APA 7th</button> | |
| <button class="pill" onclick="selectPill(this, 'MLA 9th edition')">MLA 9th</button> | |
| <button class="pill" onclick="selectPill(this, 'Chicago 17th edition')">Chicago 17th</button> | |
| <button class="pill" onclick="selectPill(this, 'Harvard')">Harvard</button> | |
| <button class="pill" onclick="selectPill(this, 'Vancouver')">Vancouver</button> | |
| </div> | |
| <button id="format-btn" onclick="formatCitation()">Format →</button> | |
| </div> | |
| <div class="error-msg" id="error-msg"></div> | |
| <div class="result-area" id="result-area"> | |
| <div class="result-header"> | |
| <span class="style-badge" id="style-badge">APA 7th</span> | |
| <button id="copy-btn" onclick="copyCitation()">Copy</button> | |
| </div> | |
| <div class="result-citation" id="result-citation"></div> | |
| <div class="notes" id="result-notes" style="display:none;"> | |
| <span class="notes-icon">note</span> | |
| <span id="notes-text"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| <span>powered by Claude · Anthropic</span> | |
| <span>citation-formatter</span> | |
| </footer> | |
| <script> | |
| let selectedStyle = 'APA 7th edition'; | |
| function selectPill(el, style) { | |
| document.querySelectorAll('.pill').forEach(p => p.classList.remove('active')); | |
| el.classList.add('active'); | |
| selectedStyle = style; | |
| } | |
| function getKey() { return localStorage.getItem('cf_api_key') || ''; } | |
| function toggleKey() { | |
| const body = document.getElementById('key-body'); | |
| const toggle = document.getElementById('key-toggle'); | |
| const isOpen = body.classList.toggle('open'); | |
| toggle.classList.toggle('open', isOpen); | |
| if (isOpen) { | |
| const saved = getKey(); | |
| if (saved) document.getElementById('key-input').value = saved; | |
| } | |
| } | |
| function saveKey() { | |
| const val = document.getElementById('key-input').value.trim(); | |
| const status = document.getElementById('key-status'); | |
| const label = document.getElementById('key-toggle-label'); | |
| if (!val.startsWith('sk-')) { | |
| status.textContent = "That doesn't look like a valid key (should start with sk-)."; | |
| status.className = 'key-status err'; | |
| return; | |
| } | |
| localStorage.setItem('cf_api_key', val); | |
| status.textContent = 'Key saved.'; | |
| status.className = 'key-status ok'; | |
| label.textContent = '\uD83D\uDD13 API KEY \u2014 SET \u2713'; | |
| setTimeout(() => { | |
| document.getElementById('key-body').classList.remove('open'); | |
| document.getElementById('key-toggle').classList.remove('open'); | |
| }, 800); | |
| } | |
| window.addEventListener('DOMContentLoaded', () => { | |
| if (getKey()) { | |
| document.getElementById('key-toggle-label').textContent = '\uD83D\uDD13 API KEY \u2014 SET \u2713'; | |
| } | |
| }); | |
| async function formatCitation() { | |
| const apiKey = getKey(); | |
| const input = document.getElementById('citation-input').value.trim(); | |
| const btn = document.getElementById('format-btn'); | |
| const errorEl = document.getElementById('error-msg'); | |
| const resultArea = document.getElementById('result-area'); | |
| errorEl.style.display = 'none'; | |
| resultArea.classList.remove('visible'); | |
| if (!apiKey) { | |
| errorEl.textContent = 'Please set your Anthropic API key first (click the key section above).'; | |
| errorEl.style.display = 'block'; | |
| return; | |
| } | |
| if (!input) { | |
| errorEl.textContent = 'Please paste a citation to format.'; | |
| errorEl.style.display = 'block'; | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="spinner"></span>Formatting\u2026'; | |
| try { | |
| const response = await fetch('https://api.anthropic.com/v1/messages', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': apiKey, | |
| 'anthropic-version': '2023-06-01', | |
| 'anthropic-dangerous-direct-browser-access': 'true' | |
| }, | |
| body: JSON.stringify({ | |
| model: 'claude-haiku-4-5-20251001', | |
| max_tokens: 1000, | |
| system: `You are a citation formatting expert. Given a messy, incomplete, or incorrectly formatted citation, return a corrected version in the requested style. Respond ONLY with a JSON object — no markdown, no backticks, no preamble. Format: {"formatted": "the clean citation here", "notes": "brief notes about assumptions made or missing info, or empty string if none"}`, | |
| messages: [ | |
| { role: 'user', content: `Format this citation in ${selectedStyle} style:\n\n${input}` } | |
| ] | |
| }) | |
| }); | |
| if (!response.ok) throw new Error(`API error: ${response.status}`); | |
| const data = await response.json(); | |
| const text = data.content.map(i => i.text || '').join(''); | |
| const parsed = JSON.parse(text.replace(/```json|```/g, '').trim()); | |
| document.getElementById('style-badge').textContent = selectedStyle; | |
| document.getElementById('result-citation').textContent = parsed.formatted; | |
| const notesEl = document.getElementById('result-notes'); | |
| const notesText = document.getElementById('notes-text'); | |
| if (parsed.notes) { | |
| notesText.textContent = parsed.notes; | |
| notesEl.style.display = 'flex'; | |
| } else { | |
| notesEl.style.display = 'none'; | |
| } | |
| resultArea.classList.add('visible'); | |
| } catch (err) { | |
| console.error(err); | |
| errorEl.textContent = 'Something went wrong. Check your API key and try again.'; | |
| errorEl.style.display = 'block'; | |
| } | |
| btn.disabled = false; | |
| btn.innerHTML = 'Format →'; | |
| } | |
| function copyCitation() { | |
| const text = document.getElementById('result-citation').textContent; | |
| navigator.clipboard.writeText(text).then(() => { | |
| const btn = document.getElementById('copy-btn'); | |
| btn.textContent = 'Copied!'; | |
| setTimeout(() => btn.textContent = 'Copy', 1500); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |