Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"humanize-duration": "3.34.0",
"jszip": "3.10.1",
"lexical": "0.44.0",
"linkifyjs": "4.3.3",
"lodash": "4.18.1",
"lucide-react": "0.555.0",
"monaco-editor": "0.55.1",
Expand Down
8 changes: 8 additions & 0 deletions site/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,62 @@ export const SystemMessageWithoutHookNotice: Story = {
},
};

export const UserPromptWithLinks: Story = {
args: {
...defaultArgs,
urlTransform: (url) =>
url.replace("http://localhost:3000", "https://proxy.example.com"),
parsedMessages: buildMessages([
{
...baseMessage,
id: 1,
role: "user",
content: [
{
type: "text",
text: "Please see https://coder.com/docs. Preview http://localhost:3000/app",
},
],
},
]),
},
play: async ({ canvasElement }) => {
Comment thread
ibetitsmike marked this conversation as resolved.
const canvas = within(canvasElement);
const docsLink = canvas.getByRole("link", {
name: "https://coder.com/docs",
});
expect(docsLink).toHaveAttribute("href", "https://coder.com/docs");
expect(docsLink).toHaveAttribute("target", "_blank");
expect(docsLink).toHaveAttribute(
"rel",
expect.stringContaining("noopener"),
);
const localhostLink = canvas.getByRole("link", {
name: "http://localhost:3000/app",
});
expect(localhostLink).toHaveAttribute(
"href",
"https://proxy.example.com/app",
);
expect(localhostLink).toHaveTextContent("http://localhost:3000/app");

let clickedHref: string | null = null;
const captureClick = (event: MouseEvent) => {
event.preventDefault();
if (event.target instanceof HTMLAnchorElement) {
clickedHref = event.target.getAttribute("href");
}
};
canvasElement.addEventListener("click", captureClick, true);
try {
await userEvent.click(localhostLink);
} finally {
canvasElement.removeEventListener("click", captureClick, true);
}
expect(clickedHref).toBe("https://proxy.example.com/app");
},
};

export const LifecycleHookNoticeOnUserMessage: Story = {
args: {
...defaultArgs,
Expand Down Expand Up @@ -1462,7 +1518,7 @@ export const UserMessageWithInlineFileRef: Story = {
end_line: 42,
content: "export const Button = ...",
},
{ type: "text", text: " to use the new API?" },
{ type: "text", text: " https://coder.com/docs" },
],
},
{
Expand All @@ -1482,7 +1538,9 @@ export const UserMessageWithInlineFileRef: Story = {
const canvas = within(canvasElement);
expect(canvas.getByText(/Button\.tsx/)).toBeInTheDocument();
expect(canvas.getByText(/Can you refactor/)).toBeInTheDocument();
expect(canvas.getByText(/to use the new API/)).toBeInTheDocument();
expect(
canvas.getByRole("link", { name: "https://coder.com/docs" }),
).toHaveAttribute("href", "https://coder.com/docs");
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ const ChatMessageItem = memo<{
<UserMessageContent
displayState={displayState}
markdown={parsed.markdown}
urlTransform={urlTransform}
isEditing={
messageId !== undefined && editingMessageId === messageId
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type FC, Fragment } from "react";
import type { FC } from "react";
import type { UrlTransform } from "streamdown";
import { cn } from "#/utils/cn";
import { Message, MessageContent } from "../ChatElements";
import { LinkifiedText } from "../ChatElements/LinkifiedText";
import { FileReferenceChip } from "../ChatMessageInput/FileReferenceChip";
import {
hasInlineContentAfter,
Expand Down Expand Up @@ -31,9 +33,16 @@ const renderUserInlineBlock = (
inlineParts: readonly InlinePart[],
block: UserInlineRenderBlock,
index: number,
urlTransform?: UrlTransform,
) => {
if (block.type === "response") {
return <Fragment key={index}>{block.text}</Fragment>;
return (
<LinkifiedText
key={index}
text={block.text}
urlTransform={urlTransform}
/>
);
}

return (
Expand All @@ -50,22 +59,27 @@ const renderUserInlineBlock = (
);
};

const renderUserInlineContent = (blocks: readonly UserInlineRenderBlock[]) => {
const renderUserInlineContent = (
blocks: readonly UserInlineRenderBlock[],
urlTransform?: UrlTransform,
) => {
const inlineParts = getInlineParts(blocks);
return blocks.map((block, index) =>
renderUserInlineBlock(inlineParts, block, index),
renderUserInlineBlock(inlineParts, block, index, urlTransform),
);
};

export const UserMessageContent: FC<{
displayState: MessageDisplayState;
markdown: string;
urlTransform?: UrlTransform;
isEditing?: boolean;
onImageClick?: (src: string) => void;
onTextFileClick?: (attachment: PreviewTextAttachment) => void;
}> = ({
displayState,
markdown,
urlTransform,
isEditing = false,
onImageClick,
onTextFileClick,
Expand All @@ -85,8 +99,16 @@ export const UserMessageContent: FC<{
{displayState.hasUserMessageBody && (
<span className="min-w-0 flex-1">
{displayState.userInlineContent.length > 0
? renderUserInlineContent(displayState.userInlineContent)
: markdown || ""}
? renderUserInlineContent(
displayState.userInlineContent,
urlTransform,
)
: markdown && (
<LinkifiedText
text={markdown}
urlTransform={urlTransform}
/>
)}
</span>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type React from "react";
import { Fragment } from "react";
import type { UrlTransform } from "streamdown";
import { splitTextForLinks } from "./linkify";

export const LinkifiedText: React.FC<{
text: string;
urlTransform?: UrlTransform;
}> = ({ text, urlTransform }) => {
const segments = splitTextForLinks(text);
if (!segments.some((segment) => segment.kind === "url")) {
return text;
}
return segments.map((segment, index) => {
if (segment.kind === "text") {
return <Fragment key={index}>{segment.value}</Fragment>;
}
const href =
urlTransform?.(segment.value, "href", {
type: "element",
tagName: "a",
properties: { href: segment.value },
children: [{ type: "text", value: segment.value }],
}) ?? segment.value;
return (
<a
key={index}
href={href}
target="_blank"
rel="noopener noreferrer"
className="font-[inherit] text-content-link underline underline-offset-2 hover:no-underline"
>
{segment.value}
</a>
);
});
};
Loading
Loading