Files
cozsweet-frontend-nextjs/src/app/chat/components/chat-input-bar.tsx
T

125 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* ChatInputBar 输入栏
*
* 原始 Dart: lib/ui/chat/widgets/chat_input_bar.dart121 行)
*
* 状态:
* - input:受控文本
* - isFocused:是否聚焦(控制外层 pink-bg / box-shadow / padding-top
* - hasContent:是否有 trim 内容(决定发送按钮是否启用)
*
* 容器层次(与 Dart 一致):
* - 外层 `.bar`focused 时 `padding-top: md` + 粉色背景 + 阴影
* - 内层 `.row`:白底 + `border-radius: 32` + focused 时 accent 边框
* - Row[ChatInputTextField (Expanded), ChatSendButton]
*
* 行为:
* - send 后清空 input + 保持 textarea 焦点(与 Dart `_focusNode.requestFocus()` 一致)
*
* 注意:Dart 源里的 `ImageUploadButton` 已被注释(`// const ImageUploadButton()`),
* `ChatMoreButton` / `FunctionButtonBar` 不存在于 Dart 源;本组件仅保留与 Dart 一致的部分。
*/
import { useRef, useState } from "react";
import { useChatDispatch } from "@/stores/chat/chat-context";
import { useUserState } from "@/stores/user/user-context";
import { Logger } from "@/utils";
import { ChatInputActionPanel } from "./chat-input-action-panel";
import { ChatInputTextField } from "./chat-input-text-field";
import { ChatSendButton } from "./chat-send-button";
import styles from "./chat-input-bar.module.css";
const log = new Logger("AppChatComponentsChatInputBar");
export interface ChatInputBarProps {
disabled?: boolean;
}
export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
const dispatch = useChatDispatch();
const { currentUser } = useUserState();
const [input, setInput] = useState("");
const [isFocused, setIsFocused] = useState(false);
const [isActionPanelOpen, setIsActionPanelOpen] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const hasContent = input.trim().length > 0;
const isActive = isFocused || isActionPanelOpen;
const voiceMinutesRemaining = currentUser?.voiceMinutesRemaining ?? 0;
const handleInputChange = (value: string) => {
setInput(value);
if (value.trim().length > 0) {
setIsActionPanelOpen(false);
}
};
const handleSend = () => {
if (!hasContent) return;
log.debug("[chat-input-bar] handleSend", {
contentLength: input.length,
contentPreview: input.slice(0, 50),
hasContent,
disabled,
isFocused,
});
dispatch({ type: "ChatSendMessage", content: input });
setInput("");
textareaRef.current?.focus();
};
const handleActionButtonClick = () => {
if (hasContent) {
handleSend();
return;
}
textareaRef.current?.blur();
setIsFocused(false);
setIsActionPanelOpen((open) => !open);
};
const handleVoiceMessageClick = () => {
log.debug("[chat-input-bar] voice message action clicked", {
voiceMinutesRemaining,
});
};
return (
<div
className={`${styles.bar} ${isActive ? styles.barFocused : ""} ${
isActionPanelOpen ? styles.barExpanded : ""
}`}
>
<div
className={`${styles.row} ${isActive ? styles.rowFocused : ""} ${
isActionPanelOpen ? styles.rowExpanded : ""
}`}
>
<ChatInputTextField
ref={textareaRef}
value={input}
onChange={handleInputChange}
onSubmit={handleSend}
onFocusChange={setIsFocused}
disabled={disabled}
/>
<ChatSendButton
disabled={disabled}
hasContent={hasContent}
isExpanded={isActionPanelOpen}
onClick={handleActionButtonClick}
/>
</div>
{isActionPanelOpen ? (
<ChatInputActionPanel
voiceMinutesRemaining={voiceMinutesRemaining}
onVoiceMessageClick={handleVoiceMessageClick}
/>
) : null}
</div>
);
}