抓大鹅F3实现
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-30 21:01:36 +08:00
parent 22f6e6f4e7
commit 08815d98bc
39 changed files with 5891 additions and 18 deletions

View File

@@ -1,4 +1,4 @@
import { ArrowLeft, Paperclip, Send, Sparkles } from 'lucide-react';
import { ArrowLeft, ImagePlus, Paperclip, Send, Sparkles, X } from 'lucide-react';
import type { ChangeEvent } from 'react';
import { useEffect, useRef, useState } from 'react';
@@ -75,15 +75,21 @@ type CreationAgentWorkspaceProps = {
isBusy?: boolean;
error?: string | null;
quickActions?: CreationAgentQuickAction[];
referenceImagePreviewSrc?: string | null;
referenceImageLabel?: string | null;
referenceImageError?: string | null;
onBack: () => void;
onSubmitText: (text: string, quickActionKey?: string) => void;
onPrimaryAction: () => void;
onQuickAction?: (action: CreationAgentQuickAction) => void;
onReferenceImageChange?: (file: File) => Promise<void> | void;
onClearReferenceImage?: () => void;
};
const AUTO_SCROLL_FOLLOW_THRESHOLD_PX = 96;
const DOCUMENT_INPUT_ACCEPT =
'.txt,.md,.markdown,.csv,.json,text/plain,text/markdown,text/csv,application/json';
const REFERENCE_IMAGE_INPUT_ACCEPT = 'image/png,image/jpeg,image/webp';
function uniqueRecommendedReplies(recommendedReplies: string[] = []) {
return [
@@ -290,19 +296,26 @@ export function CreationAgentWorkspace({
isBusy = false,
error = null,
quickActions = [],
referenceImagePreviewSrc = null,
referenceImageLabel = null,
referenceImageError = null,
onBack,
onSubmitText,
onPrimaryAction,
onQuickAction,
onReferenceImageChange,
onClearReferenceImage,
}: CreationAgentWorkspaceProps) {
const [draftText, setDraftText] = useState('');
const [documentInputError, setDocumentInputError] = useState<string | null>(
null,
);
const [isParsingDocumentInput, setIsParsingDocumentInput] = useState(false);
const [isReadingReferenceImage, setIsReadingReferenceImage] = useState(false);
// 统一聊天区只在用户仍停留在底部附近时跟随新内容,避免流式回复持续抢走阅读位置。
const messageListRef = useRef<HTMLDivElement | null>(null);
const documentInputRef = useRef<HTMLInputElement | null>(null);
const referenceImageInputRef = useRef<HTMLInputElement | null>(null);
const shouldAutoScrollRef = useRef(true);
useEffect(() => {
@@ -376,7 +389,7 @@ export function CreationAgentWorkspace({
const submit = () => {
const text = draftText.trim();
if (!text || isBusy || isParsingDocumentInput) {
if (!text || isBusy || isParsingDocumentInput || isReadingReferenceImage) {
return;
}
@@ -399,6 +412,10 @@ export function CreationAgentWorkspace({
documentInputRef.current?.click();
};
const openReferenceImagePicker = () => {
referenceImageInputRef.current?.click();
};
const handleDocumentInputChange = async (
event: ChangeEvent<HTMLInputElement>,
) => {
@@ -426,6 +443,25 @@ export function CreationAgentWorkspace({
}
};
const handleReferenceImageInputChange = async (
event: ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0] ?? null;
event.target.value = '';
if (!file || isBusy || isReadingReferenceImage || !onReferenceImageChange) {
return;
}
setIsReadingReferenceImage(true);
try {
await onReferenceImageChange(file);
} finally {
setIsReadingReferenceImage(false);
}
};
return (
<div className="mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
<div
@@ -545,9 +581,36 @@ export function CreationAgentWorkspace({
)}
</div>
{documentInputError || error ? (
{referenceImagePreviewSrc ? (
<div className="mx-4 mb-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
<div className="h-12 w-12 shrink-0 overflow-hidden rounded-[0.9rem] bg-[var(--platform-track-fill)]">
<img
src={referenceImagePreviewSrc}
alt="参考图"
className="h-full w-full object-cover"
/>
</div>
<div className="min-w-0 flex-1 truncate text-sm font-semibold text-[var(--platform-text-strong)]">
{referenceImageLabel || '已选择参考图'}
</div>
{onClearReferenceImage ? (
<button
type="button"
disabled={isBusy || isReadingReferenceImage}
onClick={onClearReferenceImage}
className="platform-icon-button h-9 w-9"
aria-label="移除参考图"
title="移除参考图"
>
<X className="h-4 w-4" />
</button>
) : null}
</div>
) : null}
{documentInputError || referenceImageError || error ? (
<div className="mx-4 mb-3 rounded-[1rem] border border-red-200/70 bg-red-50 px-3 py-2 text-sm text-red-600">
{documentInputError || error}
{documentInputError || referenceImageError || error}
</div>
) : null}
@@ -560,6 +623,15 @@ export function CreationAgentWorkspace({
className="hidden"
onChange={handleDocumentInputChange}
/>
{onReferenceImageChange ? (
<input
ref={referenceImageInputRef}
type="file"
accept={REFERENCE_IMAGE_INPUT_ACCEPT}
className="hidden"
onChange={handleReferenceImageInputChange}
/>
) : null}
<button
type="button"
aria-label={
@@ -575,9 +647,30 @@ export function CreationAgentWorkspace({
className={`h-4 w-4 ${isParsingDocumentInput ? 'animate-pulse' : ''}`}
/>
</button>
{onReferenceImageChange ? (
<button
type="button"
aria-label={
isReadingReferenceImage ? '正在读取参考图' : '上传参考图'
}
title={
isReadingReferenceImage ? '正在读取参考图' : '上传参考图'
}
aria-busy={isReadingReferenceImage}
disabled={isBusy || isReadingReferenceImage}
onClick={openReferenceImagePicker}
className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full text-[var(--platform-text-base)] hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-40"
>
<ImagePlus
className={`h-4 w-4 ${isReadingReferenceImage ? 'animate-pulse' : ''}`}
/>
</button>
) : null}
<textarea
value={draftText}
disabled={isBusy || isParsingDocumentInput}
disabled={
isBusy || isParsingDocumentInput || isReadingReferenceImage
}
rows={2}
onChange={(event) => {
setDraftText(event.target.value);
@@ -595,7 +688,12 @@ export function CreationAgentWorkspace({
<button
type="button"
aria-label="发送"
disabled={isBusy || isParsingDocumentInput || !draftText.trim()}
disabled={
isBusy ||
isParsingDocumentInput ||
isReadingReferenceImage ||
!draftText.trim()
}
onClick={submit}
className={`inline-flex h-11 w-11 items-center justify-center rounded-full text-white disabled:cursor-not-allowed disabled:opacity-40 ${theme.userBubbleClass}`}
>