Clarify model scope in composer and settings
This commit is contained in:
1 parent
2487de2cc0
commit
6f37da38a6
7 files changed
+83
-16
No files matched your search
+3
-3
@@ -455,9 +455,9 @@ $('modelSelect').onchange=async()=>{
|
||||
const warn=_checkProviderMismatch(selectedModel);
|
||||
if(warn&&typeof showToast==='function') showToast(warn,4000);
|
||||
}
|
||||
// Notify user that model changes only take effect in the next conversation (#419)
|
||||
if(S.messages && S.messages.length > 0 && typeof showToast==='function'){
|
||||
showToast('Model change takes effect in your next conversation', 3000);
|
||||
// Clarify scope: composer model changes are session-local, not the global default.
|
||||
if(typeof showToast==='function'){
|
||||
showToast(t('model_scope_toast')||'Applies to this conversation from your next message.', 3000);
|
||||
}
|
||||
};
|
||||
$('msg').addEventListener('input',()=>{
|
||||
|
||||
@@ -70,6 +70,8 @@ const LOCALES = {
|
||||
model_custom_placeholder: 'e.g. openai/gpt-5.4',
|
||||
model_search_placeholder: 'Search models…',
|
||||
model_search_no_results: 'No models found',
|
||||
model_scope_advisory: 'Applies to this conversation from your next message.',
|
||||
model_scope_toast: 'Applies to this conversation from your next message.',
|
||||
// commands.js
|
||||
cmd_clear: 'Clear conversation messages',
|
||||
cmd_compress: 'Manually compress conversation context (usage: /compress [focus topic])',
|
||||
@@ -298,6 +300,7 @@ const LOCALES = {
|
||||
settings_title: 'Settings',
|
||||
settings_save_btn: 'Save Settings',
|
||||
settings_label_model: 'Default Model',
|
||||
settings_desc_model: 'Used for new conversations. Existing conversations keep their selected model.',
|
||||
settings_label_send_key: 'Send Key',
|
||||
settings_label_theme: 'Theme',
|
||||
settings_label_skin: 'Skin',
|
||||
@@ -824,6 +827,8 @@ const LOCALES = {
|
||||
focus_label: 'Фокус',
|
||||
model_search_no_results: 'Модели не найдены',
|
||||
model_search_placeholder: 'Поиск моделей…',
|
||||
model_scope_advisory: 'Применяется к этой беседе со следующего сообщения.',
|
||||
model_scope_toast: 'Применяется к этой беседе со следующего сообщения.',
|
||||
reference_only_label: 'Только справка',
|
||||
settings_label_skin: 'Скин',
|
||||
settings_label_font_size: 'Размер шрифта',
|
||||
@@ -885,6 +890,7 @@ const LOCALES = {
|
||||
settings_title: 'Настройки',
|
||||
settings_save_btn: 'Сохранить настройки',
|
||||
settings_label_model: 'Модель по умолчанию',
|
||||
settings_desc_model: 'Используется для новых бесед. Существующие беседы сохраняют выбранную модель.',
|
||||
settings_label_send_key: 'Клавиша отправки',
|
||||
settings_label_theme: 'Тема',
|
||||
settings_label_language: 'Язык',
|
||||
@@ -1407,6 +1413,8 @@ const LOCALES = {
|
||||
model_custom_placeholder: 'p. ej. openai/gpt-5.4',
|
||||
model_search_placeholder: 'Buscar modelos…',
|
||||
model_search_no_results: 'No se encontraron modelos',
|
||||
model_scope_advisory: 'Se aplica a esta conversación desde tu próximo mensaje.',
|
||||
model_scope_toast: 'Se aplica a esta conversación desde tu próximo mensaje.',
|
||||
// commands.js
|
||||
cmd_help: 'Listar los comandos disponibles',
|
||||
cmd_clear: 'Borrar los mensajes de la conversación',
|
||||
@@ -1518,6 +1526,7 @@ const LOCALES = {
|
||||
settings_title: 'Configuración',
|
||||
settings_save_btn: 'Guardar configuración',
|
||||
settings_label_model: 'Modelo predeterminado',
|
||||
settings_desc_model: 'Se usa para conversaciones nuevas. Las conversaciones existentes conservan su modelo seleccionado.',
|
||||
settings_label_send_key: 'Tecla de envío',
|
||||
settings_label_theme: 'Tema',
|
||||
settings_label_skin: 'Piel',
|
||||
@@ -2079,6 +2088,8 @@ const LOCALES = {
|
||||
theme_usage: 'Nutzung: /theme ',
|
||||
theme_set: 'Theme: ',
|
||||
no_active_session: 'Keine aktive Sitzung',
|
||||
model_scope_advisory: 'Gilt für diesen Chat ab Ihrer nächsten Nachricht.',
|
||||
model_scope_toast: 'Gilt für diesen Chat ab Ihrer nächsten Nachricht.',
|
||||
cmd_queue: 'Nachricht f\u00fcr den n\u00e4chsten Durchgang einreihen',
|
||||
cmd_interrupt: 'Aktuellen Durchgang abbrechen und neue Nachricht senden',
|
||||
cmd_steer: 'Korrektursignal einf\u00fcgen ohne Unterbrechung',
|
||||
@@ -2156,6 +2167,7 @@ const LOCALES = {
|
||||
settings_title: 'Einstellungen',
|
||||
settings_save_btn: 'Einstellungen speichern',
|
||||
settings_label_model: 'Standard-Modell',
|
||||
settings_desc_model: 'Wird für neue Chats verwendet. Bestehende Chats behalten ihr ausgewähltes Modell.',
|
||||
settings_label_send_key: 'Sende-Taste',
|
||||
settings_label_theme: 'Theme',
|
||||
settings_label_skin: 'Skin',
|
||||
@@ -2452,6 +2464,8 @@ const LOCALES = {
|
||||
model_custom_placeholder: '\u4f8b\u5982 openai/gpt-5.4',
|
||||
model_search_placeholder: '\u641c\u7d22\u6a21\u578b\u2026',
|
||||
model_search_no_results: '\u672a\u627e\u5230\u6a21\u578b',
|
||||
model_scope_advisory: '\u4ece\u4e0b\u4e00\u6761\u6d88\u606f\u8d77\u5e94\u7528\u4e8e\u5f53\u524d\u5bf9\u8bdd\u3002',
|
||||
model_scope_toast: '\u4ece\u4e0b\u4e00\u6761\u6d88\u606f\u8d77\u5e94\u7528\u4e8e\u5f53\u524d\u5bf9\u8bdd\u3002',
|
||||
// commands.js
|
||||
cmd_help: '\u67e5\u770b\u53ef\u7528\u547d\u4ee4',
|
||||
cmd_clear: '\u6e05\u7a7a\u5f53\u524d\u5bf9\u8bdd\u6d88\u606f',
|
||||
@@ -2570,6 +2584,7 @@ const LOCALES = {
|
||||
settings_title: '\u8bbe\u7f6e',
|
||||
settings_save_btn: '\u4fdd\u5b58\u8bbe\u7f6e',
|
||||
settings_label_model: '\u9ed8\u8ba4\u6a21\u578b',
|
||||
settings_desc_model: '\u7528\u4e8e\u65b0\u5bf9\u8bdd\u3002\u73b0\u6709\u5bf9\u8bdd\u4fdd\u6301\u5404\u81ea\u9009\u5b9a\u7684\u6a21\u578b\u3002',
|
||||
settings_label_send_key: '\u53d1\u9001\u5feb\u6377\u952e',
|
||||
settings_label_theme: '\u4e3b\u9898',
|
||||
settings_label_skin: '\u76ae\u80a4',
|
||||
@@ -3217,6 +3232,7 @@ const LOCALES = {
|
||||
settings_title: '\u8a2d\u5b9a',
|
||||
settings_save_btn: '\u5132\u5b58\u8a2d\u5b9a',
|
||||
settings_label_model: '\u9ed8\u8a8d\u6a21\u578b',
|
||||
settings_desc_model: '\u7528\u65bc\u65b0\u6703\u8a71\u3002\u73fe\u6709\u6703\u8a71\u6703\u4fdd\u7559\u5404\u81ea\u9078\u5b9a\u7684\u6a21\u578b\u3002',
|
||||
settings_label_send_key: '\u767c\u9001\u5feb\u6377\u9375',
|
||||
settings_label_theme: '\u4e3b\u984c',
|
||||
settings_label_skin: '佈景',
|
||||
@@ -3468,6 +3484,8 @@ const LOCALES = {
|
||||
model_custom_placeholder: '\u4f8b\u5982 openai/gpt-5.4',
|
||||
model_search_no_results: '\u627e\u4e0d\u5230\u6a21\u578b',
|
||||
model_search_placeholder: '\u641c\u5c0b\u6a21\u578b\u2026',
|
||||
model_scope_advisory: '\u5f9e\u4e0b\u4e00\u5247\u8a0a\u606f\u8d77\u9069\u7528\u65bc\u6b64\u6703\u8a71\u3002',
|
||||
model_scope_toast: '\u5f9e\u4e0b\u4e00\u5247\u8a0a\u606f\u8d77\u9069\u7528\u65bc\u6b64\u6703\u8a71\u3002',
|
||||
my_notes: '\u6211\u7684\u5099\u8a3b',
|
||||
name_required: '\u9700\u8981\u540d\u7a31',
|
||||
never: '\u5f9e\u4e0d',
|
||||
@@ -3767,6 +3785,8 @@ const LOCALES = {
|
||||
model_custom_placeholder: 'e.g. openai/gpt-5.4',
|
||||
model_search_placeholder: 'Search models…',
|
||||
model_search_no_results: 'No models found',
|
||||
model_scope_advisory: '다음 메시지부터 이 대화에 적용됩니다.',
|
||||
model_scope_toast: '다음 메시지부터 이 대화에 적용됩니다.',
|
||||
// commands.js
|
||||
cmd_clear: '대화 메시지 지우기',
|
||||
cmd_compress: 'Manually compress conversation context (usage: /compress [focus topic])',
|
||||
@@ -3994,6 +4014,7 @@ const LOCALES = {
|
||||
settings_title: '설정',
|
||||
settings_save_btn: '설정 저장',
|
||||
settings_label_model: '기본 모델',
|
||||
settings_desc_model: '새 대화에 사용됩니다. 기존 대화는 선택된 모델을 유지합니다.',
|
||||
settings_label_send_key: '전송 키',
|
||||
settings_label_theme: '테마',
|
||||
settings_label_skin: '스킨',
|
||||
|
||||
@@ -625,6 +625,7 @@
|
||||
<div class="settings-field">
|
||||
<label for="settingsModel" data-i18n="settings_label_model">Default Model</label>
|
||||
<select id="settingsModel" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px"></select>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_model">Used for new conversations. Existing conversations keep their selected model.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="settingsSendKey" data-i18n="settings_label_send_key">Send Key</label>
|
||||
|
||||
@@ -976,6 +976,7 @@
|
||||
.ws-dropdown-footer{left:0;right:auto;bottom:calc(100% + 4px);min-width:280px;max-width:min(420px,calc(100vw - 32px));}
|
||||
.model-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;min-width:280px;max-width:min(420px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:200;overflow:hidden;max-height:320px;overflow-y:auto;}
|
||||
.model-dropdown.open{display:block;}
|
||||
.model-scope-note{position:sticky;top:0;z-index:2;padding:9px 14px;border-bottom:1px solid var(--border);color:var(--text);font-size:11px;font-weight:650;line-height:1.4;background:color-mix(in srgb,var(--surface) 82%,var(--accent-bg));box-shadow:0 1px 0 rgba(0,0,0,.12);}
|
||||
.model-group{padding:8px 14px 4px;font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted);text-transform:uppercase;}
|
||||
.model-opt{padding:10px 14px;cursor:pointer;transition:background .12s;display:flex;flex-direction:column;gap:3px;align-items:flex-start;}
|
||||
.model-opt:hover{background:rgba(255,255,255,.07);}
|
||||
|
||||
@@ -350,6 +350,9 @@ function renderModelDropdown(){
|
||||
}
|
||||
}
|
||||
// Create search input FIRST before filterModels definition
|
||||
const _scopeNote=document.createElement('div');
|
||||
_scopeNote.className='model-scope-note';
|
||||
_scopeNote.textContent=t('model_scope_advisory')||'Applies to this conversation from your next message.';
|
||||
const _searchRow=document.createElement('div');
|
||||
_searchRow.className='model-search-row';
|
||||
_searchRow.innerHTML=`<input class="model-search-input" type="text" placeholder="${esc(t('model_search_placeholder')||'Search models…')}" spellcheck="false" autocomplete="off"><button class="model-search-clear" title="Clear search">${li('x',10)}</button>`;
|
||||
@@ -378,6 +381,7 @@ function renderModelDropdown(){
|
||||
// Clear and rebuild
|
||||
dd.innerHTML='';
|
||||
// Add search and custom elements first (CRITICAL: must be before models)
|
||||
dd.appendChild(_scopeNote);
|
||||
dd.appendChild(_searchRow);
|
||||
dd.appendChild(_custSep);
|
||||
dd.appendChild(_custRow);
|
||||
@@ -425,6 +429,7 @@ function renderModelDropdown(){
|
||||
_ci.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();_applyCustom();}if(e.key==='Escape'){closeModelDropdown();}});
|
||||
_ci.addEventListener('click',e=>e.stopPropagation());
|
||||
// Add search and custom elements to dropdown (initial render)
|
||||
dd.appendChild(_scopeNote);
|
||||
dd.appendChild(_searchRow);
|
||||
dd.appendChild(_custSep);
|
||||
dd.appendChild(_custRow);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def read(rel: str) -> str:
|
||||
return (REPO / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_composer_model_dropdown_has_scope_advisory():
|
||||
ui = read("static/ui.js")
|
||||
style = read("static/style.css")
|
||||
|
||||
assert "model-scope-note" in ui
|
||||
assert "model_scope_advisory" in ui
|
||||
assert "Applies to this conversation from your next message." in ui
|
||||
assert ui.index("dd.appendChild(_scopeNote);") < ui.index("dd.appendChild(_searchRow);")
|
||||
assert ".model-scope-note" in style
|
||||
assert "position:sticky" in style
|
||||
|
||||
|
||||
def test_model_selection_toast_describes_conversation_scope():
|
||||
boot = read("static/boot.js")
|
||||
i18n = read("static/i18n.js")
|
||||
|
||||
assert "model_scope_toast" in boot
|
||||
assert "Applies to this conversation from your next message." in i18n
|
||||
assert "model_scope_advisory: 'Applies to this conversation from your next message.'" in i18n
|
||||
assert "model_scope_toast: 'Applies to this conversation from your next message.'" in i18n
|
||||
assert "Model change takes effect in your next conversation" not in boot
|
||||
|
||||
|
||||
def test_settings_default_model_copy_describes_new_conversations():
|
||||
html = read("static/index.html")
|
||||
i18n = read("static/i18n.js")
|
||||
|
||||
assert 'data-i18n="settings_desc_model"' in html
|
||||
assert "Used for new conversations. Existing conversations keep their selected model." in html
|
||||
assert "settings_desc_model" in i18n
|
||||
@@ -576,34 +576,33 @@ def test_api_session_is_side_effect_free_for_stale_models():
|
||||
# ── Model switch toast (#419) ─────────────────────────────────────────────────
|
||||
|
||||
class TestModelSwitchToast:
|
||||
"""Toast appears when user switches model during an active session."""
|
||||
"""Toast appears when user switches the current conversation model."""
|
||||
|
||||
def test_toast_in_model_select_onchange(self):
|
||||
"""modelSelect.onchange must show a toast when S.messages is non-empty."""
|
||||
"""modelSelect.onchange must show a scope toast after selecting a model."""
|
||||
src = _read("static/boot.js")
|
||||
# Find the onchange block
|
||||
idx = src.find("modelSelect').onchange")
|
||||
assert idx != -1, "modelSelect.onchange not found in boot.js"
|
||||
block = src[idx:idx + 1100]
|
||||
assert "Model change takes effect in your next conversation" in block, (
|
||||
"modelSelect.onchange must show a toast when switching model mid-session"
|
||||
assert "model_scope_toast" in block, (
|
||||
"modelSelect.onchange must show that the selected model applies to this conversation"
|
||||
)
|
||||
|
||||
def test_toast_guards_on_messages_length(self):
|
||||
"""Toast must only fire when there are existing messages (active session)."""
|
||||
def test_toast_is_not_gated_on_messages_length(self):
|
||||
"""Toast must fire for every model selection, not only sessions with messages."""
|
||||
src = _read("static/boot.js")
|
||||
idx = src.find("Model change takes effect in your next conversation")
|
||||
idx = src.find("model_scope_toast")
|
||||
assert idx != -1
|
||||
# Look back 200 chars for the S.messages guard
|
||||
surrounding = src[max(0, idx - 200):idx + 50]
|
||||
assert "S.messages" in surrounding and ".length" in surrounding, (
|
||||
"Model switch toast must be gated on S.messages.length > 0"
|
||||
surrounding = src[max(0, idx - 220):idx + 80]
|
||||
assert not ("S.messages" in surrounding and ".length" in surrounding), (
|
||||
"Model scope toast should not be gated on S.messages.length"
|
||||
)
|
||||
|
||||
def test_toast_uses_show_toast_not_alert(self):
|
||||
"""Toast must use showToast(), not alert()."""
|
||||
src = _read("static/boot.js")
|
||||
idx = src.find("Model change takes effect in your next conversation")
|
||||
idx = src.find("model_scope_toast")
|
||||
assert idx != -1
|
||||
surrounding = src[max(0, idx - 50):idx + 100]
|
||||
assert "showToast" in surrounding, "Must use showToast() not alert()"
|
||||
@@ -612,7 +611,7 @@ class TestModelSwitchToast:
|
||||
def test_toast_has_typeof_showtoast_guard(self):
|
||||
"""Toast call must guard typeof showToast to be safe during boot."""
|
||||
src = _read("static/boot.js")
|
||||
idx = src.find("Model change takes effect in your next conversation")
|
||||
idx = src.find("model_scope_toast")
|
||||
assert idx != -1
|
||||
surrounding = src[max(0, idx - 100):idx + 50]
|
||||
assert "typeof showToast" in surrounding, (
|
||||
|
||||
Reference in new issue
Block a user