mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-12 00:32:38 +02:00
replace ChatButton with ChatInputBar
This commit is contained in:
parent
35c99cb999
commit
c60d6d11ff
3 changed files with 155 additions and 90 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue