Files
LocalAI/addons/web/ui/src/lib/model.js
T
Karl-Gustav Kallasmaa c7fb75bfcd feat(web): add Keenable as a search provider (works without a key)
Signed-off-by: Karl-Gustav Kallasmaa <29797627+KGKallasmaa@users.noreply.github.com>
(cherry picked from commit a93be7cc10)
(cherry picked from commit 4c46293b151e03779e8ad580de9303da91afdc27)
2026-11-03 19:29:02 +00:00

121 lines
4.7 KiB
JavaScript

// What the settings section of the Web add-on draws, and what it sends. The
// status tool answers the settings only to an administrator, and never the
// keys: a provider says whether its key is set.
export const PROVIDERS = ['brave', 'tavily', 'google', 'keenable', 'searxng', 'duckduckgo']
export const KEYED = ['brave', 'tavily', 'google', 'keenable']
// The keyless entry of the order ('duckduckgo', its saved name) is the chain
// of the built-in search: these result pages, tried in turn.
export const KEYLESS_ENTRY = 'duckduckgo'
export const KEYLESS = ['duckduckgo', 'brave_html', 'bing_html']
// servingEntries turns the providers that serve, as the add-on names them,
// into the entries of the order: the pages of the keyless chain are one entry.
function servingEntries(list) {
const out = []
for (const n of Array.isArray(list) ? list : []) {
const entry = KEYLESS.includes(n) ? KEYLESS_ENTRY : n
if (PROVIDERS.includes(entry) && !out.includes(entry)) out.push(entry)
}
return out
}
// coolingRows lists the providers the add-on skips for a few minutes, with
// why: a bot check or a refusal of this server.
function coolingRows(cooling) {
if (!cooling || typeof cooling !== 'object' || Array.isArray(cooling)) return []
return Object.keys(cooling)
.filter(n => KEYLESS.includes(n) || PROVIDERS.includes(n))
.sort((a, b) => KEYLESS.indexOf(a) - KEYLESS.indexOf(b))
.map(n => ({ name: n, reason: text(cooling[n]) }))
}
const text = (v) => (typeof v === 'string' ? v : '')
export function settingsModel(status) {
if (!status || typeof status !== 'object' || status.ok !== true || status.admin !== true) return { hidden: true }
const s = status.settings && typeof status.settings === 'object' ? status.settings : {}
const byName = {}
for (const p of Array.isArray(s.providers) ? s.providers : []) {
if (p && PROVIDERS.includes(p.name)) byName[p.name] = p
}
const order = (Array.isArray(s.order) ? s.order : []).filter(n => PROVIDERS.includes(n))
const providers = PROVIDERS.map(name => {
const p = byName[name] || {}
return { name, keyed: KEYED.includes(name), keySet: p.keySet === true, ready: p.ready === true, missing: text(p.missing) }
})
return {
hidden: false,
order,
providers,
googleEngine: text(s.googleEngine),
searxngUrl: text(s.searxngUrl),
serving: servingEntries(status.serving),
cooling: coolingRows(status.cooling),
note: text(s.note),
}
}
// The draft starts from the saved settings. Key fields start empty: an empty
// field keeps the saved key.
export function draftOf(model) {
return {
order: [...model.order],
keys: Object.fromEntries(KEYED.map(n => [n, ''])),
clear: [],
googleEngine: model.googleEngine,
searxngUrl: model.searxngUrl,
}
}
// The listed providers: those of the order first, in order, then the others.
export function rows(model, draft) {
const on = draft.order
const rest = PROVIDERS.filter(n => !on.includes(n))
return [...on, ...rest].map(name => ({ ...model.providers.find(p => p.name === name), on: on.includes(name), position: on.indexOf(name) }))
}
export function toggle(draft, name) {
const order = draft.order.includes(name) ? draft.order.filter(n => n !== name) : [...draft.order, name]
return { ...draft, order }
}
export function move(draft, name, by) {
const i = draft.order.indexOf(name)
const j = i + by
if (i < 0 || j < 0 || j >= draft.order.length) return draft
const order = [...draft.order]
;[order[i], order[j]] = [order[j], order[i]]
return { ...draft, order }
}
export function setKey(draft, name, value) {
return { ...draft, keys: { ...draft.keys, [name]: value }, clear: draft.clear.filter(n => n !== name) }
}
export function clearKey(draft, name) {
return { ...draft, keys: { ...draft.keys, [name]: '' }, clear: draft.clear.includes(name) ? draft.clear : [...draft.clear, name] }
}
// The arguments of set-settings: only the keys that were typed, so a saved key
// is never sent back.
export function change(draft) {
const keys = {}
for (const n of KEYED) {
const v = text(draft.keys[n]).trim()
if (v) keys[n] = v
}
const out = { order: [...draft.order], googleEngine: draft.googleEngine.trim(), searxngUrl: draft.searxngUrl.trim() }
if (Object.keys(keys).length > 0) out.keys = keys
if (draft.clear.length > 0) out.clear = [...draft.clear]
return out
}
// The rows of a test-search answer.
export function testRows(answer) {
if (!answer || answer.ok !== true || !Array.isArray(answer.providers)) return []
return answer.providers
.filter(p => p && (PROVIDERS.includes(p.name) || KEYLESS.includes(p.name)))
.map(p => ({ name: p.name, ok: p.ok === true, ready: p.ready === true, results: Number.isInteger(p.results) ? p.results : 0, host: text(p.host), error: text(p.error) }))
}