picoclaw/web/frontend/src/components/chat/chat-composer.tsx
wenjie c513ad22d7
fix(web): refactor pico chat flow and fix proxied websocket URLs (#1639)
- move chat controller, state, protocol, history, and websocket logic into a dedicated chat feature module
- improve chat reconnection, session hydration, and send gating based on actual websocket state
- preserve gateway status during transient SSE disconnects and update stop state immediately
- generate wss websocket URLs behind HTTPS proxies and add backend tests for forwarded proto handling
2026-03-16 16:25:16 +08:00

67 lines
2.2 KiB
TypeScript

import { IconArrowUp } from "@tabler/icons-react"
import type { KeyboardEvent } from "react"
import { useTranslation } from "react-i18next"
import TextareaAutosize from "react-textarea-autosize"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
interface ChatComposerProps {
input: string
onInputChange: (value: string) => void
onSend: () => void
isConnected: boolean
hasDefaultModel: boolean
}
export function ChatComposer({
input,
onInputChange,
onSend,
isConnected,
hasDefaultModel,
}: ChatComposerProps) {
const { t } = useTranslation()
const canInput = isConnected && hasDefaultModel
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.nativeEvent.isComposing) return
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
onSend()
}
}
return (
<div className="bg-background shrink-0 px-4 pt-4 pb-[calc(1rem+env(safe-area-inset-bottom))] md:px-8 md:pb-8 lg:px-24 xl:px-48">
<div className="bg-card border-border/80 mx-auto flex max-w-[1000px] flex-col rounded-2xl border p-3 shadow-md">
<TextareaAutosize
value={input}
onChange={(e) => onInputChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t("chat.placeholder")}
disabled={!canInput}
className={cn(
"placeholder:text-muted-foreground max-h-[200px] min-h-[60px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent",
!canInput && "cursor-not-allowed",
)}
minRows={1}
maxRows={8}
/>
<div className="mt-2 flex items-center justify-between px-1">
<div className="flex items-center gap-1">{/* action buttons */}</div>
<Button
size="icon"
className="size-8 rounded-full bg-violet-500 text-white transition-transform hover:bg-violet-600 active:scale-95"
onClick={onSend}
disabled={!input.trim() || !canInput}
>
<IconArrowUp className="size-4" />
</Button>
</div>
</div>
</div>
)
}