refactor: extract canvas detail slot helpers

This commit is contained in:
Your Name 2026-05-27 10:41:10 +08:00
parent 6a19c43094
commit 9821832364
3 changed files with 98 additions and 19 deletions

View File

@ -163,6 +163,11 @@ import {
replaceSavedDetailArchive,
sanitizeDetailArchiveList,
} from "@/components/canvas/chat-panel/canvas-detail-archive";
import {
applyDetailSlotAttachment,
fillDetailSlotAttachments,
getDetailSlotTargetIndexes,
} from "@/components/canvas/chat-panel/canvas-detail-slots";
import {
clampChatRequestText,
deriveSessionTitleFromMessages,
@ -4954,11 +4959,7 @@ export function CanvasChatPanel({
const updateDetailSlotAttachment = useCallback(
(slotIndex: number, attachment: PickedObjectAttachment | null) => {
setDetailSlots((current) => {
const next = [...current];
next[slotIndex] = attachment;
return next;
});
setDetailSlots((current) => applyDetailSlotAttachment(current, slotIndex, attachment));
},
[]
);
@ -4968,13 +4969,7 @@ export function CanvasChatPanel({
if (slotIndex < 0 || slotIndex >= DETAIL_SLOT_CONFIGS.length) return 0;
if (selectedImageNodes.length === 0) return 0;
const slotRole = DETAIL_SLOT_CONFIGS[slotIndex]?.role;
const sameRoleIndexes = DETAIL_SLOT_CONFIGS.map((slot, index) =>
slot.role === slotRole ? index : -1
).filter((index) => index >= 0);
const startOffset = sameRoleIndexes.indexOf(slotIndex);
const targetIndexes =
startOffset >= 0 ? sameRoleIndexes.slice(startOffset) : sameRoleIndexes;
const targetIndexes = getDetailSlotTargetIndexes(slotIndex);
if (targetIndexes.length === 0) return 0;
const candidateAttachments = selectedImageNodes
@ -4990,13 +4985,9 @@ export function CanvasChatPanel({
let filledCount = 0;
setDetailSlots((current) => {
const next = [...current];
for (let i = 0; i < targetIndexes.length && i < candidateAttachments.length; i += 1) {
const index = targetIndexes[i]!;
next[index] = candidateAttachments[i]!;
filledCount += 1;
}
return next;
const result = fillDetailSlotAttachments(current, targetIndexes, candidateAttachments);
filledCount = result.filledCount;
return result.slots;
});
return filledCount;
},

View File

@ -0,0 +1,48 @@
import {
type DetailSlotConfig,
DETAIL_SLOT_CONFIGS,
} from "@/components/canvas/chat-panel/canvas-detail-config";
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
export function getDetailSlotTargetIndexes(
slotIndex: number,
slotConfigs: DetailSlotConfig[] = DETAIL_SLOT_CONFIGS
): number[] {
if (slotIndex < 0 || slotIndex >= slotConfigs.length) return [];
const slotRole = slotConfigs[slotIndex]?.role;
const sameRoleIndexes = slotConfigs
.map((slot, index) => (slot.role === slotRole ? index : -1))
.filter((index) => index >= 0);
const startOffset = sameRoleIndexes.indexOf(slotIndex);
return startOffset >= 0 ? sameRoleIndexes.slice(startOffset) : sameRoleIndexes;
}
export function applyDetailSlotAttachment(
current: Array<PickedObjectAttachment | null>,
slotIndex: number,
attachment: PickedObjectAttachment | null
): Array<PickedObjectAttachment | null> {
const next = [...current];
if (slotIndex < 0) return next;
next[slotIndex] = attachment;
return next;
}
export function fillDetailSlotAttachments(
current: Array<PickedObjectAttachment | null>,
targetIndexes: number[],
attachments: PickedObjectAttachment[]
): {
slots: Array<PickedObjectAttachment | null>;
filledCount: number;
} {
const next = [...current];
let filledCount = 0;
for (let i = 0; i < targetIndexes.length && i < attachments.length; i += 1) {
const index = targetIndexes[i]!;
if (index < 0) continue;
next[index] = attachments[i]!;
filledCount += 1;
}
return { slots: next, filledCount };
}

View File

@ -33,6 +33,12 @@ import {
replaceSavedDetailArchive,
sanitizeDetailArchiveList,
} from "@/components/canvas/chat-panel/canvas-detail-archive";
import {
applyDetailSlotAttachment,
fillDetailSlotAttachments,
getDetailSlotTargetIndexes,
} from "@/components/canvas/chat-panel/canvas-detail-slots";
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
import type { ChatMessage } from "@/lib/types";
test.describe("canvas chat pure helpers", () => {
@ -404,4 +410,38 @@ test.describe("canvas chat pure helpers", () => {
}));
expect(prependDetailArchive(manyArchives, saved)).toHaveLength(30);
});
test("fills detail slots by current role group without crossing groups", () => {
expect(getDetailSlotTargetIndexes(0)).toEqual([0, 1, 2, 3, 4]);
expect(getDetailSlotTargetIndexes(3)).toEqual([3, 4]);
expect(getDetailSlotTargetIndexes(5)).toEqual([5]);
expect(getDetailSlotTargetIndexes(7)).toEqual([7, 8]);
expect(getDetailSlotTargetIndexes(-1)).toEqual([]);
expect(getDetailSlotTargetIndexes(99)).toEqual([]);
const current = Array.from({ length: DETAIL_SLOT_CONFIGS.length }, () => null);
const productA: PickedObjectAttachment = {
name: "product-a",
url: "https://cdn.example.com/product-a.png",
contentType: "image/png",
};
const productB: PickedObjectAttachment = {
name: "product-b",
url: "https://cdn.example.com/product-b.png",
contentType: "image/png",
};
const updatedSingle = applyDetailSlotAttachment(current, 5, productA);
expect(updatedSingle[5]).toBe(productA);
expect(current[5]).toBeNull();
const filled = fillDetailSlotAttachments(current, getDetailSlotTargetIndexes(3), [
productA,
productB,
]);
expect(filled.filledCount).toBe(2);
expect(filled.slots[3]).toBe(productA);
expect(filled.slots[4]).toBe(productB);
expect(filled.slots[5]).toBeNull();
});
});