AngelaColmen's picture
Update index.html
39d6e45 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Subject Heading Generator</title>
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f2eb;
--surface: #faf8f4;
--border: #d6d0c4;
--border-light: #e8e3d9;
--text: #1a1714;
--text-muted: #6b6560;
--text-faint: #9e9890;
--accent: #2c5f4a;
--accent-light: #e8f0ec;
--accent-mid: #3d7a61;
--danger: #8b2020;
--mono: 'DM Mono', monospace;
--serif: 'Lora', Georgia, serif;
}
body {
font-family: var(--serif);
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem 1.5rem 4rem;
}
header {
text-align: center;
margin-bottom: 3rem;
}
header::before {
content: '';
display: block;
width: 40px;
height: 2px;
background: var(--accent);
margin: 0 auto 1.5rem;
}
h1 {
font-size: clamp(1.6rem, 4vw, 2.2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.2;
margin-bottom: 0.5rem;
}
h1 em { font-style: italic; color: var(--accent); }
.subtitle {
font-size: 0.95rem;
color: var(--text-muted);
font-style: italic;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 2rem;
width: 100%;
max-width: 680px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.key-section {
margin-bottom: 1.5rem;
border: 1px dashed var(--border);
border-radius: 3px;
overflow: hidden;
}
.key-toggle {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6rem 0.9rem;
background: #f0ede6;
border: none;
cursor: pointer;
font-family: var(--mono);
font-size: 0.75rem;
color: var(--text-muted);
text-align: left;
}
.key-toggle:hover { background: #ece9e1; }
.key-toggle .arrow { font-size: 0.65rem; transition: transform 0.2s; color: var(--text-faint); }
.key-toggle.open .arrow { transform: rotate(180deg); }
.key-body {
display: none;
padding: 0.9rem;
background: #f7f5f0;
border-top: 1px dashed var(--border);
}
.key-body.open { display: block; }
.key-hint {
font-family: var(--mono);
font-size: 0.72rem;
color: var(--text-faint);
line-height: 1.6;
}
.key-hint a { color: var(--accent); }
.key-row {
display: flex;
gap: 0.5rem;
align-items: center;
margin-top: 0.5rem;
}
.key-input {
flex: 1;
height: 34px;
font-family: var(--mono);
font-size: 0.8rem;
padding: 0 0.75rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
outline: none;
letter-spacing: 0.05em;
}
.key-input:focus { border-color: var(--accent); }
.key-save-btn {
font-family: var(--mono);
font-size: 0.75rem;
height: 34px;
padding: 0 0.9rem;
background: var(--accent);
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
white-space: nowrap;
}
.key-save-btn:hover { background: var(--accent-mid); }
.key-status {
font-family: var(--mono);
font-size: 0.72rem;
margin-top: 0.5rem;
min-height: 1rem;
}
.key-status.ok { color: var(--accent); }
.key-status.err { color: var(--danger); }
.field-label {
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.1em;
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.6;
padding: 0.75rem 1rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
outline: none;
transition: border-color 0.15s;
}
textarea:focus { border-color: var(--accent); }
textarea::placeholder { color: var(--text-faint); font-style: italic; }
.controls {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
align-items: center;
flex-wrap: wrap;
}
select {
font-family: var(--mono);
font-size: 0.8rem;
padding: 0 0.9rem;
height: 38px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
outline: none;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236b6560'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 28px;
}
select:focus { border-color: var(--accent); }
button#gen-btn {
font-family: var(--mono);
font-size: 0.8rem;
letter-spacing: 0.05em;
padding: 0 1.4rem;
height: 38px;
background: var(--accent);
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
transition: background 0.15s, opacity 0.15s;
flex-shrink: 0;
}
button#gen-btn:hover { background: var(--accent-mid); }
button#gen-btn:disabled { opacity: 0.55; cursor: default; }
.error-msg {
display: none;
font-family: var(--mono);
font-size: 0.8rem;
color: var(--danger);
margin-top: 0.75rem;
padding: 0.6rem 0.9rem;
background: #fcf0f0;
border: 1px solid #e8c0c0;
border-radius: 3px;
}
.divider {
border: none;
border-top: 1px solid var(--border-light);
margin: 1.75rem 0;
}
.result-area { display: none; }
.result-area.visible { display: block; }
.result-label {
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-faint);
margin-bottom: 0.75rem;
}
.headings-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.heading-item {
background: var(--bg);
border: 1px solid var(--border-light);
border-left: 3px solid var(--accent);
border-radius: 0 3px 3px 0;
padding: 0.7rem 1rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.heading-text {
font-family: var(--mono);
font-size: 0.85rem;
color: var(--text);
line-height: 1.5;
flex: 1;
}
.heading-type {
font-family: var(--mono);
font-size: 0.7rem;
color: var(--text-faint);
margin-top: 3px;
}
.copy-small {
font-family: var(--mono);
font-size: 0.7rem;
height: 24px;
padding: 0 8px;
background: transparent;
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text-muted);
cursor: pointer;
flex-shrink: 0;
transition: border-color 0.15s, color 0.15s;
}
.copy-small:hover { border-color: var(--accent); color: var(--accent); }
.notes {
margin-top: 1rem;
font-size: 0.85rem;
font-style: italic;
color: var(--text-muted);
line-height: 1.6;
padding-left: 0.75rem;
border-left: 1px solid var(--border);
}
.notes::before {
content: 'Note: ';
font-family: var(--mono);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.07em;
font-style: normal;
color: var(--text-faint);
}
.copy-all-btn {
font-family: var(--mono);
font-size: 0.72rem;
letter-spacing: 0.05em;
padding: 4px 10px;
background: transparent;
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text-muted);
cursor: pointer;
margin-top: 1rem;
transition: border-color 0.15s, color 0.15s;
}
.copy-all-btn:hover { border-color: var(--accent); color: var(--accent); }
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid rgba(255,255,255,0.4);
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: 2.5rem;
text-align: center;
font-size: 0.8rem;
font-family: var(--mono);
color: var(--text-faint);
}
</style>
</head>
<body>
<header>
<h1>Subject Heading <em>Generator</em></h1>
<p class="subtitle">Paste a book title or description — get catalog-ready subject headings.</p>
</header>
<div class="card">
<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">Your key is stored only in your browser's localStorage — never hardcoded or shared. Get one 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="desc-input">Book title / description</label>
<textarea
id="desc-input"
placeholder="e.g. A novel about a young woman navigating grief and identity after the death of her mother in rural Appalachia..."
></textarea>
<div class="controls">
<select id="scheme-select">
<option value="Library of Congress Subject Headings (LCSH)">LCSH</option>
<option value="Sears List of Subject Headings">Sears</option>
<option value="both LCSH and Sears">Both</option>
</select>
<button id="gen-btn" onclick="generate()">Generate headings</button>
</div>
<div class="error-msg" id="error-msg"></div>
<div class="result-area" id="result-area">
<hr class="divider" />
<p class="result-label" id="result-label">Suggested headings</p>
<div class="headings-list" id="headings-list"></div>
<p class="notes" id="result-notes" style="display:none;"></p>
<button class="copy-all-btn" onclick="copyAll()">Copy all headings</button>
</div>
</div>
<footer>powered by Claude · anthropic</footer>
<script>
function getKey() { return localStorage.getItem('shg_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('shg_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 generate() {
const apiKey = getKey();
const input = document.getElementById('desc-input').value.trim();
const scheme = document.getElementById('scheme-select').value;
const btn = document.getElementById('gen-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 enter a book title or description.';
errorEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>Generating\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 an expert library cataloger with deep knowledge of ${scheme}. Given a book title or description, suggest appropriate subject headings. Respond ONLY with a JSON object — no markdown, no backticks, no preamble. Format: {"headings": [{"heading": "the subject heading string", "type": "main heading / subdivision / genre form / etc"}], "notes": "any brief cataloging notes or empty string"}`,
messages: [
{ role: 'user', content: `Suggest ${scheme} subject headings for this book:\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());
const list = document.getElementById('headings-list');
list.innerHTML = '';
parsed.headings.forEach(h => {
const item = document.createElement('div');
item.className = 'heading-item';
const safeHeading = h.heading.replace(/'/g, "\\'");
item.innerHTML = `
<div style="flex:1;">
<div class="heading-text">${h.heading}</div>
<div class="heading-type">${h.type}</div>
</div>
<button class="copy-small" onclick="copyOne(this, '${safeHeading}')">Copy</button>
`;
list.appendChild(item);
});
const notesEl = document.getElementById('result-notes');
if (parsed.notes) { notesEl.textContent = parsed.notes; notesEl.style.display = 'block'; }
else { notesEl.style.display = 'none'; }
document.getElementById('result-label').textContent = `Suggested ${scheme} headings`;
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.textContent = 'Generate headings';
}
function copyOne(btn, text) {
navigator.clipboard.writeText(text).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 1500);
});
}
function copyAll() {
const items = document.querySelectorAll('.heading-text');
const text = Array.from(items).map(i => i.textContent).join('\n');
navigator.clipboard.writeText(text).then(() => {
const btn = document.querySelector('.copy-all-btn');
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy all headings', 1500);
});
}
</script>
</body>
</html>