feat(chat): expand empty input actions
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
.panel {
|
||||
min-height: 170px;
|
||||
padding: 24px 16px 20px;
|
||||
border-top: 1px solid rgba(246, 87, 160, 0.14);
|
||||
background: #fff1f4;
|
||||
}
|
||||
|
||||
.voiceCard {
|
||||
width: 142px;
|
||||
min-height: 153px;
|
||||
padding: 12px 12px 14px;
|
||||
border: 1px solid rgba(30, 30, 30, 0.08);
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 2px rgba(30, 30, 30, 0.03);
|
||||
color: #1e1e1e;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.voiceCard:focus-visible {
|
||||
outline: 2px solid var(--color-accent, #f84d96);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.voiceIcon {
|
||||
display: block;
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.voiceTitle,
|
||||
.voiceMinutes {
|
||||
font-family: var(--font-athelas), Athelas, serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.08;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.voiceTitle {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.voiceMinutes {
|
||||
color: var(--color-accent, #f84d96);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
import styles from "./chat-input-action-panel.module.css";
|
||||
|
||||
export interface ChatInputActionPanelProps {
|
||||
voiceMinutesRemaining: number;
|
||||
onVoiceMessageClick?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputActionPanel({
|
||||
voiceMinutesRemaining,
|
||||
onVoiceMessageClick,
|
||||
}: ChatInputActionPanelProps) {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.voiceCard}
|
||||
onClick={onVoiceMessageClick}
|
||||
aria-label={`Voice message, ${voiceMinutesRemaining} minutes remaining`}
|
||||
>
|
||||
<Image
|
||||
src="/images/chat/ic_voicemessage_act.png"
|
||||
alt=""
|
||||
width={78}
|
||||
height={78}
|
||||
className={styles.voiceIcon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className={styles.voiceTitle}>voice message</span>
|
||||
<span className={styles.voiceMinutes}>({voiceMinutesRemaining}min)</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,13 @@
|
||||
box-shadow: 0 4px 12px var(--color-input-box-shadow, rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.barExpanded {
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
background: #fff1f4;
|
||||
}
|
||||
|
||||
/* 内层:白底 + 大圆角(Dart AppRadius.radius32)+ focused 时 accent 边框 */
|
||||
.row {
|
||||
display: flex;
|
||||
@@ -30,3 +37,9 @@
|
||||
.rowFocused {
|
||||
border-color: var(--color-accent, #f84d96);
|
||||
}
|
||||
|
||||
.rowExpanded {
|
||||
margin: 0 var(--spacing-md, 12px) var(--spacing-md, 12px);
|
||||
border-color: #ff8fc7;
|
||||
box-shadow: 0 2px 10px rgba(246, 87, 160, 0.16);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
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";
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
const log = new Logger("AppChatComponentsChatInputBar");
|
||||
|
||||
@@ -37,11 +39,22 @@ export interface ChatInputBarProps {
|
||||
|
||||
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;
|
||||
@@ -57,23 +70,55 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
|
||||
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} ${isFocused ? styles.barFocused : ""}`}>
|
||||
<div className={`${styles.row} ${isFocused ? styles.rowFocused : ""}`}>
|
||||
<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={setInput}
|
||||
onChange={handleInputChange}
|
||||
onSubmit={handleSend}
|
||||
onFocusChange={setIsFocused}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ChatSendButton
|
||||
disabled={!hasContent || disabled}
|
||||
disabled={disabled}
|
||||
hasContent={hasContent}
|
||||
onClick={handleSend}
|
||||
isExpanded={isActionPanelOpen}
|
||||
onClick={handleActionButtonClick}
|
||||
/>
|
||||
</div>
|
||||
{isActionPanelOpen ? (
|
||||
<ChatInputActionPanel
|
||||
voiceMinutesRemaining={voiceMinutesRemaining}
|
||||
onVoiceMessageClick={handleVoiceMessageClick}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,15 +17,18 @@ import styles from "./chat-send-button.module.css";
|
||||
export interface ChatSendButtonProps {
|
||||
disabled: boolean;
|
||||
hasContent: boolean;
|
||||
isExpanded?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ChatSendButton({
|
||||
disabled,
|
||||
hasContent,
|
||||
isExpanded = false,
|
||||
onClick,
|
||||
}: ChatSendButtonProps) {
|
||||
const Icon = hasContent ? ArrowUp : Plus;
|
||||
const ariaLabel = hasContent ? "Send message" : "Open chat actions";
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -33,7 +36,8 @@ export function ChatSendButton({
|
||||
className={`${styles.button} ${disabled ? "" : styles.buttonActive}`}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
aria-label={hasContent ? "Send message" : "Add message content"}
|
||||
aria-expanded={hasContent ? undefined : isExpanded}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<Icon className={styles.icon} size={24} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./ai-disclosure-banner";
|
||||
export * from "./browser-hint-overlay";
|
||||
export * from "./chat-area";
|
||||
export * from "./chat-header";
|
||||
export * from "./chat-input-action-panel";
|
||||
export * from "./chat-input-bar";
|
||||
export * from "./chat-input-text-field";
|
||||
export * from "./chat-quota-exhausted-banner";
|
||||
|
||||
Reference in New Issue
Block a user