Changeset 0.23.0 (#153)
This commit is contained in:
@@ -187,31 +187,44 @@ const ChannelModels = {
|
||||
* Shows a dropdown of channel models when user types @.
|
||||
*/
|
||||
onInput(inputEl) {
|
||||
if (this._roster.length < 2) return; // no autocomplete for single-model channels
|
||||
|
||||
const text = typeof inputEl.getValue === 'function' ? inputEl.getValue() : inputEl.value;
|
||||
const cursorPos = typeof inputEl.getCursorPos === 'function' ? inputEl.getCursorPos() : inputEl.selectionStart;
|
||||
|
||||
// Find the @token being typed (search backward from cursor)
|
||||
const before = text.slice(0, cursorPos);
|
||||
const atIdx = before.lastIndexOf('@');
|
||||
if (atIdx < 0) { this.hideAutocomplete(); return; }
|
||||
|
||||
// @ must be at start or preceded by whitespace
|
||||
if (atIdx > 0 && before[atIdx - 1] !== ' ' && before[atIdx - 1] !== '\n') {
|
||||
this.hideAutocomplete();
|
||||
return;
|
||||
}
|
||||
|
||||
const partial = before.slice(atIdx + 1).toLowerCase();
|
||||
const matches = this._roster.filter(m =>
|
||||
m.display_name.toLowerCase().startsWith(partial) ||
|
||||
m.display_name.toLowerCase().replace(/-/g, ' ').startsWith(partial.replace(/-/g, ' '))
|
||||
);
|
||||
const partial = before.slice(atIdx + 1).toLowerCase().replace(/-/g, ' ');
|
||||
if (partial.length === 0) {
|
||||
// Just typed @ — show everything
|
||||
}
|
||||
|
||||
// Build match list from ALL enabled models + personas (not just roster)
|
||||
const allModels = (App.models || []).filter(m => !m.hidden);
|
||||
const matches = allModels.filter(m => {
|
||||
// Match against persona handle
|
||||
const handle = (m.personaHandle || '').toLowerCase().replace(/-/g, ' ');
|
||||
// Match against model ID
|
||||
const modelId = (m.baseModelId || m.id || '').toLowerCase().replace(/-/g, ' ');
|
||||
// Match against display name
|
||||
const name = (m.name || '').toLowerCase().replace(/-/g, ' ');
|
||||
const firstName = name.split(' ')[0] || '';
|
||||
|
||||
if (partial.length === 0) return true; // show all on bare @
|
||||
return handle.startsWith(partial)
|
||||
|| modelId.startsWith(partial)
|
||||
|| name.startsWith(partial)
|
||||
|| firstName.startsWith(partial);
|
||||
});
|
||||
|
||||
if (matches.length === 0) { this.hideAutocomplete(); return; }
|
||||
|
||||
this.showAutocomplete(inputEl, atIdx, cursorPos, matches);
|
||||
// Cap at 10 to keep dropdown manageable
|
||||
this.showAutocomplete(inputEl, atIdx, cursorPos, matches.slice(0, 10));
|
||||
},
|
||||
|
||||
showAutocomplete(inputEl, atIdx, cursorPos, matches) {
|
||||
@@ -221,19 +234,36 @@ const ChannelModels = {
|
||||
wrap.className = 'mention-ac';
|
||||
wrap.id = 'mentionAc';
|
||||
|
||||
// Position relative to input
|
||||
const inputRect = (inputEl.el || inputEl).getBoundingClientRect();
|
||||
wrap.style.bottom = (window.innerHeight - inputRect.top + 4) + 'px';
|
||||
wrap.style.left = inputRect.left + 'px';
|
||||
wrap.style.minWidth = '200px';
|
||||
wrap.style.minWidth = '280px';
|
||||
|
||||
matches.forEach((m, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'mention-ac-item' + (i === 0 ? ' mention-ac-active' : '');
|
||||
item.dataset.name = m.display_name;
|
||||
item.innerHTML = `<span class="mention-ac-name">${esc(m.display_name)}</span><span class="mention-ac-model">${esc(m.model_id)}</span>`;
|
||||
|
||||
// Determine the @handle to insert
|
||||
const mentionToken = m.isPersona
|
||||
? (m.personaHandle || m.name?.replace(/\s+/g, '-').toLowerCase() || m.id)
|
||||
: (m.baseModelId || m.id);
|
||||
item.dataset.handle = mentionToken;
|
||||
|
||||
const avatar = m.personaAvatar
|
||||
? `<img src="${esc(m.personaAvatar)}" class="mention-ac-avatar" alt="">`
|
||||
: m.isPersona
|
||||
? `<span class="mention-ac-avatar mention-ac-avatar-fallback">⚡</span>`
|
||||
: `<span class="mention-ac-avatar mention-ac-avatar-fallback">🤖</span>`;
|
||||
|
||||
const handleText = m.isPersona && m.personaHandle
|
||||
? `@${esc(m.personaHandle)}`
|
||||
: `@${esc(m.baseModelId || m.id)}`;
|
||||
|
||||
const providerHint = m.provider ? esc(m.provider) : '';
|
||||
|
||||
item.innerHTML = `${avatar}<div class="mention-ac-info"><span class="mention-ac-name">${esc(m.name || m.id)}</span><span class="mention-ac-handle">${handleText}</span></div><span class="mention-ac-model">${providerHint}</span>`;
|
||||
item.addEventListener('click', () => {
|
||||
this._insertMention(inputEl, atIdx, cursorPos, m.display_name);
|
||||
this._insertMention(inputEl, atIdx, cursorPos, mentionToken);
|
||||
});
|
||||
wrap.appendChild(item);
|
||||
});
|
||||
@@ -278,9 +308,7 @@ const ChannelModels = {
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const active = items[activeIdx];
|
||||
if (active) {
|
||||
active.click();
|
||||
}
|
||||
if (active) active.click();
|
||||
return true;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
@@ -326,6 +354,31 @@ const ChannelModels = {
|
||||
if (cm?.display_name) return cm.display_name;
|
||||
const m = App.models?.find(x => x.id === modelId || x.baseModelId === modelId);
|
||||
return m?.name || modelId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve persona avatar for a channel model roster entry.
|
||||
* Looks up via persona_id in App.models.
|
||||
*/
|
||||
_resolveAvatar(rosterEntry) {
|
||||
if (!rosterEntry.persona_id) return null;
|
||||
const persona = App.models?.find(m => m.personaId === rosterEntry.persona_id);
|
||||
return persona?.personaAvatar || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get persona info for a given model_id from the roster + App.models.
|
||||
* Returns { displayName, avatar, personaId } or null.
|
||||
*/
|
||||
resolvePersonaInfo(modelId) {
|
||||
const cm = this._roster.find(m => m.model_id === modelId);
|
||||
if (!cm) return null;
|
||||
const avatar = this._resolveAvatar(cm);
|
||||
return {
|
||||
displayName: cm.display_name || modelId,
|
||||
avatar: avatar,
|
||||
personaId: cm.persona_id || null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user