Save current image generation workspace
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import {
|
||||
parsePromptMentionUrl,
|
||||
type PromptMentionSource,
|
||||
} from "@/lib/canvas/prompt-mentions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type PromptSegment =
|
||||
| { type: "text"; value: string }
|
||||
| { type: "mention"; source: PromptMentionSource };
|
||||
|
||||
type PromptMentionPreviewProps = {
|
||||
prompt: string;
|
||||
mentionSources: PromptMentionSource[];
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
chipClassName?: string;
|
||||
variant?: "link" | "image";
|
||||
placeholder?: ReactNode;
|
||||
};
|
||||
|
||||
export function PromptMentionPreview({
|
||||
prompt,
|
||||
mentionSources,
|
||||
className,
|
||||
textClassName,
|
||||
chipClassName,
|
||||
variant = "link",
|
||||
placeholder,
|
||||
}: PromptMentionPreviewProps): ReactElement | null {
|
||||
const segments = splitPromptSegments(prompt, mentionSources);
|
||||
|
||||
if (!segments.length) {
|
||||
return placeholder ? <>{placeholder}</> : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("whitespace-pre-wrap break-words leading-6", className)}>
|
||||
{segments.map((segment, index) => {
|
||||
if (segment.type === "text") {
|
||||
return (
|
||||
<span
|
||||
key={`text-${index}`}
|
||||
className={cn("text-zinc-900", textClassName)}
|
||||
>
|
||||
{segment.value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "image") {
|
||||
return (
|
||||
<span
|
||||
key={`mention-${segment.source.nodeId}-${index}`}
|
||||
className={cn(
|
||||
"mx-0.5 inline-flex max-w-full align-middle items-center gap-1 rounded-full border border-zinc-200 bg-white px-1.5 py-0.5 text-xs font-medium text-zinc-800 shadow-sm",
|
||||
chipClassName,
|
||||
)}
|
||||
>
|
||||
<span className="flex size-4 shrink-0 items-center justify-center overflow-hidden rounded-full bg-zinc-100">
|
||||
{segment.source.imageUrl ? (
|
||||
<img
|
||||
alt={segment.source.description}
|
||||
className="h-full w-full object-cover"
|
||||
src={segment.source.imageUrl}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sky-700">{segment.source.label}</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
key={`mention-${segment.source.nodeId}-${index}`}
|
||||
className={cn(
|
||||
"my-0.5 inline-flex h-7 max-w-full align-middle items-center rounded-md border border-sky-200 bg-sky-50 px-2 text-xs font-medium text-sky-800",
|
||||
chipClassName,
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{segment.source.label}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function splitPromptSegments(
|
||||
prompt: string,
|
||||
mentionSources: PromptMentionSource[],
|
||||
): PromptSegment[] {
|
||||
if (!prompt) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sortedSources = [...mentionSources].sort(
|
||||
(left, right) =>
|
||||
Math.max(right.token.length, right.alias.length) -
|
||||
Math.max(left.token.length, left.alias.length),
|
||||
);
|
||||
const segments: PromptSegment[] = [];
|
||||
|
||||
let cursor = 0;
|
||||
while (cursor < prompt.length) {
|
||||
const markdownMatch = parseMarkdownMentionAt(prompt, cursor);
|
||||
const matchedSource = markdownMatch
|
||||
? sortedSources.find((source) => source.nodeId === markdownMatch.nodeId)
|
||||
: sortedSources.find((source) => prompt.startsWith(source.alias, cursor));
|
||||
|
||||
if (!matchedSource) {
|
||||
let nextCursor = cursor + 1;
|
||||
while (nextCursor < prompt.length) {
|
||||
const hasMentionAhead =
|
||||
Boolean(parseMarkdownMentionAt(prompt, nextCursor)) ||
|
||||
sortedSources.some((source) => prompt.startsWith(source.alias, nextCursor));
|
||||
if (hasMentionAhead) break;
|
||||
nextCursor += 1;
|
||||
}
|
||||
|
||||
segments.push({
|
||||
type: "text",
|
||||
value: prompt.slice(cursor, nextCursor),
|
||||
});
|
||||
cursor = nextCursor;
|
||||
continue;
|
||||
}
|
||||
|
||||
segments.push({
|
||||
type: "mention",
|
||||
source: matchedSource,
|
||||
});
|
||||
cursor += markdownMatch?.length ?? matchedSource.alias.length;
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function parseMarkdownMentionAt(prompt: string, cursor: number) {
|
||||
const match = /^\[([^\]]+)\]\(([^)]+)\)/.exec(prompt.slice(cursor));
|
||||
if (!match) return null;
|
||||
|
||||
const nodeId = parsePromptMentionUrl(match[2]);
|
||||
if (!nodeId) return null;
|
||||
|
||||
return {
|
||||
label: match[1],
|
||||
nodeId,
|
||||
length: match[0].length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user