replace ChatButton with ChatInputBar

This commit is contained in:
tusharmagar 2026-01-16 10:19:06 +05:30 committed by Ramnique Singh
parent 35c99cb999
commit c60d6d11ff
3 changed files with 155 additions and 90 deletions

View file

@ -8,7 +8,7 @@ import z from 'zod';
import { Button } from './components/ui/button'; import { Button } from './components/ui/button';
import { CheckIcon, LoaderIcon } from 'lucide-react'; import { CheckIcon, LoaderIcon } from 'lucide-react';
import { MarkdownEditor } from './components/markdown-editor'; import { MarkdownEditor } from './components/markdown-editor';
import { ChatButton } from './components/chat-button'; import { ChatInputBar } from './components/chat-button';
import { ChatSidebar } from './components/chat-sidebar'; import { ChatSidebar } from './components/chat-sidebar';
import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view'; import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view';
import { useDebounce } from './hooks/use-debounce'; import { useDebounce } from './hooks/use-debounce';
@ -592,6 +592,21 @@ function App() {
} }
} }
const handleNewChat = useCallback(() => {
setConversation([])
setCurrentAssistantMessage('')
setCurrentReasoning('')
setRunId(null)
setMessage('')
setModelUsage(null)
}, [])
const handleChatInputSubmit = (text: string) => {
setIsChatSidebarOpen(true)
// Submit immediately - the sidebar will open and show the message
handlePromptSubmit({ text })
}
const toggleExpand = (path: string, kind: 'file' | 'dir') => { const toggleExpand = (path: string, kind: 'file' | 'dir') => {
if (kind === 'file') { if (kind === 'file') {
setSelectedPath(path) setSelectedPath(path)
@ -1126,6 +1141,7 @@ function App() {
<ChatSidebar <ChatSidebar
defaultWidth={400} defaultWidth={400}
onClose={() => setIsChatSidebarOpen(false)} onClose={() => setIsChatSidebarOpen(false)}
onNewChat={handleNewChat}
conversation={conversation} conversation={conversation}
currentAssistantMessage={currentAssistantMessage} currentAssistantMessage={currentAssistantMessage}
currentReasoning={currentReasoning} currentReasoning={currentReasoning}
@ -1140,9 +1156,12 @@ function App() {
)} )}
</SidebarProvider> </SidebarProvider>
{/* Floating chat button - shown when viewing files/graph and chat sidebar is closed */} {/* Floating chat input - shown when viewing files/graph and chat sidebar is closed */}
{(selectedPath || isGraphOpen) && !isChatSidebarOpen && ( {(selectedPath || isGraphOpen) && !isChatSidebarOpen && (
<ChatButton onClick={() => setIsChatSidebarOpen(true)} /> <ChatInputBar
onSubmit={handleChatInputSubmit}
onOpen={() => setIsChatSidebarOpen(true)}
/>
)} )}
</div> </div>
</SidebarSectionProvider> </SidebarSectionProvider>

View file

@ -1,18 +1,61 @@
import { MessageSquare } from 'lucide-react' import { useState } from 'react'
import { ArrowUp } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface ChatButtonProps { interface ChatInputBarProps {
onClick: () => void onSubmit: (message: string) => void
onOpen: () => void
} }
export function ChatButton({ onClick }: ChatButtonProps) { export function ChatInputBar({ onSubmit, onOpen }: ChatInputBarProps) {
const [message, setMessage] = useState('')
const handleSubmit = () => {
const trimmed = message.trim()
if (trimmed) {
onSubmit(trimmed)
setMessage('')
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}
const handleFocus = () => {
onOpen()
}
return ( return (
<Button <div className="fixed bottom-6 right-6 z-50">
onClick={onClick} <div className="flex items-center gap-2 bg-background border border-border rounded-full shadow-xl px-4 py-2.5 w-80">
size="icon" <input
className="fixed bottom-6 right-6 h-12 w-12 rounded-full shadow-lg hover:shadow-xl transition-shadow z-50" type="text"
> value={message}
<MessageSquare className="h-5 w-5" /> onChange={(e) => setMessage(e.target.value)}
</Button> onKeyDown={handleKeyDown}
onFocus={handleFocus}
placeholder="Ask anything..."
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<Button
size="icon"
onClick={handleSubmit}
disabled={!message.trim()}
className={cn(
"h-7 w-7 rounded-full shrink-0 transition-all",
message.trim()
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "bg-muted text-muted-foreground"
)}
>
<ArrowUp className="h-4 w-4" />
</Button>
</div>
</div>
) )
} }

View file

@ -1,8 +1,13 @@
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from 'react'
import { X } from 'lucide-react' import { ArrowUp, PanelRightClose, Plus } from 'lucide-react'
import type { ChatStatus, LanguageModelUsage, ToolUIPart } from 'ai' import type { LanguageModelUsage, ToolUIPart } from 'ai'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { import {
Conversation, Conversation,
ConversationContent, ConversationContent,
@ -14,29 +19,9 @@ import {
MessageContent, MessageContent,
MessageResponse, MessageResponse,
} from '@/components/ai-elements/message' } from '@/components/ai-elements/message'
import {
PromptInput,
PromptInputBody,
PromptInputFooter,
type PromptInputMessage,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
} from '@/components/ai-elements/prompt-input'
import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning' import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning'
import { Shimmer } from '@/components/ai-elements/shimmer' import { Shimmer } from '@/components/ai-elements/shimmer'
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool' import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool'
import {
Context,
ContextCacheUsage,
ContextContent,
ContextContentBody,
ContextContentHeader,
ContextInputUsage,
ContextOutputUsage,
ContextReasoningUsage,
ContextTrigger,
} from '@/components/ai-elements/context'
interface ChatMessage { interface ChatMessage {
id: string id: string
@ -114,13 +99,14 @@ const DEFAULT_WIDTH = 400
interface ChatSidebarProps { interface ChatSidebarProps {
defaultWidth?: number defaultWidth?: number
onClose: () => void onClose: () => void
onNewChat: () => void
conversation: ConversationItem[] conversation: ConversationItem[]
currentAssistantMessage: string currentAssistantMessage: string
currentReasoning: string currentReasoning: string
isProcessing: boolean isProcessing: boolean
message: string message: string
onMessageChange: (message: string) => void onMessageChange: (message: string) => void
onSubmit: (message: PromptInputMessage) => void onSubmit: (message: { text: string }) => void
contextUsage: LanguageModelUsage contextUsage: LanguageModelUsage
maxTokens: number maxTokens: number
usedTokens: number usedTokens: number
@ -129,6 +115,7 @@ interface ChatSidebarProps {
export function ChatSidebar({ export function ChatSidebar({
defaultWidth = DEFAULT_WIDTH, defaultWidth = DEFAULT_WIDTH,
onClose, onClose,
onNewChat,
conversation, conversation,
currentAssistantMessage, currentAssistantMessage,
currentReasoning, currentReasoning,
@ -136,14 +123,12 @@ export function ChatSidebar({
message, message,
onMessageChange, onMessageChange,
onSubmit, onSubmit,
contextUsage,
maxTokens,
usedTokens,
}: ChatSidebarProps) { }: ChatSidebarProps) {
const [width, setWidth] = useState(defaultWidth) const [width, setWidth] = useState(defaultWidth)
const [isResizing, setIsResizing] = useState(false) const [isResizing, setIsResizing] = useState(false)
const startXRef = useRef(0) const startXRef = useRef(0)
const startWidthRef = useRef(0) const startWidthRef = useRef(0)
const inputRef = useRef<HTMLInputElement>(null)
const handleMouseDown = useCallback((e: React.MouseEvent) => { const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault() e.preventDefault()
@ -152,7 +137,6 @@ export function ChatSidebar({
setIsResizing(true) setIsResizing(true)
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
// Since sidebar is on right, dragging left increases width
const delta = startXRef.current - e.clientX const delta = startXRef.current - e.clientX
const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + delta)) const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + delta))
setWidth(newWidth) setWidth(newWidth)
@ -167,10 +151,24 @@ export function ChatSidebar({
document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp) document.addEventListener('mouseup', handleMouseUp)
}, [width]) }, [width])
const hasConversation = conversation.length > 0 || currentAssistantMessage || currentReasoning const hasConversation = conversation.length > 0 || currentAssistantMessage || currentReasoning
const submitStatus: ChatStatus = isProcessing ? 'streaming' : 'ready'
const canSubmit = Boolean(message.trim()) && !isProcessing const canSubmit = Boolean(message.trim()) && !isProcessing
const handleSubmit = () => {
const trimmed = message.trim()
if (trimmed && !isProcessing) {
onSubmit({ text: trimmed })
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}
const renderConversationItem = (item: ConversationItem) => { const renderConversationItem = (item: ConversationItem) => {
if (isChatMessage(item)) { if (isChatMessage(item)) {
return ( return (
@ -234,22 +232,37 @@ export function ChatSidebar({
isResizing && "after:bg-primary" isResizing && "after:bg-primary"
)} )}
/> />
{/* Header */}
<header className="flex h-12 shrink-0 items-center justify-between border-b border-border px-3"> {/* Header - minimal, no border */}
<span className="text-sm font-medium">Chat</span> <header className="flex h-12 shrink-0 items-center justify-between px-2">
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8"> <Tooltip>
<X className="h-4 w-4" /> <TooltipTrigger asChild>
</Button> <Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8 text-muted-foreground hover:text-foreground">
<PanelRightClose className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Close</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={onNewChat} className="h-8 w-8 text-muted-foreground hover:text-foreground">
<Plus className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">New chat</TooltipContent>
</Tooltip>
</header> </header>
{/* Conversation area */} {/* Conversation area */}
<div className="flex min-h-0 flex-1 flex-col"> <div className="flex min-h-0 flex-1 flex-col relative">
<Conversation className="relative flex-1 overflow-y-auto"> <Conversation className="relative flex-1 overflow-y-auto">
<ConversationContent className={hasConversation ? "px-3 pb-24" : "px-3 min-h-full items-center justify-center"}> <ConversationContent className={hasConversation ? "px-4 pb-24" : "px-4 min-h-full items-center justify-center"}>
{!hasConversation ? ( {!hasConversation ? (
<ConversationEmptyState className="h-auto"> <ConversationEmptyState className="h-auto">
<div className="text-lg font-medium text-muted-foreground"> <div className="flex flex-col items-center gap-1 text-center">
Ask anything... <div className="text-sm text-muted-foreground">
Ask anything...
</div>
</div> </div>
</ConversationEmptyState> </ConversationEmptyState>
) : ( ) : (
@ -281,46 +294,36 @@ export function ChatSidebar({
</> </>
)} )}
</ConversationContent> </ConversationContent>
<ConversationScrollButton className="bottom-20" /> <ConversationScrollButton className="bottom-24" />
</Conversation> </Conversation>
{/* Prompt input */} {/* Input area - responsive to sidebar width, matches floating bar position exactly */}
<div className="relative sticky bottom-0 z-10 bg-background pb-3 pt-4 px-3 border-t border-border"> <div className="absolute bottom-6 left-14 right-6 z-10">
<PromptInput onSubmit={onSubmit}> <div className="flex items-center gap-2 bg-background border border-border rounded-full shadow-xl px-4 py-2.5">
<PromptInputBody> <input
<PromptInputTextarea ref={inputRef}
value={message} type="text"
onChange={(e) => onMessageChange(e.target.value)} value={message}
placeholder="Type your message..." onChange={(e) => onMessageChange(e.target.value)}
disabled={isProcessing} onKeyDown={handleKeyDown}
className="min-h-[60px] max-h-[120px]" placeholder="Ask anything..."
/> disabled={isProcessing}
</PromptInputBody> className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:opacity-50"
<PromptInputFooter> />
<PromptInputTools> <Button
<Context size="icon"
maxTokens={maxTokens} onClick={handleSubmit}
usedTokens={usedTokens} disabled={!canSubmit}
usage={contextUsage} className={cn(
> "h-7 w-7 rounded-full shrink-0 transition-all",
<ContextTrigger size="sm" /> canSubmit
<ContextContent> ? "bg-primary text-primary-foreground hover:bg-primary/90"
<ContextContentHeader /> : "bg-muted text-muted-foreground"
<ContextContentBody> )}
<ContextInputUsage /> >
<ContextOutputUsage /> <ArrowUp className="h-4 w-4" />
<ContextReasoningUsage /> </Button>
<ContextCacheUsage /> </div>
</ContextContentBody>
</ContextContent>
</Context>
</PromptInputTools>
<PromptInputSubmit
disabled={!canSubmit}
status={submitStatus}
/>
</PromptInputFooter>
</PromptInput>
</div> </div>
</div> </div>
</div> </div>