mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-12 09:58:15 -04:00
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:
committed by
GitHub
parent
0f5cc4c07b
commit
8ac7e8c299
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user