fix(chat-ui): model selection toggle and new chat (#7574)

Fixes a minor glitch that happens when switching model in from the chat
pane where the header was not getting updated. Besides, it allows to
create new chat directly when clicking from the management pane to the
model.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2025-12-14 22:29:11 +01:00
committed by GitHub
parent 0f5cc4c07b
commit 8ac7e8c299
3 changed files with 141 additions and 48 deletions

View File

@@ -2473,23 +2473,53 @@ document.addEventListener('DOMContentLoaded', function() {
localStorage.removeItem(SYSTEM_PROMPT_STORAGE_KEY);
}
} else {
// Existing chats loaded - check URL parameter for MCP mode
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('mcp') === 'true') {
const activeChat = chatStore.activeChat();
if (activeChat) {
activeChat.mcpMode = true;
saveChatsToStorage();
// Existing chats loaded - check if we need to create a new chat for the model in URL
const urlModel = document.getElementById("chat-model")?.value || "";
const activeChat = chatStore.activeChat();
const shouldCreateNewChat = sessionStorage.getItem('localai_create_new_chat') === 'true';
// Clear the flag after reading it
if (shouldCreateNewChat) {
sessionStorage.removeItem('localai_create_new_chat');
}
// If we should create a new chat (from manage.html) or URL model doesn't match active chat, create new chat
// This handles navigation from manage.html or direct links to /chat/MODEL_NAME
if (urlModel && urlModel.trim() && (shouldCreateNewChat || (activeChat && activeChat.model !== urlModel) || !activeChat)) {
// Create a new chat with the model from URL
const urlParams = new URLSearchParams(window.location.search);
const mcpFromUrl = urlParams.get('mcp') === 'true';
const newChat = chatStore.createChat(urlModel, "", mcpFromUrl);
// Update context size from template if available
const contextSizeInput = document.getElementById("chat-model");
if (contextSizeInput && contextSizeInput.dataset.contextSize) {
const contextSize = parseInt(contextSizeInput.dataset.contextSize);
if (!isNaN(contextSize)) {
newChat.contextSize = contextSize;
}
}
saveChatsToStorage();
updateUIForActiveChat();
} else {
// Check URL parameter for MCP mode (update existing active chat)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('mcp') === 'true') {
if (activeChat) {
activeChat.mcpMode = true;
saveChatsToStorage();
}
}
}
}
// Update context size from template if available
// Update context size from template if available (for existing active chat)
const contextSizeInput = document.getElementById("chat-model");
if (contextSizeInput && contextSizeInput.dataset.contextSize) {
const contextSize = parseInt(contextSizeInput.dataset.contextSize);
const activeChat = chatStore.activeChat();
if (activeChat) {
if (activeChat && !activeChat.contextSize) {
activeChat.contextSize = contextSize;
}
}

View File

@@ -41,6 +41,16 @@ SOFTWARE.
__chatContextSize = {{ .ContextSize }};
{{ end }}
// Store gallery configs for header icon display
window.__galleryConfigs = {};
{{ $allGalleryConfigs:=.GalleryConfig }}
{{ range $modelName, $galleryConfig := $allGalleryConfigs }}
window.__galleryConfigs["{{$modelName}}"] = {};
{{ if $galleryConfig.Icon }}
window.__galleryConfigs["{{$modelName}}"].Icon = "{{$galleryConfig.Icon}}";
{{ end }}
{{ end }}
// Function to initialize store
function __initChatStore() {
if (!window.Alpine) return;
@@ -501,6 +511,21 @@ SOFTWARE.
}
}
// Update model selector to reflect the change (ensure it stays in sync)
const modelSelector = document.getElementById('modelSelector');
if (modelSelector) {
// Find and select the option matching the model
const optionValue = 'chat/' + modelName;
for (let i = 0; i < modelSelector.options.length; i++) {
if (modelSelector.options[i].value === optionValue) {
modelSelector.selectedIndex = i;
break;
}
}
// Trigger Alpine reactivity by dispatching change event
modelSelector.dispatchEvent(new Event('change', { bubbles: true }));
}
// Trigger MCP availability check in Alpine component
// The MCP toggle component will reactively check the data-has-mcp attribute
@@ -513,14 +538,6 @@ SOFTWARE.
if (typeof updateUIForActiveChat === 'function') {
updateUIForActiveChat();
}
// Trigger MCP availability check in Alpine component
// Dispatch a custom event that the MCP toggle component can listen to
const modelSelector = document.getElementById('modelSelector');
if (modelSelector) {
// Trigger Alpine reactivity by dispatching change event
modelSelector.dispatchEvent(new Event('change', { bubbles: true }));
}
}
</script>
<script defer src="static/chat.js"></script>
@@ -559,29 +576,31 @@ SOFTWARE.
<div class="p-3 space-y-3">
<!-- Model selection - Compact -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<label class="text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide">Model</label>
{{ if $model }}
{{ $galleryConfig:= index $allGalleryConfigs $model}}
{{ if $galleryConfig }}
<button
data-twe-ripple-init
data-twe-ripple-color="light"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors text-xs p-1"
data-modal-target="model-info-modal"
data-modal-toggle="model-info-modal"
title="Model Information">
<i class="fas fa-info-circle"></i>
</button>
{{ end }}
{{ end }}
{{ if $model }}
<a href="/models/edit/{{$model}}"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-warning)] transition-colors text-xs p-1"
title="Edit Model Configuration">
<i class="fas fa-edit"></i>
</a>
{{ end }}
<div class="flex items-center justify-between gap-2">
<label class="text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide flex-shrink-0">Model</label>
<div class="flex items-center gap-1 flex-shrink-0">
{{ if $model }}
{{ $galleryConfig:= index $allGalleryConfigs $model}}
{{ if $galleryConfig }}
<button
data-twe-ripple-init
data-twe-ripple-color="light"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors text-xs p-1 rounded hover:bg-[var(--color-bg-primary)]"
data-modal-target="model-info-modal"
data-modal-toggle="model-info-modal"
title="Model Information">
<i class="fas fa-info-circle"></i>
</button>
{{ end }}
{{ end }}
{{ if $model }}
<a href="/models/edit/{{$model}}"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-warning)] transition-colors text-xs p-1 rounded hover:bg-[var(--color-bg-primary)]"
title="Edit Model Configuration">
<i class="fas fa-edit"></i>
</a>
{{ end }}
</div>
</div>
<select
id="modelSelector"
@@ -1030,14 +1049,22 @@ SOFTWARE.
<div class="flex items-center">
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)]"></i>
{{ if $model }}
{{ $galleryConfig:= index $allGalleryConfigs $model}}
{{ if $galleryConfig }}
{{ if $galleryConfig.Icon }}<img src="{{$galleryConfig.Icon}}" class="rounded-lg w-8 h-8 mr-2">{{end}}
{{ end }}
{{ end }}
<!-- Model icon - reactive to active chat -->
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model && window.__galleryConfigs && window.__galleryConfigs[$store.chat.activeChat().model] && window.__galleryConfigs[$store.chat.activeChat().model].Icon">
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2">
</template>
<!-- Fallback icon for initial model from server (when no active chat yet) -->
<template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2">
</template>
<h1 class="text-lg font-semibold text-[var(--color-text-primary)]">
Chat {{ if .Model }} with {{.Model}} {{ end }}
Chat
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
<span x-text="' with ' + $store.chat.activeChat().model"></span>
</template>
<template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
{{ if .Model }}<span> with {{.Model}}</span>{{ end }}
</template>
</h1>
<!-- Loading indicator next to model name -->
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)]" style="display: none;">
@@ -1698,6 +1725,42 @@ SOFTWARE.
} else {
initMarkdownProcessing();
}
// Sync model selector with initial model from server on page load
// This ensures the selector is correct when navigating from manage.html or index.html
function syncModelSelectorOnLoad() {
const modelInput = document.getElementById("chat-model");
const modelSelector = document.getElementById("modelSelector");
if (modelInput && modelSelector && modelInput.value) {
const modelName = modelInput.value;
const optionValue = 'chat/' + modelName;
// Find and select the option matching the model
for (let i = 0; i < modelSelector.options.length; i++) {
if (modelSelector.options[i].value === optionValue) {
modelSelector.selectedIndex = i;
break;
}
}
}
}
// Run sync after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', syncModelSelectorOnLoad);
} else {
syncModelSelectorOnLoad();
}
// Also sync after Alpine initializes (in case it runs after DOMContentLoaded)
if (window.Alpine) {
Alpine.nextTick(syncModelSelectorOnLoad);
} else {
document.addEventListener('alpine:init', () => {
Alpine.nextTick(syncModelSelectorOnLoad);
});
}
</script>
<style>

View File

@@ -210,7 +210,7 @@
<div class="flex flex-wrap gap-1">
{{ range .KnownUsecaseStrings }}
{{ if eq . "FLAG_CHAT" }}
<a href="chat/{{$backendCfg.Name}}" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)] hover:bg-[var(--color-primary)]/20 transition-colors" title="Chat">
<a href="chat/{{$backendCfg.Name}}" onclick="sessionStorage.setItem('localai_create_new_chat', 'true');" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)] hover:bg-[var(--color-primary)]/20 transition-colors" title="Chat">
<i class="fas fa-comment-alt text-[8px] mr-1"></i>Chat
</a>
{{ end }}