Files
christianandClaude Opus 5 0db5e9b07d Bible App 1.4.34 — full source
First commit on the local Gitea server, made after moving the tree off the
Windows machine. Build outputs, node_modules and the AI models stay out
(see .gitignore); everything the installer build needs is tracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:05:20 -04:00

186 lines
6.4 KiB
JavaScript

// Minimal client for the Greek WordNet API
// Docs: https://greekwordnet.chs.harvard.edu/api
const PROXY_BASE = '/api/wordnet';
const REMOTE_BASE = 'https://greekwordnet.chs.harvard.edu';
const BASE = (typeof window !== 'undefined') ? PROXY_BASE : REMOTE_BASE;
const API_BASE = `${BASE}/api`;
async function requestJson(url, options = {}) {
// Add a short timeout to keep UI responsive
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs || 15000);
try {
const resp = await fetch(url, { ...options, signal: controller.signal });
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new Error(`HTTP ${resp.status} ${resp.statusText} for ${url} ${text ? '- ' + text.slice(0, 200) : ''}`);
}
// Most endpoints return JSON
const ct = (resp.headers.get('content-type') || '').toLowerCase();
if (ct.includes('application/json')) {
return await resp.json();
}
// Fallback: try text
return await resp.text();
} finally {
clearTimeout(timeoutId);
}
}
// ----- Lemmas -----
export async function searchLemmas(query) {
if (!query) return [];
const url = `${API_BASE}/lemmas?search=${encodeURIComponent(query)}`;
return await requestJson(url);
}
export async function getLemma(headword, pos, morpho) {
if (!headword) throw new Error('headword is required');
const parts = [encodeURIComponent(headword)];
if (pos) parts.push(encodeURIComponent(pos));
if (morpho) parts.push(encodeURIComponent(morpho));
const url = `${API_BASE}/lemmas/${parts.join('/')}/`;
return await requestJson(url);
}
export async function getLemmaSynsets(headword, pos, morpho) {
if (!headword) throw new Error('headword is required');
const parts = [encodeURIComponent(headword)];
if (pos) parts.push(encodeURIComponent(pos));
if (morpho) parts.push(encodeURIComponent(morpho));
const url = `${API_BASE}/lemmas/${parts.join('/')}/synsets`;
return await requestJson(url);
}
export async function getLemmaRelations(headword, pos, morpho) {
if (!headword) throw new Error('headword is required');
const parts = [encodeURIComponent(headword)];
if (pos) parts.push(encodeURIComponent(pos));
if (morpho) parts.push(encodeURIComponent(morpho));
const url = `${API_BASE}/lemmas/${parts.join('/')}/relations`;
return await requestJson(url);
}
// ----- Synsets -----
export async function getSynset(pos, offset) {
if (!pos || !offset) throw new Error('pos and offset are required');
const url = `${API_BASE}/synsets/${encodeURIComponent(pos)}/${encodeURIComponent(offset)}/`;
return await requestJson(url);
}
export async function getSynsetLemmas(pos, offset) {
if (!pos || !offset) throw new Error('pos and offset are required');
const url = `${API_BASE}/synsets/${encodeURIComponent(pos)}/${encodeURIComponent(offset)}/lemmas`;
return await requestJson(url);
}
export async function getSynsetRelations(pos, offset) {
if (!pos || !offset) throw new Error('pos and offset are required');
const url = `${API_BASE}/synsets/${encodeURIComponent(pos)}/${encodeURIComponent(offset)}/relations`;
return await requestJson(url);
}
// options is forwarded to requestJson, so callers can impose a shorter timeout than the 15s default.
// The word-hint pass uses 2s: it runs over the internet and must never hold up the interface.
export async function searchSynsets(glossQuery, options) {
if (!glossQuery) return [];
const url = `${API_BASE}/synsets?search=${encodeURIComponent(glossQuery)}`;
return await requestJson(url, options || {});
}
// ----- Semfields -----
export async function getSemfield(ddcsCode) {
if (!ddcsCode) throw new Error('ddcsCode is required');
const url = `${API_BASE}/semfields/${encodeURIComponent(ddcsCode)}`;
return await requestJson(url);
}
export async function searchSemfields(query) {
if (!query) return [];
const url = `${API_BASE}/semfields?search=${encodeURIComponent(query)}`;
return await requestJson(url);
}
// ----- Lemmatization / Translation / Sentiment -----
export async function lemmatize(form, pos) {
if (!form) return [];
const path1 = pos
? `/lemmatize/${encodeURIComponent(form)}/${encodeURIComponent(pos)}`
: `/lemmatize/${encodeURIComponent(form)}`;
try {
return await requestJson(`${BASE}${path1}`);
} catch (_) {
// Fallback strategies if upstream lemmatizer errors:
// 1) Try API search as a loose approximation
try {
const results = await requestJson(`${API_BASE}/lemmas?search=${encodeURIComponent(form)}`);
return Array.isArray(results) ? results : [];
} catch {
// 2) Give up with an empty list to keep UI stable
return [];
}
}
}
export async function translate(langIso639, word, pos) {
if (!langIso639 || !word) throw new Error('lang and word are required');
const path = pos
? `/translate/${encodeURIComponent(langIso639)}/${encodeURIComponent(word)}/${encodeURIComponent(pos)}`
: `/translate/${encodeURIComponent(langIso639)}/${encodeURIComponent(word)}`;
return await requestJson(`${BASE}${path}`);
}
export async function analyzeSentiment(text, options = {}) {
if (!text) return { polarity: 0, tokens: [] };
const body = {
text,
...(options.weighting ? { weighting: options.weighting } : {}),
...(Array.isArray(options.excluded) ? { excluded: options.excluded } : {})
};
return await requestJson(`${BASE}/sentiment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
// Convenience: small helper to get synsets for a lemma headword quickly
export async function lookupLemma(headword, pos, morpho) {
const [lemmaInfo, synsets] = await Promise.allSettled([
getLemma(headword, pos, morpho),
getLemmaSynsets(headword, pos, morpho)
]);
return {
lemma: lemmaInfo.status === 'fulfilled' ? lemmaInfo.value : null,
synsets: synsets.status === 'fulfilled' ? synsets.value : [],
error: [
lemmaInfo.status === 'rejected' ? String(lemmaInfo.reason?.message || lemmaInfo.reason) : '',
synsets.status === 'rejected' ? String(synsets.reason?.message || synsets.reason) : ''
].filter(Boolean).join('; ')
};
}
export default {
searchLemmas,
getLemma,
getLemmaSynsets,
getLemmaRelations,
getSynset,
getSynsetLemmas,
getSynsetRelations,
searchSynsets,
getSemfield,
searchSemfields,
lemmatize,
translate,
analyzeSentiment,
lookupLemma
};