/* ────────────────────────────────────────────────────────────────────────────
* SOY AI Labs — 표준 AI 모델 셀렉터 로더
*
* 관리자 페이지(`관리 > AI 설정 > 사용 가능한 AI 목록`)에서 활성화한 모델만
* 화면 드롭다운에 노출합니다. 모든 사용자/창작 화면은 이 모듈만 사용해야 합니다.
*
* 표준 API: GET /api/enabled-models
* 응답: {
* success: true,
* models: [{ name: "gemini:xxx" | "ollama-xxx", type: "gemini" | "ollama" }],
* default_analysis_model: "...",
* default_answer_model: "..."
* }
*
* 사용 예:
* SoyEnabledModels.populate(document.getElementById('modelSelect'), {
* kind: 'analysis', // 'analysis' | 'answer' (기본값 자동선택용)
* preset: 'gemini:xxx', // 우선 선택할 모델 (선택)
* placeholder: '모델 선택', // 첫 옵션의 라벨 (선택, 없으면 옵션 미추가)
* includeAll: false, // '전체 모델' 옵션 추가 여부 (기본 false)
* strip: true, // gemini: 접두어 제거하여 표시 (기본 true)
* typeFilter: ['gemini'], // 특정 타입만 표시 (선택, 예: ['gemini'])
* onChange: function (val) { ... } // 선택 변경 콜백 (선택)
* });
* ──────────────────────────────────────────────────────────────────────────── */
(function (global) {
'use strict';
var STATE = { cache: null, inflight: null };
function fetchEnabledModels(force) {
if (!force && STATE.cache) return Promise.resolve(STATE.cache);
if (STATE.inflight) return STATE.inflight;
STATE.inflight = fetch('/api/enabled-models', { credentials: 'include' })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
STATE.cache = {
models: Array.isArray(data.models) ? data.models : [],
default_analysis_model: data.default_analysis_model || '',
default_answer_model: data.default_answer_model || '',
};
return STATE.cache;
})
.finally(function () { STATE.inflight = null; });
return STATE.inflight;
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (m) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
});
}
function displayName(name, strip) {
if (!name) return '';
return (strip !== false && name.indexOf('gemini:') === 0) ? name.substring(7) : name;
}
function buildHtml(models, opts) {
var parts = [];
if (opts.placeholder) {
parts.push('');
}
if (opts.includeAll) {
parts.push('');
}
var gemini = models.filter(function (m) { return m.type === 'gemini'; });
var ollama = models.filter(function (m) { return m.type === 'ollama'; });
var etc = models.filter(function (m) { return m.type !== 'gemini' && m.type !== 'ollama'; });
function group(label, list) {
if (!list.length) return;
parts.push('');
}
group('✨ Gemini', gemini);
group('🤖 Ollama', ollama);
group('기타', etc);
return parts.join('');
}
/**
* 셀렉트 요소를 표준 모델 목록으로 채웁니다.
* 반환: Promise — 성공 시 선택된 모델명(string|null)을 resolve
*/
function populate(selectEl, opts) {
opts = opts || {};
if (!selectEl) return Promise.resolve(null);
var prevPlaceholder = selectEl.innerHTML;
selectEl.disabled = true;
if (!selectEl.dataset.soyOriginalPlaceholder) {
selectEl.innerHTML = '';
}
return fetchEnabledModels(opts.force).then(function (data) {
// 타입 필터 적용 (예: 콘티 어시스트는 ['gemini']만)
var models = data.models;
if (Array.isArray(opts.typeFilter) && opts.typeFilter.length) {
models = models.filter(function (m) { return opts.typeFilter.indexOf(m.type) !== -1; });
}
var html = '';
if (!models.length) {
html = '';
selectEl.innerHTML = html;
selectEl.disabled = false;
return null;
}
html = buildHtml(models, opts);
selectEl.innerHTML = html;
// 기본 선택값 결정: preset → kind 기반 default → 첫 옵션
var fallbackKey = (opts.kind === 'analysis') ? 'default_analysis_model' : 'default_answer_model';
var candidates = [opts.preset, data[fallbackKey]].filter(Boolean);
var chosen = null;
for (var i = 0; i < candidates.length; i++) {
if (models.some(function (m) { return m.name === candidates[i]; })) {
chosen = candidates[i];
break;
}
}
if (!chosen && opts.includeAll) chosen = 'all';
if (chosen) selectEl.value = chosen;
else if (!opts.placeholder && selectEl.options.length > 0) {
// 플레이스홀더가 없으면 첫 실제 옵션 선택
for (var j = 0; j < selectEl.options.length; j++) {
if (selectEl.options[j].value) { selectEl.value = selectEl.options[j].value; break; }
}
chosen = selectEl.value || null;
}
selectEl.disabled = false;
if (typeof opts.onChange === 'function' && chosen) {
try { opts.onChange(chosen); } catch (e) { /* ignore */ }
}
return chosen;
}).catch(function (err) {
console.warn('[SoyEnabledModels] 로드 실패:', err);
selectEl.innerHTML = prevPlaceholder.indexOf('option') === -1
? ''
: prevPlaceholder;
selectEl.disabled = false;
return null;
});
}
/** 여러 셀렉트를 한 번의 API 호출로 동시에 채웁니다. */
function populateAll(targets) {
return fetchEnabledModels().then(function () {
return Promise.all((targets || []).map(function (t) {
return populate(t.element, t.options || {});
}));
});
}
function refresh() {
STATE.cache = null;
return fetchEnabledModels(true);
}
global.SoyEnabledModels = {
populate: populate,
populateAll: populateAll,
fetch: fetchEnabledModels,
refresh: refresh,
};
})(window);