citation-formatter / index.html
AngelaColmen's picture
Update index.html
495e82b verified
<!DOCTYPE html>
<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">&#128273; API KEY — CLICK TO SET</span>
<span class="arrow">&#9660;</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&#10;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 &rarr;</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 &rarr;';
}
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>