From 3290e71978f6098c465cad06e85768073f2c7108 Mon Sep 17 00:00:00 2001 From: Andrew Barnes Date: Sun, 12 Jul 2026 19:23:00 -0400 Subject: [PATCH] fix(chat): make conversation layout responsive Signed-off-by: Andrew Barnes --- .../inertia/components/chat/ChatInterface.tsx | 66 +++-- admin/inertia/components/chat/ChatSidebar.tsx | 31 ++- admin/inertia/components/chat/index.tsx | 251 ++++++++++-------- 3 files changed, 209 insertions(+), 139 deletions(-) diff --git a/admin/inertia/components/chat/ChatInterface.tsx b/admin/inertia/components/chat/ChatInterface.tsx index 57f9d64..e2c021a 100644 --- a/admin/inertia/components/chat/ChatInterface.tsx +++ b/admin/inertia/components/chat/ChatInterface.tsx @@ -28,7 +28,7 @@ export default function ChatInterface({ chatSuggestions = [], chatSuggestionsEnabled = false, chatSuggestionsLoading = false, - rewriteModelAvailable = false + rewriteModelAvailable = false, }: ChatInterfaceProps) { const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props const { addNotification } = useNotifications() @@ -95,33 +95,39 @@ export default function ChatInterface({

Interact with your installed language models directly in the Command Center.

- {chatSuggestionsEnabled && chatSuggestions && chatSuggestions.length > 0 && !chatSuggestionsLoading && ( -
-

Suggestions:

-
- {chatSuggestions.map((suggestion, index) => ( - - ))} + {chatSuggestionsEnabled && + chatSuggestions && + chatSuggestions.length > 0 && + !chatSuggestionsLoading && ( +
+

Suggestions:

+
+ {chatSuggestions.map((suggestion, index) => ( + + ))} +
-
- )} + )} {/* Display bouncing dots while loading suggestions */} - {chatSuggestionsEnabled && chatSuggestionsLoading && } + {chatSuggestionsEnabled && chatSuggestionsLoading && ( + + )} {!chatSuggestionsEnabled && (
- Need some inspiration? Enable chat suggestions in settings to get started with example prompts. + Need some inspiration? Enable chat suggestions in settings to get started with + example prompts.
)}
@@ -144,7 +150,7 @@ export default function ChatInterface({ {isLoading && (
-
+
@@ -203,8 +209,8 @@ export default function ChatInterface({ title={`Download ${DEFAULT_QUERY_REWRITE_MODEL}?`} confirmText="Download" cancelText="Cancel" - confirmIcon='IconDownload' - confirmVariant='primary' + confirmIcon="IconDownload" + confirmVariant="primary" confirmLoading={isDownloading} onConfirm={handleDownloadModel} onCancel={() => setDownloadDialogOpen(false)} @@ -212,8 +218,10 @@ export default function ChatInterface({ >

This will dispatch a background download job for{' '} - {DEFAULT_QUERY_REWRITE_MODEL} and may take some time to complete. The model - will be used to rewrite queries for improved RAG retrieval performance. Note that download is only supported when using Ollama. If using an OpenAI API interface, please download the model with that software. + {DEFAULT_QUERY_REWRITE_MODEL} and may + take some time to complete. The model will be used to rewrite queries for improved RAG + retrieval performance. Note that download is only supported when using Ollama. If using + an OpenAI API interface, please download the model with that software.

diff --git a/admin/inertia/components/chat/ChatSidebar.tsx b/admin/inertia/components/chat/ChatSidebar.tsx index 07e59fa..93cbd3e 100644 --- a/admin/inertia/components/chat/ChatSidebar.tsx +++ b/admin/inertia/components/chat/ChatSidebar.tsx @@ -13,6 +13,8 @@ interface ChatSidebarProps { onNewChat: () => void onClearHistory: () => void isInModal?: boolean + isMobileOpen?: boolean + onMobileClose?: () => void } export default function ChatSidebar({ @@ -22,6 +24,8 @@ export default function ChatSidebar({ onNewChat, onClearHistory, isInModal = false, + isMobileOpen = false, + onMobileClose, }: ChatSidebarProps) { const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props const [isKnowledgeBaseModalOpen, setIsKnowledgeBaseModalOpen] = useState( @@ -39,9 +43,25 @@ export default function ChatSidebar({ } return ( -
+
+ ) } diff --git a/admin/inertia/components/chat/index.tsx b/admin/inertia/components/chat/index.tsx index 9be60d4..bc7c40e 100644 --- a/admin/inertia/components/chat/index.tsx +++ b/admin/inertia/components/chat/index.tsx @@ -9,7 +9,7 @@ import { formatBytes } from '~/lib/util' import { useModals } from '~/context/ModalContext' import { ChatMessage } from '../../../types/chat' import classNames from '~/lib/classNames' -import { IconX } from '@tabler/icons-react' +import { IconMenu2, IconX } from '@tabler/icons-react' import { DEFAULT_QUERY_REWRITE_MODEL } from '../../../constants/ollama' import { useSystemSetting } from '~/hooks/useSystemSetting' @@ -36,8 +36,18 @@ export default function Chat({ const [pendingModelSwitch, setPendingModelSwitch] = useState(null) const pageLoadNormalizedRef = useRef(false) const [isStreamingResponse, setIsStreamingResponse] = useState(false) + const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false) const streamAbortRef = useRef(null) + useEffect(() => { + if (!isMobileSidebarOpen) return + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsMobileSidebarOpen(false) + } + window.addEventListener('keydown', closeOnEscape) + return () => window.removeEventListener('keydown', closeOnEscape) + }, [isMobileSidebarOpen]) + // Fetch all sessions const { data: sessions = [] } = useQuery({ queryKey: ['chatSessions'], @@ -84,7 +94,7 @@ export default function Chat({ }) const rewriteModelAvailable = useMemo(() => { - return installedModels.some(model => model.name === DEFAULT_QUERY_REWRITE_MODEL) + return installedModels.some((model) => model.name === DEFAULT_QUERY_REWRITE_MODEL) }, [installedModels]) const deleteAllSessionsMutation = useMutation({ @@ -323,7 +333,12 @@ export default function Chat({ try { await api.streamChatMessage( - { model: selectedModel || 'llama3.2', messages: chatMessages, stream: true, sessionId: sessionId ? Number(sessionId) : undefined }, + { + model: selectedModel || 'llama3.2', + messages: chatMessages, + stream: true, + sessionId: sessionId ? Number(sessionId) : undefined, + }, (chunkContent, chunkThinking, done) => { if (chunkThinking.length > 0 && thinkingStartTime === null) { thinkingStartTime = Date.now() @@ -348,20 +363,23 @@ export default function Chat({ if (isThinkingPhase && chunkContent.length > 0) { isThinkingPhase = false if (thinkingStartTime !== null) { - thinkingDuration = Math.max(1, Math.round((Date.now() - thinkingStartTime) / 1000)) + thinkingDuration = Math.max( + 1, + Math.round((Date.now() - thinkingStartTime) / 1000) + ) } } setMessages((prev) => prev.map((m) => m.id === assistantMsgId ? { - ...m, - content: m.content + chunkContent, - thinking: (m.thinking ?? '') + chunkThinking, - isStreaming: !done, - isThinking: isThinkingPhase, - thinkingDuration: thinkingDuration ?? undefined, - } + ...m, + content: m.content + chunkContent, + thinking: (m.thinking ?? '') + chunkThinking, + isStreaming: !done, + isThinking: isThinkingPhase, + thinkingDuration: thinkingDuration ?? undefined, + } : m ) ) @@ -376,9 +394,7 @@ export default function Chat({ setMessages((prev) => { const hasAssistantMsg = prev.some((m) => m.id === assistantMsgId) if (hasAssistantMsg) { - return prev.map((m) => - m.id === assistantMsgId ? { ...m, isStreaming: false } : m - ) + return prev.map((m) => (m.id === assistantMsgId ? { ...m, isStreaming: false } : m)) } return [ ...prev, @@ -399,9 +415,7 @@ export default function Chat({ if (fullContent && sessionId) { // Ensure the streaming cursor is removed setMessages((prev) => - prev.map((m) => - m.id === assistantMsgId ? { ...m, isStreaming: false } : m - ) + prev.map((m) => (m.id === assistantMsgId ? { ...m, isStreaming: false } : m)) ) // Refresh sessions to pick up backend-persisted messages and title @@ -422,103 +436,128 @@ export default function Chat({ return ( <> - {pendingModelSwitch && ( - -

- Switching to {pendingModelSwitch} will start a new chat. Your current - conversation stays available in the sidebar. -

-
- )} -
+

+ Switching to {pendingModelSwitch} will start a new chat. Your current + conversation stays available in the sidebar. +

+ )} - > - -
- -
-

- {activeSession?.title || 'New Chat'} -

-
- {remoteOllamaUrlSetting?.value && ( - + setIsMobileSidebarOpen(false)} + /> + {isMobileSidebarOpen && ( + )}
- {isInModal && ( - - )}
+
-
- ) }