feat(chat): support ai photo viewing

This commit is contained in:
2026-06-23 10:33:20 +08:00
parent cebc8f7443
commit d81abd6efd
16 changed files with 160 additions and 45 deletions
@@ -1,25 +1,4 @@
"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";
@@ -44,7 +44,7 @@ export const ChatInputTextField = forwardRef<
onChange,
onSubmit,
onFocusChange,
placeholder = "|Say anything thats on your mind ^U^…",
placeholder = "Say anything thats on your mind ^U^…",
disabled = false,
autoFocus = false,
},
@@ -27,6 +27,8 @@ import { ROUTES } from "@/router/routes";
import styles from "./chat-quota-exhausted-banner.module.css";
export interface ChatQuotaExhaustedBannerProps {
title?: string;
ctaLabel?: string;
/**
* 自定义点击回调(不传则默认 router.push(ROUTES.subscription)
* - 测试时可传 mock
@@ -36,9 +38,12 @@ export interface ChatQuotaExhaustedBannerProps {
}
export function ChatQuotaExhaustedBanner({
title = "The limit for free chat times\nhas been reached today",
ctaLabel = "Unlock your membership to continue",
onUnlock,
}: ChatQuotaExhaustedBannerProps) {
const router = useRouter();
const titleLines = title.split("\n");
const handleClick = () => {
if (onUnlock) {
@@ -56,17 +61,20 @@ export function ChatQuotaExhaustedBanner({
data-testid="chat-quota-exhausted-banner"
>
<p className={styles.text}>
The limit for free chat times
<br />
has been reached today
{titleLines.map((line, index) => (
<span key={`${line}-${index}`}>
{line}
{index < titleLines.length - 1 ? <br /> : null}
</span>
))}
</p>
<button
type="button"
className={styles.cta}
onClick={handleClick}
aria-label="Unlock your membership to continue"
aria-label={ctaLabel}
>
Unlock your membership to continue
{ctaLabel}
</button>
</div>
);