8starlabs-ui
uiopen-in-chatsource: ok · hit
Open In Chat
A component that opens an LLM chat with a pre-filled prompt.
npx shadcn add "https://ui.8starlabs.com/r/open-in-chat.json"free
Where it goes
chat
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
https://ui.8starlabs.com/r/button.jsonhttps://ui.8starlabs.com/r/dropdown-menu.jsonhttps://ui.8starlabs.com/r/tooltip.json
Files2 · source cached 10/14/2026, 3:00:54 PM
registry/8starlabs-ui/blocks/open-in-chat.tsx
"use client";
import * as React from "react";
import { ChevronDown, ExternalLink } from "lucide-react";
import {
ClaudeIcon,
GrokIcon,
OpenAIIcon,
PerplexityIcon,
V0Icon
} from "@/registry/8starlabs-ui/ui/ai-icons";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/8starlabs-ui/blocks/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/registry/8starlabs-ui/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/registry/8starlabs-ui/ui/tooltip";
export type AiLinkName = "ChatGPT" | "Claude" | "Perplexity" | "Grok" | "v0";
export type OpenInChatLayout = "dropdown" | "horizontal";
export type OpenInChatIcon = React.ElementType<React.ComponentProps<"svg">>;
export type AiLinkData = {
baseUrl: string;
paramName?: string;
icon: OpenInChatIcon;
};
export type OpenInChatContextValue = {
prompt: string;
layout: OpenInChatLayout;
};
export type OpenInChatBaseProps = Omit<
React.ComponentPropsWithoutRef<"div">,
"align"
> & {
prompt: string;
triggerLabel?: React.ReactNode;
triggerClassName?: string;
contentClassName?: string;
align?: React.ComponentProps<typeof DropdownMenuContent>["align"];
side?: React.ComponentProps<typeof DropdownMenuContent>["side"];
};
export type OpenInChatDropdownProps = OpenInChatBaseProps & {
layout?: "dropdown";
trigger?: React.ReactElement | null;
};
export type OpenInChatHorizontalProps = OpenInChatBaseProps & {
layout: "horizontal";
trigger?: never;
};
export type OpenInChatProps =
| OpenInChatDropdownProps
| OpenInChatHorizontalProps;
export type OpenInChatLinkProps = {
baseUrl: string;
paramName?: string;
icon: OpenInChatIcon;
label: string;
prompt?: string;
className?: string;
};
const OpenInChatContext = React.createContext<OpenInChatContextValue | null>(
null
);
export const aiLinksDataMap: Record<AiLinkName, AiLinkData> = {
ChatGPT: {
baseUrl: "https://chatgpt.com/",
icon: OpenAIIcon
},
Claude: {
baseUrl: "https://claude.ai/new",
icon: ClaudeIcon
},
Perplexity: {
baseUrl: "https://www.perplexity.ai/search/new",
icon: PerplexityIcon
},
Grok: {
baseUrl: "https://grok.com/",
icon: GrokIcon
},
v0: {
baseUrl: "https://v0.app/chat",
icon: V0Icon
}
};
export function getPromptUrl(
baseUrl: string,
prompt: string,
paramName: string = "q"
) {
const url = new URL(baseUrl);
url.searchParams.set(paramName, prompt);
return url.toString();
}
export function useOpenInChat() {
const context = React.useContext(OpenInChatContext);
if (!context) {
throw new Error("OpenInChat links must be used inside OpenInChat.");
}
return context;
}
export default function OpenInChat({
prompt,
children,
layout = "dropdown",
trigger,
triggerLabel = "Open in chat",
className,
triggerClassName,
contentClassName,
align = "end",
side,
...props
}: OpenInChatProps) {
const contextValue = React.useMemo(
() => ({ prompt, layout }),
[prompt, layout]
);
const items =
React.Children.count(children) > 0
? children
: (Object.entries(aiLinksDataMap) as [AiLinkName, AiLinkData][]).map(
([label, dat], idx) => (
<OpenInChatLink
label={label}
baseUrl={dat.baseUrl}
icon={dat.icon}
paramName={dat.paramName}
key={idx}
/>
)
);
if (layout === "horizontal") {
return (
<TooltipProvider>
<OpenInChatContext.Provider value={contextValue}>
<div
{...props}
className={cn(
"border-border bg-card text-muted-foreground inline-flex w-fit items-center justify-center gap-1.5 rounded-md border px-2 py-1 text-sm shadow-xs",
triggerClassName,
className
)}
>
{triggerLabel ? <span>{triggerLabel}</span> : null}
{items}
</div>
</OpenInChatContext.Provider>
</TooltipProvider>
);
}
return (
<OpenInChatContext.Provider value={contextValue}>
<div {...props} className={cn("inline-flex", className)}>
<DropdownMenu>
<DropdownMenuTrigger
render={
trigger ?? (
<Button
type="button"
variant="outline"
className={cn("gap-2", triggerClassName)}
>
{triggerLabel}
<ChevronDown
className="size-4 opacity-70"
aria-hidden="true"
/>
</Button>
)
}
/>
<DropdownMenuContent
align={align}
side={side}
className={cn("w-56", contentClassName)}
>
{items}
</DropdownMenuContent>
</DropdownMenu>
</div>
</OpenInChatContext.Provider>
);
}
export function OpenInChatLink({
label,
prompt,
className,
baseUrl,
paramName = "q",
icon
}: OpenInChatLinkProps) {
const { prompt: contextPrompt, layout } = useOpenInChat();
const href = getPromptUrl(baseUrl, prompt ?? contextPrompt, paramName);
const Icon = icon;
if (layout === "horizontal") {
return (
<Tooltip>
<TooltipTrigger
render={
<a
href={href}
target="_blank"
rel="noreferrer"
aria-label={`Open in ${label}`}
className={cn(
"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-6 shrink-0 items-center justify-center rounded-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
className
)}
>
<Icon className="size-4" aria-hidden="true" />
</a>
}
/>
<TooltipContent sideOffset={6}>Open in {label}</TooltipContent>
</Tooltip>
);
}
return (
<DropdownMenuItem
className={cn("cursor-pointer", className)}
render={
<a href={href} target="_blank" rel="noreferrer">
<Icon className="size-4" aria-hidden="true" />
<span className="flex-1">Open in {label}</span>
<ExternalLink className="size-4 opacity-60" aria-hidden="true" />
</a>
}
/>
);
}
export type OpenInChatProviderLinkProps = Pick<
OpenInChatLinkProps,
"prompt" | "className"
>;
export function OpenInChatGPT(params: OpenInChatProviderLinkProps) {
const dat = aiLinksDataMap.ChatGPT;
return (
<OpenInChatLink
label="ChatGPT"
baseUrl={dat.baseUrl}
icon={dat.icon}
paramName={dat.paramName}
{...params}
/>
);
}
export function OpenInClaude(params: OpenInChatProviderLinkProps) {
const dat = aiLinksDataMap.Claude;
return (
<OpenInChatLink
label="Claude"
baseUrl={dat.baseUrl}
icon={dat.icon}
paramName={dat.paramName}
{...params}
/>
);
}
export function OpenInPerplexity(params: OpenInChatProviderLinkProps) {
const dat = aiLinksDataMap.Perplexity;
return (
<OpenInChatLink
label="Perplexity"
baseUrl={dat.baseUrl}
icon={dat.icon}
paramName={dat.paramName}
{...params}
/>
);
}
export function OpenInGrok(params: OpenInChatProviderLinkProps) {
const dat = aiLinksDataMap.Grok;
return (
<OpenInChatLink
label="Grok"
baseUrl={dat.baseUrl}
icon={dat.icon}
paramName={dat.paramName}
{...params}
/>
);
}
export function OpenInV0(params: OpenInChatProviderLinkProps) {
const dat = aiLinksDataMap.v0;
return (
<OpenInChatLink
label="v0"
baseUrl={dat.baseUrl}
icon={dat.icon}
paramName={dat.paramName}
{...params}
/>
);
}
registry/8starlabs-ui/ui/ai-icons.tsx
import { forwardRef } from "react";
import { siClaude, siPerplexity, siV0 } from "simple-icons/icons";
type BrandIconProps = React.ComponentProps<"svg">;
type SimpleIconData = {
path: string;
};
function createSimpleIcon(icon: SimpleIconData, displayName: string) {
const Component = forwardRef<SVGSVGElement, BrandIconProps>((props, ref) => (
<svg ref={ref} viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d={icon.path} />
</svg>
));
Component.displayName = displayName;
return Component;
}
export const OpenAIIcon = forwardRef<SVGSVGElement, BrandIconProps>(
(props, ref) => (
<svg ref={ref} viewBox="0 0 24 24" fill="none" {...props}>
<path
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
fill="currentColor"
/>
</svg>
)
);
export const ClaudeIcon = createSimpleIcon(siClaude, "Claude Icon");
export const PerplexityIcon = createSimpleIcon(siPerplexity, "Perplexity Icon");
export const V0Icon = createSimpleIcon(siV0, "v0 Icon");
export const GrokIcon = forwardRef<SVGSVGElement, BrandIconProps>(
(props, ref) => (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
viewBox="68 74 376 362"
fill="currentColor"
{...props}
>
<path d="M213.235 306.019l178.976-180.002v.169l51.695-51.763c-.924 1.32-1.86 2.605-2.785 3.89-39.281 54.164-58.46 80.649-43.07 146.922l-.09-.101c10.61 45.11-.744 95.137-37.398 131.836-46.216 46.306-120.167 56.611-181.063 14.928l42.462-19.675c38.863 15.278 81.392 8.57 111.947-22.03 30.566-30.6 37.432-75.159 22.065-112.252-2.92-7.025-11.67-8.795-17.792-4.263l-124.947 92.341zm-25.786 22.437-.033.034L68.094 435.217c7.565-10.429 16.957-20.294 26.327-30.149 26.428-27.803 52.653-55.359 36.654-94.302-21.422-52.112-8.952-113.177 30.724-152.898 41.243-41.254 101.98-51.661 152.706-30.758 11.23 4.172 21.016 10.114 28.638 15.639l-42.359 19.584c-39.44-16.563-84.629-5.299-112.207 22.313-37.298 37.308-44.84 102.003-1.128 143.81z" />
</svg>
)
);