Changeset 0.23.0 (#153)
This commit is contained in:
@@ -17,7 +17,17 @@ function avatarHTML(role, avatarDataURI) {
|
||||
|
||||
// Look up the current model/persona avatar for assistant messages.
|
||||
function assistantAvatarURI(msg) {
|
||||
// Try to find persona avatar from the model that generated this message
|
||||
// 1. Check channel model roster for persona avatar (v0.23.0)
|
||||
if (typeof ChannelModels !== 'undefined' && msg?.model) {
|
||||
const info = ChannelModels.resolvePersonaInfo(msg.model);
|
||||
if (info?.avatar) return info.avatar;
|
||||
}
|
||||
// 2. Check participant_id directly (for chained messages)
|
||||
if (msg?.participant_type === 'persona' && msg?.participant_id) {
|
||||
const m = App.models.find(x => x.personaId === msg.participant_id);
|
||||
if (m?.personaAvatar) return m.personaAvatar;
|
||||
}
|
||||
// 3. Standard lookup by model ID or persona ID
|
||||
const modelId = msg?.model || msg?.persona_id;
|
||||
if (modelId) {
|
||||
const m = App.models.find(x => x.id === modelId || x.personaId === modelId);
|
||||
@@ -37,6 +47,7 @@ function renderPersonaForm(containerEl, options = {}) {
|
||||
|
||||
containerEl.innerHTML = `
|
||||
<div class="form-group"><label>Name</label><input type="text" id="${pfx}_name" placeholder="Code Reviewer"></div>
|
||||
<div class="form-group"><label>@mention Handle</label><input type="text" id="${pfx}_handle" placeholder="code-reviewer" maxlength="50" class="mono-input"><div class="form-hint">Auto-generated from name. Used for @mentions in chat.</div></div>
|
||||
${showAvatar ? `
|
||||
<div class="avatar-upload-row" style="margin-bottom:0.75rem">
|
||||
<div class="avatar-preview avatar-preview-sm" id="${pfx}_avatarPreview">
|
||||
@@ -95,6 +106,22 @@ function renderPersonaForm(containerEl, options = {}) {
|
||||
if (options.onCancel) options.onCancel();
|
||||
});
|
||||
|
||||
// Auto-generate handle from name
|
||||
let _handleManuallyEdited = false;
|
||||
document.getElementById(`${pfx}_handle`)?.addEventListener('input', () => { _handleManuallyEdited = true; });
|
||||
document.getElementById(`${pfx}_name`)?.addEventListener('input', () => {
|
||||
if (_handleManuallyEdited) return;
|
||||
const name = document.getElementById(`${pfx}_name`)?.value || '';
|
||||
const handle = name.toLowerCase().trim()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 50);
|
||||
const hEl = document.getElementById(`${pfx}_handle`);
|
||||
if (hEl) hEl.value = handle;
|
||||
});
|
||||
|
||||
// Submit wiring
|
||||
document.getElementById(`${pfx}_submitBtn`)?.addEventListener('click', () => {
|
||||
if (options.onSubmit) options.onSubmit(form.getValues());
|
||||
@@ -104,6 +131,7 @@ function renderPersonaForm(containerEl, options = {}) {
|
||||
getValues() {
|
||||
const v = {
|
||||
name: document.getElementById(`${pfx}_name`)?.value.trim() || '',
|
||||
handle: document.getElementById(`${pfx}_handle`)?.value.trim() || '',
|
||||
description: document.getElementById(`${pfx}_desc`)?.value.trim() || '',
|
||||
base_model_id: document.getElementById(`${pfx}_model`)?.value || '',
|
||||
system_prompt: document.getElementById(`${pfx}_prompt`)?.value || '',
|
||||
@@ -129,6 +157,7 @@ function renderPersonaForm(containerEl, options = {}) {
|
||||
setValues(p) {
|
||||
const el = id => document.getElementById(`${pfx}_${id}`);
|
||||
if (el('name')) el('name').value = p.name || '';
|
||||
if (el('handle')) { el('handle').value = p.handle || ''; _handleManuallyEdited = !!p.handle; }
|
||||
if (el('desc')) el('desc').value = p.description || '';
|
||||
if (el('prompt')) el('prompt').value = p.system_prompt || '';
|
||||
if (el('temp')) el('temp').value = p.temperature != null ? p.temperature : '';
|
||||
@@ -141,7 +170,7 @@ function renderPersonaForm(containerEl, options = {}) {
|
||||
if (el('memoryExtractionPrompt')) el('memoryExtractionPrompt').value = p.memory_extraction_prompt || '';
|
||||
},
|
||||
clearForm() {
|
||||
['name','desc','prompt','temp','maxTokens'].forEach(f => {
|
||||
['name','handle','desc','prompt','temp','maxTokens'].forEach(f => {
|
||||
const el = document.getElementById(`${pfx}_${f}`);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
@@ -288,6 +317,8 @@ const UI = {
|
||||
// ── Chat item renderer ──────────────────
|
||||
const renderChatItem = (c) => {
|
||||
const time = _relativeTime(c.updatedAt);
|
||||
const typeIcon = c.type === 'group' ? '<span class="chat-type-icon" title="Group Chat">👥</span> '
|
||||
: c.type === 'workflow' ? '<span class="chat-type-icon" title="Workflow">⚙</span> ' : '';
|
||||
return `
|
||||
<div class="chat-item ${c.id === App.currentChatId ? 'active' : ''}"
|
||||
onclick="selectChat('${c.id}')"
|
||||
@@ -295,7 +326,7 @@ const UI = {
|
||||
draggable="true"
|
||||
ondragstart="onChatDragStart(event,'${c.id}')"
|
||||
ondragend="onChatDragEnd(event)">
|
||||
<span class="chat-item-title" ondblclick="startRenameChat('${c.id}');event.stopPropagation();" title="Double-click to rename">${esc(c.title)}</span>
|
||||
<span class="chat-item-title" ondblclick="startRenameChat('${c.id}');event.stopPropagation();" title="Double-click to rename">${typeIcon}${esc(c.title)}</span>
|
||||
<span class="chat-item-time">${time}</span>
|
||||
<button class="chat-item-delete" onclick="deleteChat('${c.id}');event.stopPropagation();" title="Delete">✕</button>
|
||||
</div>`;
|
||||
@@ -487,9 +518,11 @@ const UI = {
|
||||
const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
||||
let assistantLabel = 'Assistant';
|
||||
if (!isUser) {
|
||||
// Use stored modelName, or resolve from current model list, or fall back to model id
|
||||
// Use stored modelName, or resolve from channel roster, or model list, or fall back
|
||||
if (msg.modelName) {
|
||||
assistantLabel = msg.modelName;
|
||||
} else if (typeof ChannelModels !== 'undefined' && msg.model) {
|
||||
assistantLabel = ChannelModels.resolveDisplayName(msg.model);
|
||||
} else if (msg.model) {
|
||||
const m = App.models.find(x => x.id === msg.model || x.baseModelId === msg.model);
|
||||
assistantLabel = m?.name || msg.model;
|
||||
@@ -598,7 +631,15 @@ const UI = {
|
||||
|
||||
const label = modelName || 'Assistant';
|
||||
const currentModel = App.findModel(App.settings.model);
|
||||
const streamAvatar = currentModel?.personaAvatar || null;
|
||||
// Resolve avatar: channel roster persona → selected model → null
|
||||
let streamAvatar = null;
|
||||
if (typeof ChannelModels !== 'undefined') {
|
||||
const info = ChannelModels.resolvePersonaInfo(currentModel?.baseModelId);
|
||||
if (info?.avatar) streamAvatar = info.avatar;
|
||||
}
|
||||
if (!streamAvatar) {
|
||||
streamAvatar = currentModel?.personaAvatar || null;
|
||||
}
|
||||
|
||||
// Multi-model state: when model_start events arrive, we create
|
||||
// separate message bubbles per model. Without them, single-bubble.
|
||||
@@ -911,8 +952,9 @@ const UI = {
|
||||
div.className = 'model-dropdown-item';
|
||||
div.dataset.value = m.id;
|
||||
const avatar = m.personaAvatar ? `<img src="${m.personaAvatar}" class="dropdown-avatar" alt="">` : '';
|
||||
const handleHint = m.isPersona && m.personaHandle ? `<span class="item-handle">@${esc(m.personaHandle)}</span>` : '';
|
||||
const teamBadge = m.source === 'team' && m.teamName ? `<span class="badge-team" style="font-size:9px;padding:0 4px">👥 ${esc(m.teamName)}</span>` : '';
|
||||
div.innerHTML = `${avatar}<span class="item-label">${esc(m.name || m.id)}</span>${teamBadge}${m.provider ? `<span class="item-provider">${esc(m.provider)}</span>` : ''}`;
|
||||
div.innerHTML = `${avatar}<span class="item-label">${esc(m.name || m.id)}${handleHint}</span>${teamBadge}${m.provider ? `<span class="item-provider">${esc(m.provider)}</span>` : ''}`;
|
||||
div.addEventListener('click', () => {
|
||||
UI.setModelValue(m.id, (m.name || m.id) + (m.provider ? ` (${m.provider})` : ''));
|
||||
UI._closeModelDropdown();
|
||||
|
||||
Reference in New Issue
Block a user