Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

287 lines
14 KiB
TypeScript

"use client";
import { useId } from "react";
import type { SyntaxHighlighterProps } from "@assistant-ui/react-streamdown";
import { CheckIcon, CopyIcon, DownloadIcon } from "lucide-react";
import { PreviewCard } from "@base-ui/react/preview-card";
import { useCopyToClipboard } from "@assistant-ui/ui/hooks/use-copy-to-clipboard";
import { analytics } from "@/lib/analytics";
import {
trackXuluxDownload,
useXuluxAnalytics,
withXuluxContext,
} from "@/lib/xulux/analytics-context";
export type OpenInData = {
title: string;
downloadUrl?: string;
prompt?: string;
};
function isValidOpenInUrl(url: unknown): url is string {
if (typeof url !== "string" || url.trim().length === 0) return false;
if (url.includes("<") || url.includes(">")) return false;
if (/from-tool-result|placeholder|<downloadurl/i.test(url)) return false;
if (url.startsWith("/")) return true;
try {
const parsed = new URL(url);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
function toAbsoluteDownloadUrl(url: string): string {
if (/^https?:\/\//i.test(url)) return url;
if (typeof window === "undefined") return url;
return new URL(url, window.location.origin).href;
}
function getDocsAppendix(): string {
const docsIndexUrl =
typeof window !== "undefined"
? `${window.location.origin}/llms.txt`
: "https://www.assistant-ui.com/llms.txt";
return (
`Before implementing, read ${docsIndexUrl} to discover relevant documentation pages. ` +
"Traverse the index and read the specific pages you need (installation, architecture, runtimes, components) so your setup matches current assistant-ui APIs. " +
"Use the assistant-ui CLI for scaffolding — do not manually create projects with create-next-app."
);
}
function buildPrompt(data: OpenInData): string {
const sections: string[] = [];
if (data.prompt?.trim()) sections.push(data.prompt.trim());
const downloadUrl = isValidOpenInUrl(data.downloadUrl)
? data.downloadUrl
: undefined;
if (downloadUrl) {
sections.push(`Download: ${toAbsoluteDownloadUrl(downloadUrl)}`);
}
sections.push(getDocsAppendix());
return sections.join("\n\n");
}
function ClaudeLogo() {
return (
<svg viewBox="0 0 24 24" className="size-3.5 fill-current" aria-hidden>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"
/>
</svg>
);
}
function CodexLogo() {
return (
<svg viewBox="0 0 24 24" className="size-3.5 fill-current" aria-hidden>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"
/>
</svg>
);
}
function CursorLogo() {
return (
<svg viewBox="0 0 24 24" className="size-3.5 fill-current" aria-hidden>
<path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23" />
</svg>
);
}
function ConductorLogo() {
return (
<svg viewBox="0 0 115 174" className="size-3.5 fill-current" aria-hidden>
<path d="M4.57422 63.6992H22.373V37.251H4.57422C3.58785 37.2511 2.78711 38.0517 2.78711 39.0381V61.9121C2.78725 62.8984 3.58794 63.6991 4.57422 63.6992Z" />
<path d="M36.5977 63.6992H18.7988V37.251H36.5977C37.584 37.2511 38.3848 38.0517 38.3848 39.0381V61.9121C38.3846 62.8984 37.5839 63.6991 36.5977 63.6992Z" />
<path d="M4.57422 100.297H22.373V73.8486H4.57422C3.58785 73.8488 2.78711 74.6493 2.78711 75.6357V98.5098C2.78725 99.496 3.58794 100.297 4.57422 100.297Z" />
<path d="M36.5977 100.297H18.7988V73.8486H36.5977C37.584 73.8488 38.3848 74.6493 38.3848 75.6357V98.5098C38.3846 99.496 37.5839 100.297 36.5977 100.297Z" />
<path d="M4.57422 136.896H22.373V110.447H4.57422C3.58785 110.447 2.78711 111.248 2.78711 112.234V135.108C2.78725 136.095 3.58794 136.895 4.57422 136.896Z" />
<path d="M36.5977 136.896H18.7988V110.447H36.5977C37.584 110.447 38.3848 111.248 38.3848 112.234V135.108C38.3846 136.095 37.5839 136.895 36.5977 136.896Z" />
<path d="M22.873 173.493H40.6719V147.045H22.873C21.8867 147.045 21.0859 147.846 21.0859 148.832V171.706C21.0861 172.692 21.8868 173.493 22.873 173.493Z" />
<path d="M37.0967 173.493V147.045H58.9707V173.493H37.0967Z" />
<path d="M55.3955 173.493V147.045H77.2695V173.493H55.3955Z" />
<path d="M91.4941 173.493H73.6953V147.045H91.4941C92.4805 147.045 93.2812 147.846 93.2812 148.832V171.706C93.2811 172.692 92.4804 173.493 91.4941 173.493Z" />
<path d="M77.7695 136.896H95.5684V110.447H77.7695C76.7832 110.447 75.9824 111.248 75.9824 112.234V135.108C75.9826 136.095 76.7833 136.895 77.7695 136.896Z" />
<path d="M109.793 136.896H91.9941V110.447H109.793C110.779 110.447 111.58 111.248 111.58 112.234V135.108C111.58 136.095 110.779 136.895 109.793 136.896Z" />
<path d="M22.873 27.1006H40.6719V0.652344H22.873C21.8867 0.652488 21.0859 1.45305 21.0859 2.43945V25.3135C21.0861 26.2998 21.8868 27.1004 22.873 27.1006Z" />
<path d="M37.0967 27.1006V0.652344H58.9707V27.1006H37.0967Z" />
<path d="M55.3955 27.1006V0.652344H77.2695V27.1006H55.3955Z" />
<path d="M73.6963 27.1006V0.652344H95.5703V27.1006H73.6963Z" />
<path d="M109.793 27.1006H91.9941V0.652344H109.793C110.779 0.652488 111.58 1.45305 111.58 2.43945V25.3135C111.58 26.2998 110.779 27.1004 109.793 27.1006Z" />
<path d="M77.7695 63.6992H95.5684V37.251H77.7695C76.7832 37.2511 75.9824 38.0517 75.9824 39.0381V61.9121C75.9826 62.8984 76.7833 63.6991 77.7695 63.6992Z" />
<path d="M109.793 63.6992H91.9941V37.251H109.793C110.779 37.2511 111.58 38.0517 111.58 39.0381V61.9121C111.58 62.8984 110.779 63.6991 109.793 63.6992Z" />
</svg>
);
}
function ChatGPTLogo() {
return (
<svg viewBox="0 0 24 24" className="size-3.5 fill-current" aria-hidden>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
);
}
function VSCodeLogo() {
return (
<svg viewBox="0 0 24 24" className="size-3.5 fill-current" aria-hidden>
<path d="M16.2 2.5 7.4 10.6 3.2 7.4 1.5 8.2v7.6l1.7.8 4.2-3.2 8.8 8.1 5.3-2.2V4.7l-5.3-2.2ZM4 10.5l2 1.5-2 1.5v-3Zm4.2 1.5 7-5.3v10.6l-7-5.3Zm10.3 5.4-1.5.6V6l1.5.6v10.8Z" />
</svg>
);
}
const PLATFORM_LOGOS = [
{ id: "claude", Logo: ClaudeLogo },
{ id: "codex", Logo: CodexLogo },
{ id: "vscode", Logo: VSCodeLogo },
{ id: "cursor", Logo: CursorLogo },
{ id: "conductor", Logo: ConductorLogo },
{ id: "chatgpt", Logo: ChatGPTLogo },
] as const;
export function OpenInCard({
title,
downloadUrl,
prompt: agentPrompt,
}: OpenInData) {
const analyticsCtx = useXuluxAnalytics();
const prompt = buildPrompt({
title,
...(downloadUrl ? { downloadUrl } : {}),
...(agentPrompt ? { prompt: agentPrompt } : {}),
});
const { isCopied, copyToClipboard } = useCopyToClipboard();
const validDownloadUrl = isValidOpenInUrl(downloadUrl)
? downloadUrl
: undefined;
const hasDownload = Boolean(validDownloadUrl);
const promptPreviewId = useId();
return (
<div className="border-border bg-muted/30 my-4 rounded-lg border px-4 py-3 text-sm shadow-sm">
{/* Row 1: headline + decorative logos */}
<div className="mb-2.5 flex items-center gap-2.5">
<span className="text-foreground text-[13px] font-semibold">
Continue in your coding agent
</span>
<span className="bg-border/70 h-4 w-px shrink-0" aria-hidden />
<span className="flex items-center gap-2">
{PLATFORM_LOGOS.map(({ id, Logo }) => (
<span
key={id}
className="text-muted-foreground/50 inline-flex"
aria-hidden
>
<Logo />
</span>
))}
</span>
</div>
{/* Row 2: actions */}
<div className="flex flex-wrap items-center gap-2.5">
{hasDownload && (
<>
<a
href={toAbsoluteDownloadUrl(validDownloadUrl!)}
download
aria-label="Download starter"
onClick={() =>
trackXuluxDownload(analyticsCtx, {
surface: "open_in_card",
download_type: "demo",
})
}
className="border-border bg-background hover:bg-muted inline-flex size-8 items-center justify-center rounded-md border transition-colors"
>
<DownloadIcon className="size-4" />
</a>
<span className="text-muted-foreground/70 text-xs">or</span>
</>
)}
<PreviewCard.Root>
<PreviewCard.Trigger
delay={150}
closeDelay={150}
render={
<button
type="button"
onClick={() => {
copyToClipboard(prompt);
analytics.xulux.converted(
withXuluxContext(analyticsCtx, {
action: "copy_prompt",
surface: "open_in_card",
}),
);
}}
className="bg-foreground text-background hover:bg-foreground/90 inline-flex h-8 items-center gap-1.5 rounded-md px-3.5 text-xs font-medium transition-colors"
aria-describedby={promptPreviewId}
/>
}
>
{isCopied ? (
<CheckIcon className="size-3.5" />
) : (
<CopyIcon className="size-3.5" />
)}
{isCopied ? "Copied!" : "Copy prompt"}
</PreviewCard.Trigger>
<PreviewCard.Portal>
<PreviewCard.Positioner
side="bottom"
align="start"
sideOffset={8}
collisionPadding={12}
>
<PreviewCard.Popup
id={promptPreviewId}
role="tooltip"
className="border-border bg-popover text-popover-foreground z-[2147483647] w-[min(24rem,calc(100vw-2rem))] rounded-lg border p-3 text-xs shadow-lg"
>
<div className="text-muted-foreground mb-2 font-medium">
Prompt preview
</div>
<pre className="max-h-56 overflow-auto font-mono leading-normal break-words whitespace-pre-wrap">
{prompt}
</pre>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
</div>
</div>
);
}
export function OpenInSyntaxHighlighter({ code }: SyntaxHighlighterProps) {
let data: OpenInData;
try {
data = JSON.parse(code) as OpenInData;
} catch {
return null;
}
if (!data.title) return null;
const downloadUrl = isValidOpenInUrl(data.downloadUrl)
? data.downloadUrl
: undefined;
return (
<OpenInCard
title={data.title}
{...(downloadUrl ? { downloadUrl } : {})}
{...(data.prompt ? { prompt: data.prompt } : {})}
/>
);
}