Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
98411e6
feat: install Dexie.js and create chat database structure
peppescg Dec 23, 2025
5863c5d
feat: integrate Dexie chat history with ChatContext
peppescg Dec 23, 2025
72f9763
feat: auto-load last conversation on chat mount
peppescg Dec 23, 2025
1b65f74
fix: create new conversation when clearing messages
peppescg Dec 23, 2025
2884bb6
fix: add 'use client' directive to use-chat-history hook
peppescg Dec 23, 2025
a993513
refactor: rename database to ToolHiveCloudUIChatDB
peppescg Dec 23, 2025
5b735a1
feat: add loading spinner while restoring chat history
peppescg Dec 23, 2025
3eb8395
revert: remove loading spinner for chat history
peppescg Dec 23, 2025
8b8fa5b
feat: add conversation history popover with search
peppescg Dec 23, 2025
2da5b32
fix: resolve hydration error and UI issues in conversation list
peppescg Dec 23, 2025
d2f1415
fix: add spacing between conversation items
peppescg Dec 23, 2025
dee0549
style: change Clear Chat button to secondary variant
peppescg Dec 23, 2025
81ea4a5
refactor: move the sidebar to the right side
peppescg Dec 23, 2025
efeb82c
fix: ensure delete button is always visible with long titles
peppescg Dec 23, 2025
9e3b433
fix: preserve conversation order when loading
peppescg Dec 23, 2025
ef9e903
refactor: use shadcn Button component in conversation list
peppescg Dec 23, 2025
435d9c5
style: use size-* instead of h-* w-* for icons
peppescg Dec 23, 2025
1c53b75
chore: remove redundant comments
peppescg Dec 23, 2025
85801c2
refactor: use shadcn Button for popover trigger
peppescg Dec 23, 2025
39c2f3c
refactor(chat): use shadcn Button component consistently
peppescg Dec 23, 2025
4922eaf
refactor: extract chat transport and persistence into dedicated hooks
peppescg Dec 23, 2025
ee3da5b
refactor: optimize chat history and eliminate props drilling
peppescg Dec 23, 2025
dda9d65
perf: use bulkPut instead of delete+bulkAdd for messages
peppescg Dec 23, 2025
2c5414e
refactor: simplify chat persistence - remove debounce
peppescg Dec 23, 2025
147225a
refactor: make clearMessages synchronous with fire-and-forget
peppescg Dec 23, 2025
6a49533
refactor: rename Clear Chat to New Conversation
peppescg Dec 23, 2025
4a330e4
feat: add 'Clear all messages' option to conversation dropdown
peppescg Dec 23, 2025
e3601ab
refactor: use toast for errors and disable New Conversation when empty
peppescg Dec 23, 2025
9ee2d35
fix: improve conversation list item layout
peppescg Dec 23, 2025
211ba58
fix: conversation list overflow and always show delete icon
peppescg Dec 23, 2025
ddd6825
leftover
peppescg Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
Expand All @@ -53,6 +54,7 @@
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"date-fns": "^4.1.0",
"dexie": "^4.2.1",
"jose": "^6.1.2",
"json-schema-faker": "^0.5.6",
"lucide-react": "^0.562.0",
Expand Down
47 changes: 47 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 76 additions & 26 deletions src/components/chat/chat-header.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,100 @@
"use client";

import { Trash2 } from "lucide-react";
import { ChevronDown, Plus } from "lucide-react";
import { useState } from "react";
import type { StoredConversation } from "@/features/assistant/db";
import { useConfirm } from "@/hooks/use-confirm";
import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { ConversationList } from "./conversation-list";

interface ChatHeaderProps {
conversations: StoredConversation[];
currentConversationId: string | null;
onSelectConversation: (id: string) => void;
onDeleteConversation: (id: string) => void;
onNewConversation: () => void;
onClearAll: () => Promise<void>;
hasMessages: boolean;
onClearMessages?: () => void;
}

export function ChatHeader({ hasMessages, onClearMessages }: ChatHeaderProps) {
export function ChatHeader({
conversations,
currentConversationId,
onSelectConversation,
onDeleteConversation,
onNewConversation,
onClearAll,
hasMessages,
}: ChatHeaderProps) {
const { confirm, ConfirmDialog } = useConfirm();
const [isOpen, setIsOpen] = useState(false);

const handleClearMessages = async () => {
if (!onClearMessages) return;
const handleNewConversation = () => {
onNewConversation();
};

const handleDeleteConversation = async (id: string) => {
const confirmed = await confirm({
title: "Clear all messages?",
description: "Are you sure you want to delete all messages?",
confirmText: "Clear",
title: "Delete conversation?",
description: "This will permanently delete this conversation.",
confirmText: "Delete",
cancelText: "Cancel",
});
if (confirmed) {
onClearMessages();
onDeleteConversation(id);
}
};

const handleClearAll = async () => {
const confirmed = await confirm({
title: "Clear all conversations?",
description:
"This will permanently delete all conversations and messages.",
confirmText: "Clear All",
cancelText: "Cancel",
});
if (confirmed) {
await onClearAll();
setIsOpen(false);
}
};

return (
<>
<div className="flex items-center justify-between border-b p-4">
<div>
<h1 className="text-2xl font-bold">Assistant</h1>
<p className="text-muted-foreground text-sm">
Chat with AI using MCP servers
</p>
</div>
{hasMessages && onClearMessages && (
<Button
onClick={handleClearMessages}
variant="outline"
size="sm"
className="cursor-pointer"
>
<Trash2 className="mr-2 h-4 w-4" />
Clear Chat
</Button>
)}
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" className="h-auto gap-2 p-1">
<span className="text-2xl font-bold">Assistant</span>
<ChevronDown className="text-muted-foreground size-4" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="p-0">
<ConversationList
conversations={conversations}
currentConversationId={currentConversationId}
onSelectConversation={onSelectConversation}
onDeleteConversation={handleDeleteConversation}
onClearAll={handleClearAll}
onClose={() => setIsOpen(false)}
/>
</PopoverContent>
</Popover>

<p className="text-muted-foreground flex-1 pl-2 text-sm">
Chat with AI using MCP servers
</p>

<Button
onClick={handleNewConversation}
variant="secondary"
size="sm"
disabled={!hasMessages}
>
<Plus className="mr-2 size-4" />
New Conversation
</Button>
</div>
{ConfirmDialog}
</>
Expand Down
86 changes: 55 additions & 31 deletions src/components/chat/chat-interface.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
"use client";

import type { ChatStatus, FileUIPart, UIMessage } from "ai";
import { ChevronDown } from "lucide-react";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { useChatContext } from "@/features/assistant/contexts/chat-context";
import { useAutoScroll } from "@/hooks/use-auto-scroll";
import { ChatEmptyState } from "./chat-empty-state";
import { ChatHeader } from "./chat-header";
import { ChatInputPrompt } from "./chat-input";
import { ChatMessages } from "./chat-messages";
import { ErrorAlert } from "./error-alert";

interface ChatInterfaceProps {
messages: UIMessage[];
status: ChatStatus;
error?: Error;
cancelRequest: () => void;
onClearMessages?: () => void;
sendMessage: (args: { text: string; files?: FileUIPart[] }) => Promise<void>;
selectedModel: string;
onModelChange: (model: string) => void;
}
export function ChatInterface() {
const {
messages,
status,
error,
stop,
clearError,
sendMessage,
selectedModel,
setSelectedModel,
clearMessages,
conversations,
currentConversationId,
loadConversation,
deleteConversation,
clearAllConversations,
} = useChatContext();

export function ChatInterface({
messages,
status,
error,
cancelRequest,
onClearMessages,
sendMessage,
selectedModel,
onModelChange,
}: ChatInterfaceProps) {
const {
messagesEndRef,
messagesContainerRef,
Expand All @@ -41,10 +39,38 @@ export function ChatInterface({

const isLoading = status === "streaming" || status === "submitted";
const hasMessages = messages.length > 0;
const previousErrorRef = useRef<string | null>(null);

useEffect(() => {
const errorMessage = error?.message ?? null;
if (errorMessage && errorMessage !== previousErrorRef.current) {
toast.error("Something went wrong", {
description: errorMessage,
});
}
previousErrorRef.current = errorMessage;
}, [error]);

const handleCancelRequest = async () => {
await stop();
clearError();
};

const handleNewConversation = () => {
clearMessages();
};

return (
<div className="flex h-full flex-col px-8 pb-4">
<ChatHeader hasMessages={hasMessages} onClearMessages={onClearMessages} />
<ChatHeader
conversations={conversations}
currentConversationId={currentConversationId}
onSelectConversation={loadConversation}
onDeleteConversation={deleteConversation}
onNewConversation={handleNewConversation}
onClearAll={clearAllConversations}
hasMessages={hasMessages}
/>

{hasMessages && <Separator />}

Expand All @@ -61,35 +87,33 @@ export function ChatInterface({
<ChatEmptyState
status={status}
onSendMessage={sendMessage}
onStopGeneration={cancelRequest}
onStopGeneration={handleCancelRequest}
selectedModel={selectedModel}
onModelChange={onModelChange}
onModelChange={setSelectedModel}
/>
)}

{showScrollToBottom && (
<Button
size="sm"
variant="secondary"
className="animate-in fade-in-0 slide-in-from-bottom-2 absolute bottom-4 left-1/2 z-50 h-10 w-10 -translate-x-1/2 cursor-pointer rounded-full p-0 duration-200"
className="animate-in fade-in-0 slide-in-from-bottom-2 absolute bottom-4 left-1/2 z-50 size-10 -translate-x-1/2 cursor-pointer rounded-full p-0 duration-200"
onClick={scrollToBottom}
>
<ChevronDown className="h-4 w-4" />
<ChevronDown className="size-4" />
</Button>
)}
</div>

<ErrorAlert error={error?.message ?? null} />

{hasMessages && (
<div className="w-full">
<ChatInputPrompt
onStopGeneration={cancelRequest}
onStopGeneration={handleCancelRequest}
hasProviderAndModel={true}
status={status}
onSendMessage={sendMessage}
selectedModel={selectedModel}
onModelChange={onModelChange}
onModelChange={setSelectedModel}
/>
</div>
)}
Expand Down
Loading
Loading