"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 (
{segments.map((segment, index) => {
if (segment.type === "text") {
return (
{segment.value}
);
}
if (variant === "image") {
return (
{segment.source.imageUrl ? (
) : null}
{segment.source.label}
);
}
return (
{segment.source.label}
);
})}
);
}
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,
};
}