feat(chat): expand empty input actions

This commit is contained in:
2026-06-22 19:14:27 +08:00
parent e68cabdc54
commit cebc8f7443
6 changed files with 157 additions and 7 deletions
@@ -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)); 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 边框 */ /* 内层:白底 + 大圆角(Dart AppRadius.radius32+ focused 时 accent 边框 */
.row { .row {
display: flex; display: flex;
@@ -30,3 +37,9 @@
.rowFocused { .rowFocused {
border-color: var(--color-accent, #f84d96); 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);
}
+51 -6
View File
@@ -23,11 +23,13 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useChatDispatch } from "@/stores/chat/chat-context"; 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 { ChatInputTextField } from "./chat-input-text-field";
import { ChatSendButton } from "./chat-send-button"; import { ChatSendButton } from "./chat-send-button";
import styles from "./chat-input-bar.module.css"; import styles from "./chat-input-bar.module.css";
import { Logger } from "@/utils";
const log = new Logger("AppChatComponentsChatInputBar"); const log = new Logger("AppChatComponentsChatInputBar");
@@ -37,11 +39,22 @@ export interface ChatInputBarProps {
export function ChatInputBar({ disabled = false }: ChatInputBarProps) { export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
const dispatch = useChatDispatch(); const dispatch = useChatDispatch();
const { currentUser } = useUserState();
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [isActionPanelOpen, setIsActionPanelOpen] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const hasContent = input.trim().length > 0; 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 = () => { const handleSend = () => {
if (!hasContent) return; if (!hasContent) return;
@@ -57,23 +70,55 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
textareaRef.current?.focus(); 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 ( return (
<div className={`${styles.bar} ${isFocused ? styles.barFocused : ""}`}> <div
<div className={`${styles.row} ${isFocused ? styles.rowFocused : ""}`}> className={`${styles.bar} ${isActive ? styles.barFocused : ""} ${
isActionPanelOpen ? styles.barExpanded : ""
}`}
>
<div
className={`${styles.row} ${isActive ? styles.rowFocused : ""} ${
isActionPanelOpen ? styles.rowExpanded : ""
}`}
>
<ChatInputTextField <ChatInputTextField
ref={textareaRef} ref={textareaRef}
value={input} value={input}
onChange={setInput} onChange={handleInputChange}
onSubmit={handleSend} onSubmit={handleSend}
onFocusChange={setIsFocused} onFocusChange={setIsFocused}
disabled={disabled} disabled={disabled}
/> />
<ChatSendButton <ChatSendButton
disabled={!hasContent || disabled} disabled={disabled}
hasContent={hasContent} hasContent={hasContent}
onClick={handleSend} isExpanded={isActionPanelOpen}
onClick={handleActionButtonClick}
/> />
</div> </div>
{isActionPanelOpen ? (
<ChatInputActionPanel
voiceMinutesRemaining={voiceMinutesRemaining}
onVoiceMessageClick={handleVoiceMessageClick}
/>
) : null}
</div> </div>
); );
} }
+5 -1
View File
@@ -17,15 +17,18 @@ import styles from "./chat-send-button.module.css";
export interface ChatSendButtonProps { export interface ChatSendButtonProps {
disabled: boolean; disabled: boolean;
hasContent: boolean; hasContent: boolean;
isExpanded?: boolean;
onClick: () => void; onClick: () => void;
} }
export function ChatSendButton({ export function ChatSendButton({
disabled, disabled,
hasContent, hasContent,
isExpanded = false,
onClick, onClick,
}: ChatSendButtonProps) { }: ChatSendButtonProps) {
const Icon = hasContent ? ArrowUp : Plus; const Icon = hasContent ? ArrowUp : Plus;
const ariaLabel = hasContent ? "Send message" : "Open chat actions";
return ( return (
<button <button
@@ -33,7 +36,8 @@ export function ChatSendButton({
className={`${styles.button} ${disabled ? "" : styles.buttonActive}`} className={`${styles.button} ${disabled ? "" : styles.buttonActive}`}
disabled={disabled} disabled={disabled}
onClick={onClick} 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" /> <Icon className={styles.icon} size={24} aria-hidden="true" />
</button> </button>
+1
View File
@@ -6,6 +6,7 @@ export * from "./ai-disclosure-banner";
export * from "./browser-hint-overlay"; export * from "./browser-hint-overlay";
export * from "./chat-area"; export * from "./chat-area";
export * from "./chat-header"; export * from "./chat-header";
export * from "./chat-input-action-panel";
export * from "./chat-input-bar"; export * from "./chat-input-bar";
export * from "./chat-input-text-field"; export * from "./chat-input-text-field";
export * from "./chat-quota-exhausted-banner"; export * from "./chat-quota-exhausted-banner";