186 lines
6.4 KiB
JavaScript
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
|
||
|
|
};
|
||
|
|
|
||
|
|
|