fix: isolate attachment submission state

This commit is contained in:
2026-08-24 01:50:04 +08:00
parent bec93d0918
commit 1ca0249e69
8 changed files with 320 additions and 21 deletions

View File

@@ -301,7 +301,8 @@ export function CodingChatPanel({
]);
const handleAddFiles = useCallback((files: File[]) => {
if (!draftKey) return;
if (!draftKey
|| (targetConversationId && submissionFlightsRef.current.has(targetConversationId))) return;
const accepted: LocalComposerAttachment[] = [];
const remaining = Math.max(0, CODING_ATTACHMENT_MAX_COUNT - localAttachments.length);
let validationError = files.length > remaining
@@ -335,10 +336,11 @@ export function CodingChatPanel({
...current,
[draftKey]: [...(current[draftKey] ?? []), ...accepted],
}));
}, [draftKey, localAttachments.length]);
}, [draftKey, localAttachments.length, targetConversationId]);
const handleRemoveAttachment = useCallback((id: string) => {
if (!draftKey) return;
if (!draftKey
|| (targetConversationId && submissionFlightsRef.current.has(targetConversationId))) return;
setAttachmentsByDraftKey((current) => {
const attachment = current[draftKey]?.find((item) => item.id === id);
if (attachment) URL.revokeObjectURL(attachment.previewUrl);
@@ -349,7 +351,12 @@ export function CodingChatPanel({
});
uploadedAttachmentsRef.current.delete(id);
uploadFlightsRef.current.delete(id);
}, [draftKey]);
setSubmissionErrors((current) => {
const next = { ...current };
delete next[draftKey];
return next;
});
}, [draftKey, targetConversationId]);
const prepareAttachments = useCallback(async (
attachments: LocalComposerAttachment[],

View File

@@ -91,6 +91,7 @@ export function CodingComposer({
type="file"
accept="image/png,image/jpeg,image/webp,image/gif"
multiple
disabled={!editable || submitting}
className="sr-only"
data-testid="coding-file-attachment-input"
onChange={(event) => {
@@ -110,8 +111,9 @@ export function CodingComposer({
/>
<button
type="button"
className="absolute right-1 top-1 flex h-10 w-10 translate-x-2 -translate-y-2 items-end justify-start rounded-full bg-foreground/85 p-1.5 text-background transition-[background-color,scale] duration-150 ease-out hover:bg-foreground active:scale-[0.96]"
className="absolute right-1 top-1 flex h-10 w-10 translate-x-2 -translate-y-2 items-end justify-start rounded-full bg-foreground/85 p-1.5 text-background transition-[background-color,scale] duration-150 ease-out hover:bg-foreground active:scale-[0.96] disabled:cursor-not-allowed disabled:opacity-60"
aria-label={`移除图片 ${attachment.name}`}
disabled={submitting}
onClick={() => onRemoveAttachment(attachment.id)}
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
@@ -133,6 +135,7 @@ export function CodingComposer({
if (canSend) onSubmit();
}}
onPaste={(event) => {
if (submitting) return;
const files = Array.from(event.clipboardData.files).filter((file) => file.type.startsWith('image/'));
if (files.length > 0) onAddFiles(files);
}}

View File

@@ -702,10 +702,12 @@ export function createCodingConversationStore(
};
});
let attachmentsPrepared = !input.prepareAttachments;
try {
if (input.prepareAttachments) {
submittedDraft.attachments = (await input.prepareAttachments())
.map((attachment) => ({ ...attachment }));
attachmentsPrepared = true;
set((state) => {
const currentEntry = state.entriesByConversationId[input.conversationId] ?? emptyEntry();
const requests = state.requestsByConversationId[input.conversationId] ?? {};
@@ -765,6 +767,7 @@ export function createCodingConversationStore(
} catch (error) {
const failure = errorDetails(error);
const uncertain = failure.code === 'CODING_REQUEST_UNCERTAIN';
const preSubmitFailure = !attachmentsPrepared;
set((state) => {
const currentEntry = state.entriesByConversationId[input.conversationId] ?? emptyEntry();
const requests = state.requestsByConversationId[input.conversationId] ?? {};
@@ -776,7 +779,11 @@ export function createCodingConversationStore(
const reducer = uncertain
? currentEntry.reducer
: withRejectedNode(currentEntry.reducer, nodeId);
const nextEntry = { ...currentEntry, reducer, error: failure.message };
const nextEntry = {
...currentEntry,
reducer,
error: preSubmitFailure ? currentEntry.error : failure.message,
};
return {
entriesByConversationId: {
...state.entriesByConversationId,