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