59efc78c0e
WorkbenchPage.tsx: 4146 → 3047 lines (-27%) Extracted to 6 sibling modules: - workbenchConstants.ts (403L): types, MODE_META, option arrays, shared helpers - workbenchStorage.ts (172L): localStorage read/write/persist functions - workbenchReferenceUtils.ts (210L): image compression, fingerprint, file helpers - workbenchMentionUtils.tsx (79L): prompt mention parsing and token rendering - WorkbenchPromptPreview.tsx (87L): ReferencePreview, PromptPreviewLayer components - WorkbenchSelectChips.tsx (263L): SelectChip, CompoundSelectChip, InlineOptionChip All extracted code is imported back via ES module imports — no logic changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
2.5 KiB
TypeScript
88 lines
2.5 KiB
TypeScript
import { FileTextOutlined, SoundOutlined } from "@ant-design/icons";
|
|
import type { PromptMentionItem, PromptMentionTokenRange, ReferenceItem } from "./workbenchConstants";
|
|
import { renderPromptPreviewNodes, getPromptMentionTokenRanges } from "./workbenchMentionUtils";
|
|
|
|
export { getPromptMentionTokenRanges };
|
|
|
|
export function findPromptMentionRangeInside(index: number, ranges: PromptMentionTokenRange[]) {
|
|
return ranges.find((range) => index > range.start && index < range.end);
|
|
}
|
|
|
|
export function findPromptMentionRangeOverlap(start: number, end: number, ranges: PromptMentionTokenRange[]) {
|
|
return ranges.find((range) => start < range.end && end > range.start);
|
|
}
|
|
|
|
export function ReferenceInlinePreview({
|
|
item,
|
|
}: {
|
|
item: Pick<ReferenceItem, "kind" | "name" | "previewUrl">;
|
|
}) {
|
|
if ((item.kind === "image" || item.kind === "video") && item.previewUrl) {
|
|
return item.kind === "video" ? (
|
|
<video src={item.previewUrl} muted playsInline />
|
|
) : (
|
|
<img src={item.previewUrl} alt={item.name} loading="lazy" />
|
|
);
|
|
}
|
|
|
|
return item.kind === "audio" ? <SoundOutlined /> : <FileTextOutlined />;
|
|
}
|
|
|
|
export function ReferencePreview({
|
|
item,
|
|
label,
|
|
}: {
|
|
item: Pick<ReferenceItem, "kind" | "name" | "previewUrl">;
|
|
label?: string;
|
|
}) {
|
|
if ((item.kind === "image" || item.kind === "video") && item.previewUrl) {
|
|
return item.kind === "video" ? (
|
|
<video src={item.previewUrl} muted playsInline />
|
|
) : (
|
|
<img src={item.previewUrl} alt={item.name} loading="lazy" />
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span className="wb-composer__ref-icon">
|
|
{item.kind === "audio" ? <SoundOutlined /> : <FileTextOutlined />}
|
|
{label ? <span>{label}</span> : null}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function PromptPreviewLayer({
|
|
text,
|
|
items,
|
|
onTokenPointerDown,
|
|
}: {
|
|
text: string;
|
|
items: PromptMentionItem[];
|
|
onTokenPointerDown?: (index: number) => void;
|
|
}) {
|
|
const nodes = renderPromptPreviewNodes(text, items);
|
|
if (nodes.length === 0) return null;
|
|
|
|
return (
|
|
<div
|
|
className="wb-composer__highlight"
|
|
aria-hidden="true"
|
|
onPointerDown={(event) => {
|
|
const target =
|
|
event.target instanceof Element
|
|
? event.target.closest<HTMLElement>(".wb-composer__mention-inline-chip")
|
|
: null;
|
|
if (!target) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const tokenEnd = Number(target.dataset.tokenEnd);
|
|
if (Number.isFinite(tokenEnd)) {
|
|
onTokenPointerDown?.(tokenEnd);
|
|
}
|
|
}}
|
|
>
|
|
{nodes}
|
|
</div>
|
|
);
|
|
} |