diff --git a/src/app/chat/components/chat-input-action-panel.module.css b/src/app/chat/components/chat-input-action-panel.module.css
new file mode 100644
index 00000000..dc2bcc5f
--- /dev/null
+++ b/src/app/chat/components/chat-input-action-panel.module.css
@@ -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);
+}
diff --git a/src/app/chat/components/chat-input-action-panel.tsx b/src/app/chat/components/chat-input-action-panel.tsx
new file mode 100644
index 00000000..44388111
--- /dev/null
+++ b/src/app/chat/components/chat-input-action-panel.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/src/app/chat/components/chat-input-bar.module.css b/src/app/chat/components/chat-input-bar.module.css
index 4dc0a361..850c2856 100644
--- a/src/app/chat/components/chat-input-bar.module.css
+++ b/src/app/chat/components/chat-input-bar.module.css
@@ -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);
+}
diff --git a/src/app/chat/components/chat-input-bar.tsx b/src/app/chat/components/chat-input-bar.tsx
index 233b478a..2a4f7085 100644
--- a/src/app/chat/components/chat-input-bar.tsx
+++ b/src/app/chat/components/chat-input-bar.tsx
@@ -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(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 (
-
-
+
+
+ {isActionPanelOpen ? (
+
+ ) : null}
);
}
diff --git a/src/app/chat/components/chat-send-button.tsx b/src/app/chat/components/chat-send-button.tsx
index 913fddbe..fd03bbd4 100644
--- a/src/app/chat/components/chat-send-button.tsx
+++ b/src/app/chat/components/chat-send-button.tsx
@@ -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 (
diff --git a/src/app/chat/components/index.ts b/src/app/chat/components/index.ts
index e0ca93c6..c522a0b5 100644
--- a/src/app/chat/components/index.ts
+++ b/src/app/chat/components/index.ts
@@ -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";