use-mcp/examples/chat-ui/src/components/ChatApp.tsx
Glen Maddern 4680b9d3e8 Make gradient background fill entire content height
- Change from h-dvh to min-h-screen on main container
- Remove overflow-clip to allow content to expand
- Update inner containers to use min-h-screen for proper height
- Background gradient now extends with content rather than fixed to viewport
- Allows for scrolling when content exceeds viewport height
2025-06-23 21:26:36 +10:00

154 lines
5.3 KiB
TypeScript

import React, { useState, useEffect } from "react";
import ChatSidebar from "./ChatSidebar.tsx";
import ChatNavbar from "./ChatNavbar.tsx";
import ConversationThread from "./ConversationThread.tsx";
import { storeName } from "../consts.ts";
import { type Conversation } from "../types";
import { useIndexedDB } from "../hooks/useIndexedDB";
import { type Model } from "../types/models";
import { getSelectedModel, setSelectedModel as saveSelectedModel } from "../utils/modelPreferences";
import { type IDBPDatabase } from "idb";
import { type Tool } from "use-mcp/react";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ChatAppProps {}
const ChatApp: React.FC<ChatAppProps> = () => {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [conversationId, setConversationId] = useState<number | undefined>(
undefined
);
const [sidebarVisible, setSidebarVisible] = useState<boolean>(true);
const [selectedModel, setSelectedModel] = useState<Model>(getSelectedModel());
const [apiKeyUpdateTrigger, setApiKeyUpdateTrigger] = useState<number>(0);
const [mcpTools, setMcpTools] = useState<Tool[]>([]);
const [animationDelay] = useState<number>(() => -Math.random() * 60);
const db = useIndexedDB();
const handleApiKeyUpdate = () => {
setApiKeyUpdateTrigger(prev => prev + 1);
};
const handleModelChange = (model: Model) => {
setSelectedModel(model);
saveSelectedModel(model);
};
// set up conversations on app load
useEffect(() => {
getConversations();
deleteUnusedConversations();
startNewConversation();
}, [db]);
// Initialize sidebar visibility based on screen size
useEffect(() => {
const isMobile = window.matchMedia("(max-width: 768px)").matches;
setSidebarVisible(!isMobile);
}, []);
const getConversations = async () => {
if (!db) return;
const conversations = (await db.getAll(storeName)) as Conversation[];
const inverseConversations = conversations.reverse();
setConversations(inverseConversations);
};
const deleteConversation = async (id: number, showPromptToUser = true) => {
try {
if (
showPromptToUser &&
!window.confirm("Are you sure you want to delete this conversation?")
) {
return;
}
await db?.delete(storeName, id);
setConversations((prev) => prev.filter((conv) => conv.id !== id));
setConversationId(conversations[0]?.id);
} catch (error) {
console.error("Failed to delete conversation:", error);
}
};
const editConversationTitle = async (id: number, newTitle: string) => {
const conversation = (await db!.get(storeName, id)) as Conversation;
conversation.title = newTitle;
await db!.put(storeName, conversation);
setConversations((prev) =>
prev.map((conv) => (conv.id === id ? { ...conv, title: newTitle } : conv))
);
};
const startNewConversation = async () => {
//create unique id for new conversation
setConversationId(Date.now() + Math.floor(Math.random() * 1000));
if (window.matchMedia("(max-width: 768px)").matches) {
setSidebarVisible(false);
}
};
// delete conversations with no messages
const deleteUnusedConversations = async () => {
if (!db) return;
const conversations = (await db.getAll(storeName)) as Conversation[];
const unusedConversations = conversations.filter(
(conversation) => conversation.messages.length === 0
);
for (const conversation of unusedConversations) {
deleteConversation(conversation.id as number, false);
}
};
return (
<div
className="flex min-h-screen w-screen animated-bg-container"
style={{ '--random-delay': `${animationDelay}s` } as React.CSSProperties}
>
<div className="flex flex-row flex-grow flex-1 min-h-screen relative">
{/* Sidebar and Navbar components hidden but kept in codebase */}
{false && (
<>
<ChatSidebar
sidebarVisible={sidebarVisible}
setSidebarVisible={setSidebarVisible}
conversations={conversations}
conversationId={conversationId}
setConversationId={setConversationId}
deleteConversation={deleteConversation}
editConversationTitle={editConversationTitle}
startNewConversation={startNewConversation}
selectedModel={selectedModel}
onModelChange={handleModelChange}
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
onMcpToolsUpdate={setMcpTools}
/>
<ChatNavbar
sidebarVisible={sidebarVisible}
setSidebarVisible={setSidebarVisible}
/>
</>
)}
<div className="flex flex-col flex-grow h-full w-full">
<ConversationThread
conversations={conversations}
conversationId={conversationId}
setConversationId={setConversationId}
setConversations={setConversations}
db={db as IDBPDatabase}
selectedModel={selectedModel}
onApiKeyUpdate={handleApiKeyUpdate}
onModelChange={handleModelChange}
apiKeyUpdateTrigger={apiKeyUpdateTrigger}
mcpTools={mcpTools}
onMcpToolsUpdate={setMcpTools}
/>
</div>
</div>
</div>
);
};
export default ChatApp;