mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-12 08:42: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 { 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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue