fix(chat): move toolbar to Header and add New Session button

- Move ChatToolbar (session selector, refresh, thinking toggle) from
  the Chat page body into the Header component, so controls appear
  at the same level as the "Chat" title
- Add New Session button (+) to create a fresh conversation
- Add newSession action to chat store
- Header conditionally renders ChatToolbar only on /chat route
- Chat page fills full content area without duplicate toolbar
This commit is contained in:
Haze
2026-02-06 04:57:25 +08:00
Unverified
parent 3468d1bdf4
commit 94a6cecf2f
4 changed files with 43 additions and 15 deletions

View File

@@ -1,8 +1,10 @@
/** /**
* Header Component * Header Component
* Top navigation bar with page title * Top navigation bar with page title and page-specific controls.
* On the Chat page, shows session selector, refresh, thinking toggle, and new session.
*/ */
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { ChatToolbar } from '@/pages/Chat/ChatToolbar';
// Page titles mapping // Page titles mapping
const pageTitles: Record<string, string> = { const pageTitles: Record<string, string> = {
@@ -16,13 +18,15 @@ const pageTitles: Record<string, string> = {
export function Header() { export function Header() {
const location = useLocation(); const location = useLocation();
// Get current page title
const currentTitle = pageTitles[location.pathname] || 'ClawX'; const currentTitle = pageTitles[location.pathname] || 'ClawX';
const isChatPage = location.pathname === '/chat';
return ( return (
<header className="flex h-14 items-center border-b bg-background px-6"> <header className="flex h-14 items-center justify-between border-b bg-background px-6">
<h2 className="text-lg font-semibold">{currentTitle}</h2> <h2 className="text-lg font-semibold">{currentTitle}</h2>
{/* Chat-specific controls */}
{isChatPage && <ChatToolbar />}
</header> </header>
); );
} }

View File

@@ -1,8 +1,9 @@
/** /**
* Chat Toolbar * Chat Toolbar
* Session selector, refresh, and thinking toggle controls. * Session selector, new session, refresh, and thinking toggle.
* Rendered in the Header when on the Chat page.
*/ */
import { RefreshCw, Brain, ChevronDown } from 'lucide-react'; import { RefreshCw, Brain, ChevronDown, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useChatStore } from '@/stores/chat'; import { useChatStore } from '@/stores/chat';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -11,6 +12,7 @@ export function ChatToolbar() {
const sessions = useChatStore((s) => s.sessions); const sessions = useChatStore((s) => s.sessions);
const currentSessionKey = useChatStore((s) => s.currentSessionKey); const currentSessionKey = useChatStore((s) => s.currentSessionKey);
const switchSession = useChatStore((s) => s.switchSession); const switchSession = useChatStore((s) => s.switchSession);
const newSession = useChatStore((s) => s.newSession);
const refresh = useChatStore((s) => s.refresh); const refresh = useChatStore((s) => s.refresh);
const loading = useChatStore((s) => s.loading); const loading = useChatStore((s) => s.loading);
const showThinking = useChatStore((s) => s.showThinking); const showThinking = useChatStore((s) => s.showThinking);
@@ -51,6 +53,17 @@ export function ChatToolbar() {
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" /> <ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
</div> </div>
{/* New Session */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={newSession}
title="New session"
>
<Plus className="h-4 w-4" />
</Button>
{/* Refresh */} {/* Refresh */}
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -10,7 +10,6 @@ import { Card, CardContent } from '@/components/ui/card';
import { useChatStore } from '@/stores/chat'; import { useChatStore } from '@/stores/chat';
import { useGatewayStore } from '@/stores/gateway'; import { useGatewayStore } from '@/stores/gateway';
import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { ChatToolbar } from './ChatToolbar';
import { ChatMessage } from './ChatMessage'; import { ChatMessage } from './ChatMessage';
import { ChatInput } from './ChatInput'; import { ChatInput } from './ChatInput';
import { extractText } from './message-utils'; import { extractText } from './message-utils';
@@ -63,13 +62,7 @@ export function Chat() {
const streamText = streamingMessage ? extractText(streamingMessage) : ''; const streamText = streamingMessage ? extractText(streamingMessage) : '';
return ( return (
<div className="flex h-[calc(100vh-4rem)] flex-col"> <div className="flex flex-col -m-6" style={{ height: 'calc(100vh - 3.5rem)' }}>
{/* Toolbar: session selector, refresh, thinking toggle */}
<div className="flex items-center justify-between px-4 py-2 border-b">
<div /> {/* spacer */}
<ChatToolbar />
</div>
{/* Messages Area */} {/* Messages Area */}
<div className="flex-1 overflow-y-auto px-4 py-4"> <div className="flex-1 overflow-y-auto px-4 py-4">
<div className="max-w-4xl mx-auto space-y-4"> <div className="max-w-4xl mx-auto space-y-4">

View File

@@ -60,6 +60,7 @@ interface ChatState {
// Actions // Actions
loadSessions: () => Promise<void>; loadSessions: () => Promise<void>;
switchSession: (key: string) => void; switchSession: (key: string) => void;
newSession: () => void;
loadHistory: () => Promise<void>; loadHistory: () => Promise<void>;
sendMessage: (text: string) => Promise<void>; sendMessage: (text: string) => Promise<void>;
handleChatEvent: (event: Record<string, unknown>) => void; handleChatEvent: (event: Record<string, unknown>) => void;
@@ -129,6 +130,23 @@ export const useChatStore = create<ChatState>((set, get) => ({
get().loadHistory(); get().loadHistory();
}, },
// ── New session ──
newSession: () => {
// Generate a new unique session key and switch to it
const newKey = `session-${Date.now()}`;
set({
currentSessionKey: newKey,
messages: [],
streamingText: '',
streamingMessage: null,
activeRunId: null,
error: null,
});
// Reload sessions list to include the new one after first message
get().loadSessions();
},
// ── Load chat history ── // ── Load chat history ──
loadHistory: async () => { loadHistory: async () => {