mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-04 14:09:34 -04:00
fix(ui): improve view on mobile (#8598)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
7dd9a155a3
commit
a2228f1418
@@ -17,6 +17,7 @@ body {
|
||||
.app-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
background-color: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
@@ -24,6 +25,7 @@ body {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-bg-primary);
|
||||
@@ -37,6 +39,19 @@ body {
|
||||
background-color: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
/* Chat page: fix viewport height so messages scroll and input stays fixed at bottom */
|
||||
.app-layout.chat-layout {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main-content.chat-layout {
|
||||
min-height: 0;
|
||||
}
|
||||
.main-content-inner.chat-layout {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Tablet and mobile */
|
||||
@media (max-width: 1023px) {
|
||||
.main-content {
|
||||
@@ -44,6 +59,13 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Safe area for notched devices (e.g. iOS) - use on fixed bottom bars / modals */
|
||||
@supports (padding: env(safe-area-inset-bottom)) {
|
||||
.pb-safe {
|
||||
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
.chat-container { height: 90vh; display: flex; flex-direction: column; }
|
||||
.chat-messages { overflow-y: auto; flex-grow: 1; }
|
||||
.htmx-indicator{
|
||||
|
||||
@@ -587,18 +587,20 @@ SOFTWARE.
|
||||
<script defer src="static/chat.js"></script>
|
||||
{{ $allGalleryConfigs:=.GalleryConfig }}
|
||||
{{ $model:=.Model}}
|
||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]" x-data="{ settingsPanelOpen: true, showClearAlert: false }">
|
||||
<div class="app-layout">
|
||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]" x-data="{ settingsPanelOpen: true, showClearAlert: false, isMobile: false }" x-init="isMobile = window.innerWidth < 1024; if (isMobile) settingsPanelOpen = false; window.addEventListener('resize', () => { isMobile = window.innerWidth < 1024 })">
|
||||
<div class="app-layout chat-layout">
|
||||
{{template "views/partials/navbar" .}}
|
||||
|
||||
<main class="main-content">
|
||||
<div class="main-content-inner h-full flex flex-col">
|
||||
<main class="main-content chat-layout">
|
||||
<div class="main-content-inner chat-layout h-full flex flex-col">
|
||||
|
||||
<!-- Main container with settings panel -->
|
||||
<div class="flex flex-1 min-h-0 relative">
|
||||
<!-- Chat Settings Panel (right side) -->
|
||||
<!-- Backdrop for mobile when settings panel is open (click to close) -->
|
||||
<div x-show="settingsPanelOpen && isMobile" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="settingsPanelOpen = false" class="fixed inset-0 bg-black/50 z-20" aria-hidden="true"></div>
|
||||
<!-- Chat Settings Panel (right side): overlay on mobile (w-full), sidebar on desktop (md:w-56) -->
|
||||
<div
|
||||
class="chat-settings-panel bg-[var(--color-bg-secondary)] fixed top-0 right-0 bottom-0 w-56 transform transition-transform duration-300 ease-in-out z-30 border-l border-[var(--color-border-subtle)] overflow-y-auto"
|
||||
class="chat-settings-panel bg-[var(--color-bg-secondary)] fixed top-0 right-0 bottom-0 w-full md:w-56 transform transition-transform duration-300 ease-in-out z-30 border-l border-[var(--color-border-subtle)] overflow-y-auto"
|
||||
:class="settingsPanelOpen ? 'translate-x-0' : 'translate-x-full'">
|
||||
|
||||
<div class="p-3 flex justify-between items-center border-b border-[var(--color-border-subtle)]">
|
||||
@@ -1103,59 +1105,57 @@ SOFTWARE.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main chat container (shifts with settings panel) -->
|
||||
<!-- Main chat container (shifts with settings panel on desktop only; on mobile panel overlays) -->
|
||||
<div
|
||||
class="flex-1 flex flex-col min-h-0 transition-all duration-300 ease-in-out"
|
||||
:class="settingsPanelOpen ? 'mr-56' : 'mr-0'">
|
||||
:class="settingsPanelOpen ? 'md:mr-56' : 'mr-0'">
|
||||
|
||||
<!-- Chat header with toggle button -->
|
||||
<div class="flex-shrink-0 border-b border-[var(--color-bg-secondary)] p-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)]"></i>
|
||||
<!-- 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">
|
||||
<div class="flex-shrink-0 border-b border-[var(--color-bg-secondary)] p-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex items-center min-w-0 flex-1">
|
||||
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)] flex-shrink-0"></i>
|
||||
<!-- 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 flex-shrink-0">
|
||||
</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 flex-shrink-0">
|
||||
</template>
|
||||
<h1 class="text-lg font-semibold text-[var(--color-text-primary)] truncate min-w-0">
|
||||
Chat
|
||||
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
|
||||
<span x-text="' with ' + $store.chat.activeChat().model"></span>
|
||||
</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 x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
|
||||
{{ if .Model }}<span> with {{.Model}}</span>{{ end }}
|
||||
</template>
|
||||
<h1 class="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||
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;">
|
||||
<i class="fas fa-spinner fa-spin text-sm"></i>
|
||||
</div>
|
||||
</h1>
|
||||
<!-- Loading indicator next to model name -->
|
||||
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)] flex-shrink-0" style="display: none;">
|
||||
<i class="fas fa-spinner fa-spin text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button
|
||||
@click="if (confirm('Clear all messages from this conversation? This action cannot be undone.')) { $store.chat.clear(); showClearAlert = true; setTimeout(() => showClearAlert = false, 3000); }"
|
||||
id="clear"
|
||||
title="Clear current chat history"
|
||||
class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-2 rounded hover:bg-[var(--color-bg-secondary)]"
|
||||
class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center rounded hover:bg-[var(--color-bg-secondary)]"
|
||||
x-show="$store.chat.activeChat() && ($store.chat.activeChat()?.history?.length || 0) > 0">
|
||||
<i class="fa-solid fa-broom"></i>
|
||||
</button>
|
||||
<!-- Settings panel toggle button -->
|
||||
<button
|
||||
@click="settingsPanelOpen = !settingsPanelOpen"
|
||||
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-secondary)]/80 p-2 rounded transition-colors"
|
||||
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-secondary)]/80 p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center rounded transition-colors"
|
||||
title="Toggle chat settings">
|
||||
<i class="fa-solid" :class="settingsPanelOpen ? 'fa-chevron-right' : 'fa-cog'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Clear Chat Alert -->
|
||||
<!-- Clear Chat Alert (bottom on mobile to avoid covering header) -->
|
||||
<div x-show="showClearAlert"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-2"
|
||||
@@ -1163,7 +1163,7 @@ SOFTWARE.
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed top-20 right-4 z-50 max-w-sm pointer-events-none">
|
||||
class="fixed top-20 right-4 max-md:top-auto max-md:bottom-4 max-md:left-4 max-md:right-4 z-50 max-w-sm pointer-events-none">
|
||||
<div class="bg-[var(--color-primary)]/20 border border-[var(--color-primary-border)]/40 rounded-lg p-3 shadow-lg backdrop-blur-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-check-circle text-[var(--color-primary)]"></i>
|
||||
@@ -1374,7 +1374,7 @@ SOFTWARE.
|
||||
</div>
|
||||
|
||||
<!-- Chat Input -->
|
||||
<div class="flex-shrink-0 p-4 border-t border-[var(--color-bg-secondary)] bg-[var(--color-bg-primary)]" x-data="{ inputValue: '', shiftPressed: false, attachedFiles: [] }">
|
||||
<div class="flex-shrink-0 p-4 pb-safe border-t border-[var(--color-bg-secondary)] bg-[var(--color-bg-primary)]" x-data="{ inputValue: '', shiftPressed: false, attachedFiles: [] }">
|
||||
<form id="prompt" action="chat/{{.Model}}" method="get" @submit.prevent="submitPrompt" class="max-w-3xl mx-auto">
|
||||
<!-- Attachment Tags - Show above input when files are attached -->
|
||||
<div x-show="attachedFiles.length > 0" class="mb-3 flex flex-wrap gap-2 items-center">
|
||||
@@ -1394,38 +1394,38 @@ SOFTWARE.
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Token Usage and Context Window - Compact above input -->
|
||||
<div class="mb-3 flex items-center justify-between gap-4 text-xs">
|
||||
<!-- Token Usage -->
|
||||
<div class="flex items-center gap-3 text-[var(--color-text-secondary)]">
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Token Usage and Context Window - responsive: two rows on mobile -->
|
||||
<div class="mb-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3 text-xs">
|
||||
<!-- Token Usage (wraps on mobile) -->
|
||||
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-[var(--color-text-secondary)]">
|
||||
<div class="flex items-center gap-1 max-md:hidden">
|
||||
<i class="fas fa-chart-line text-[var(--color-primary)]"></i>
|
||||
<span>Prompt:</span>
|
||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.promptTokens || 0)"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center gap-1 max-md:hidden">
|
||||
<span>Completion:</span>
|
||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.completionTokens || 0)"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-3">
|
||||
<div class="flex items-center gap-1 md:border-l border-[var(--color-bg-secondary)] pl-0 md:pl-3">
|
||||
<span class="text-[var(--color-primary)] font-semibold">Total:</span>
|
||||
<span class="text-[var(--color-text-primary)] font-bold" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
|
||||
</div>
|
||||
<!-- Tokens per second display -->
|
||||
<div id="tokens-per-second-container" class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-3">
|
||||
<div id="tokens-per-second-container" class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-2 md:pl-3">
|
||||
<i class="fas fa-tachometer-alt text-[var(--color-primary)]"></i>
|
||||
<span id="tokens-per-second" class="text-[var(--color-text-primary)] font-medium">-</span>
|
||||
<span id="max-tokens-per-second-badge" class="ml-2 px-1.5 py-0.5 text-[10px] bg-[var(--color-primary)]/20 text-[var(--color-primary)] rounded border border-[var(--color-primary-border)]/30 hidden"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context Window -->
|
||||
<!-- Context Window (second row on mobile) -->
|
||||
<template x-if="$store.chat.activeChat()?.contextSize && $store.chat.activeChat().contextSize > 0">
|
||||
<div class="flex items-center gap-2 text-[var(--color-text-secondary)]">
|
||||
<div class="flex items-center gap-2 text-[var(--color-text-secondary)] flex-shrink-0">
|
||||
<i class="fas fa-database text-[var(--color-primary)]"></i>
|
||||
<span>
|
||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
|
||||
/
|
||||
/
|
||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.contextSize || 0)"></span>
|
||||
</span>
|
||||
<div class="w-16 bg-[var(--color-bg-primary)] rounded-full h-1.5 overflow-hidden border border-[var(--color-bg-secondary)]">
|
||||
@@ -1446,38 +1446,36 @@ SOFTWARE.
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Attachment buttons row (mobile only) - avoids overlap with input on narrow screens -->
|
||||
<div class="flex flex-wrap gap-2 mb-2 md:hidden">
|
||||
<button type="button" onclick="document.getElementById('input_image').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Attach images" aria-label="Attach images">
|
||||
<i class="fa-solid fa-image text-lg"></i>
|
||||
</button>
|
||||
<button type="button" onclick="document.getElementById('input_audio').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Attach an audio file" aria-label="Attach audio">
|
||||
<i class="fa-solid fa-microphone text-lg"></i>
|
||||
</button>
|
||||
<button type="button" onclick="document.getElementById('input_file').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Upload text, markdown or PDF file" aria-label="Attach file">
|
||||
<i class="fa-solid fa-file text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full">
|
||||
<textarea
|
||||
id="input"
|
||||
name="input"
|
||||
x-model="inputValue"
|
||||
class="input w-full p-3 pr-16 resize-none border-0"
|
||||
class="input w-full p-3 pr-12 md:pr-28 resize-none border-0 bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)] placeholder-[var(--color-text-secondary)] focus:outline-none rounded-xl transition-colors duration-200"
|
||||
placeholder="Send a message..."
|
||||
class="p-3 pr-16 w-full bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)] placeholder-[var(--color-text-secondary)] focus:outline-none resize-none border-0 rounded-xl transition-colors duration-200"
|
||||
required
|
||||
@keydown.shift="shiftPressed = true"
|
||||
@keyup.shift="shiftPressed = false"
|
||||
@keydown.enter.prevent="if (!shiftPressed) { submitPrompt($event); }"
|
||||
rows="2"
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
onclick="document.getElementById('input_image').click()"
|
||||
class="fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
|
||||
title="Attach images"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="document.getElementById('input_audio').click()"
|
||||
class="fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
|
||||
title="Attach an audio file"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="document.getElementById('input_file').click()"
|
||||
class="fa-solid fa-file text-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
|
||||
title="Upload text, markdown or PDF file"
|
||||
></button>
|
||||
<!-- Attachment buttons (desktop only - inside input) -->
|
||||
<button type="button" onclick="document.getElementById('input_image').click()" class="hidden md:flex fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Attach images" aria-label="Attach images"></button>
|
||||
<button type="button" onclick="document.getElementById('input_audio').click()" class="hidden md:flex fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Attach an audio file" aria-label="Attach audio"></button>
|
||||
<button type="button" onclick="document.getElementById('input_file').click()" class="hidden md:flex fa-solid fa-file text-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Upload text, markdown or PDF file" aria-label="Attach file"></button>
|
||||
|
||||
<!-- Send button and stop button in the same position -->
|
||||
<div class="absolute right-3 top-3 flex items-center">
|
||||
@@ -1486,7 +1484,7 @@ SOFTWARE.
|
||||
id="stop-button"
|
||||
type="button"
|
||||
onclick="stopRequest()"
|
||||
class="text-lg p-2 text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors duration-200"
|
||||
class="text-lg p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors duration-200"
|
||||
style="display: none;"
|
||||
title="Stop request"
|
||||
>
|
||||
@@ -1497,7 +1495,7 @@ SOFTWARE.
|
||||
<button
|
||||
id="send-button"
|
||||
type="submit"
|
||||
class="text-lg p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors duration-200"
|
||||
class="text-lg p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors duration-200"
|
||||
title="Send message (Enter)"
|
||||
>
|
||||
<i class="fa-solid fa-paper-plane"></i>
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||
<div class="app-layout">
|
||||
{{template "views/partials/navbar" .}}
|
||||
|
||||
<main class="main-content">
|
||||
<div class="main-content-inner">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user