Merge branch 'dev' into test
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));
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user