Files
Keep/keep-notes/components/chat/chat-input.tsx
Sepehr Ramezani 402e88b788 feat(ux): epic UX design improvements across agents, chat, notes, and i18n
Comprehensive UI/UX updates including agent card redesign, chat container
improvements, note editor enhancements, memory echo notifications, and
updated translations for all 15 locales.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-19 23:01:04 +02:00

149 lines
5.8 KiB
TypeScript

'use client'
import { useState, useRef, useEffect } from 'react'
import { Send, BookOpen, X, Globe } from 'lucide-react'
import { getNotebookIcon } from '@/lib/notebook-icon'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { useLanguage } from '@/lib/i18n'
interface ChatInputProps {
onSend: (message: string, notebookId?: string) => void
isLoading?: boolean
notebooks: any[]
currentNotebookId?: string | null
webSearchEnabled?: boolean
onToggleWebSearch?: () => void
webSearchAvailable?: boolean
}
export function ChatInput({ onSend, isLoading, notebooks, currentNotebookId, webSearchEnabled, onToggleWebSearch, webSearchAvailable }: ChatInputProps) {
const { t } = useLanguage()
const [input, setInput] = useState('')
const [selectedNotebook, setSelectedNotebook] = useState<string | undefined>(currentNotebookId || undefined)
const textareaRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
if (currentNotebookId) {
setSelectedNotebook(currentNotebookId)
}
}, [currentNotebookId])
const handleSend = () => {
if (!input.trim() || isLoading) return
onSend(input, selectedNotebook)
setInput('')
if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`
}
}, [input])
return (
<div className="w-full relative">
<div className="relative flex flex-col bg-slate-50 dark:bg-[#202228] rounded-[24px] border border-slate-200/60 dark:border-white/10 shadow-sm focus-within:shadow-md focus-within:border-slate-300 dark:focus-within:border-white/20 transition-all duration-300 overflow-hidden">
{/* Input Area */}
<Textarea
ref={textareaRef}
placeholder={t('chat.placeholder')}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
className="flex-1 min-h-[56px] max-h-[40vh] bg-transparent border-none focus-visible:ring-0 resize-none py-4 px-5 text-[15px] placeholder:text-slate-400"
/>
{/* Bottom Actions Bar */}
<div className="flex items-center justify-between px-3 pb-3 pt-1">
{/* Context Selector */}
<div className="flex items-center gap-2">
<Select
value={selectedNotebook || 'global'}
onValueChange={(val) => setSelectedNotebook(val === 'global' ? undefined : val)}
>
<SelectTrigger className="h-8 w-auto min-w-[130px] rounded-full bg-white dark:bg-[#1a1c22] border-slate-200 dark:border-white/10 shadow-sm text-xs font-medium gap-2 ring-offset-transparent focus:ring-0 focus:ring-offset-0 hover:bg-slate-50 dark:hover:bg-[#252830] transition-colors">
<BookOpen className="h-3.5 w-3.5 text-muted-foreground" />
<SelectValue placeholder={t('chat.allNotebooks')} />
</SelectTrigger>
<SelectContent className="rounded-xl shadow-lg border-slate-200 dark:border-white/10">
<SelectItem value="global" className="rounded-lg text-sm text-muted-foreground">{t('chat.inAllNotebooks')}</SelectItem>
{notebooks.map((nb) => (
<SelectItem key={nb.id} value={nb.id} className="rounded-lg text-sm">
{(() => {
const Icon = getNotebookIcon(nb.icon)
return <Icon className="w-3.5 h-3.5" />
})()} {nb.name}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedNotebook && (
<Badge variant="secondary" className="text-[10px] bg-primary/10 text-primary border-none rounded-full px-2.5 h-6 font-semibold tracking-wide">
{t('chat.active')}
</Badge>
)}
{webSearchAvailable && (
<button
type="button"
onClick={onToggleWebSearch}
className={cn(
"h-8 rounded-full border shadow-sm text-xs font-medium gap-1.5 flex items-center px-3 transition-all duration-200",
webSearchEnabled
? "bg-primary/10 text-primary border-primary/30 hover:bg-primary/20"
: "bg-white dark:bg-[#1a1c22] border-slate-200 dark:border-white/10 text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-[#252830]"
)}
>
<Globe className="h-3.5 w-3.5" />
{webSearchEnabled && t('chat.webSearch')}
</button>
)}
</div>
{/* Send Button */}
<Button
disabled={!input.trim() || isLoading}
onClick={handleSend}
size="icon"
className={cn(
"rounded-full h-8 w-8 transition-all duration-200",
input.trim() ? "bg-primary text-primary-foreground shadow-sm hover:scale-105" : "bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500"
)}
>
<Send className="h-4 w-4 ml-0.5" />
</Button>
</div>
</div>
<div className="text-center mt-3">
<span className="text-[11px] text-muted-foreground/60 w-full block">
{t('chat.disclaimer')}
</span>
</div>
</div>
)
}