Fix slack links (#4254)

* fix slack links

* updates

* k

* nit improvements
This commit is contained in:
pablonyx 2025-03-11 12:58:15 -07:00 committed by GitHub
parent ecbd4eb1ad
commit 4e70f99214
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 102 additions and 37 deletions

View File

@ -3,7 +3,7 @@
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useContext, useState, useRef, useLayoutEffect } from "react";
import { ChevronDownIcon } from "@/components/icons/icons";
import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown";
import MinimalMarkdown from "@/components/chat/MinimalMarkdown";
export function ChatBanner() {
const settings = useContext(SettingsContext);

View File

@ -109,7 +109,6 @@ import {
} from "@/components/resizable/constants";
import FixedLogo from "../../components/logo/FixedLogo";
import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
import {
@ -138,6 +137,7 @@ import { useSidebarShortcut } from "@/lib/browserUtilities";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import { ChatSearchModal } from "./chat_search/ChatSearchModal";
import { ErrorBanner } from "./message/Resubmit";
import MinimalMarkdown from "@/components/chat/MinimalMarkdown";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;

View File

@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { useContext, useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { transformLinkUri } from "@/lib/utils";
const ALL_USERS_INITIAL_POPUP_FLOW_COMPLETED =
"allUsersInitialPopupFlowCompleted";
@ -44,23 +45,26 @@ export function ChatPopup() {
return (
<Modal width="w-3/6 xl:w-[700px]" title={popupTitle}>
<>
<ReactMarkdown
className="prose text-text-800 dark:text-neutral-100 max-w-full"
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-link hover:text-link-hover"
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => <p {...props} className="text-sm" />,
}}
remarkPlugins={[remarkGfm]}
>
{popupContent}
</ReactMarkdown>
<div className="overflow-y-auto max-h-[90vh] py-8 px-4 text-left">
<ReactMarkdown
className="prose text-text-800 dark:text-neutral-100 max-w-full"
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-link hover:text-link-hover"
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => <p {...props} className="text-sm" />,
}}
remarkPlugins={[remarkGfm]}
urlTransform={transformLinkUri}
>
{popupContent}
</ReactMarkdown>
</div>
{showConsentError && (
<p className="text-red-500 text-sm mt-2">

View File

@ -53,6 +53,7 @@ import { copyAll, handleCopy } from "./copyingUtils";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
import { ErrorBanner, Resubmit } from "./Resubmit";
import { transformLinkUri } from "@/lib/utils";
export const AgenticMessage = ({
isStreamingQuestions,
@ -336,6 +337,7 @@ export const AgenticMessage = ({
}}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[[rehypePrism, { ignoreMissing: true }], rehypeKatex]}
urlTransform={transformLinkUri}
>
{finalAlternativeContent}
</ReactMarkdown>
@ -349,6 +351,7 @@ export const AgenticMessage = ({
components={markdownComponents}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[[rehypePrism, { ignoreMissing: true }], rehypeKatex]}
urlTransform={transformLinkUri}
>
{streamedContent +
(!isComplete && !secondLevelGenerating ? " [*]() " : "")}

View File

@ -160,8 +160,9 @@ export const MemoizedLink = memo(
const handleMouseDown = () => {
let url = href || rest.children?.toString();
if (url && !url.startsWith("http://") && !url.startsWith("https://")) {
// Try to construct a valid URL
if (url && !url.includes("://")) {
// Only add https:// if the URL doesn't already have a protocol
const httpsUrl = `https://${url}`;
try {
new URL(httpsUrl);

View File

@ -71,6 +71,7 @@ import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
import { copyAll, handleCopy } from "./copyingUtils";
import { transformLinkUri } from "@/lib/utils";
const TOOLS_WITH_CUSTOM_HANDLING = [
SEARCH_TOOL_NAME,
@ -348,7 +349,7 @@ export const AIMessage = ({
a: anchorCallback,
p: paragraphCallback,
b: ({ node, className, children }: any) => {
return <span className={className}>||||{children}</span>;
return <span className={className}>{children}</span>;
},
code: ({ node, className, children }: any) => {
const codeText = extractCodeText(
@ -381,6 +382,7 @@ export const AIMessage = ({
components={markdownComponents}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[[rehypePrism, { ignoreMissing: true }], rehypeKatex]}
urlTransform={transformLinkUri}
>
{finalContent}
</ReactMarkdown>

View File

@ -16,15 +16,15 @@ import ReactMarkdown from "react-markdown";
import { MemoizedAnchor } from "./MemoizedTextComponents";
import { MemoizedParagraph } from "./MemoizedTextComponents";
import { extractCodeText, preprocessLaTeX } from "./codeUtils";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import { CodeBlock } from "./CodeBlock";
import { CheckIcon, ChevronDown } from "lucide-react";
import { PHASE_MIN_MS, useStreamingMessages } from "./StreamingMessages";
import { CirclingArrowIcon } from "@/components/icons/icons";
import { handleCopy } from "./copyingUtils";
import { transformLinkUri } from "@/lib/utils";
export const StatusIndicator = ({ status }: { status: ToggleState }) => {
return (
@ -301,6 +301,7 @@ const SubQuestionDisplay: React.FC<{
components={markdownComponents}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
urlTransform={transformLinkUri}
>
{finalContent}
</ReactMarkdown>

View File

@ -33,6 +33,8 @@ import Link from "next/link";
import { CheckboxField } from "@/components/ui/checkbox";
import { CheckedState } from "@radix-ui/react-checkbox";
import { transformLinkUri } from "@/lib/utils";
export function SectionHeader({
children,
}: {
@ -432,6 +434,7 @@ export const MarkdownFormField = ({
<ReactMarkdown
className="prose dark:prose-invert"
remarkPlugins={[remarkGfm]}
urlTransform={transformLinkUri}
>
{field.value}
</ReactMarkdown>

View File

@ -4,19 +4,26 @@ import {
MemoizedLink,
MemoizedParagraph,
} from "@/app/chat/message/MemoizedTextComponents";
import React, { useMemo } from "react";
import React, { useMemo, CSSProperties } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypePrism from "rehype-prism-plus";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
import { transformLinkUri } from "@/lib/utils";
interface MinimalMarkdownProps {
content: string;
className?: string;
style?: CSSProperties;
}
export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({
export default function MinimalMarkdown({
content,
className = "",
}) => {
style,
}: MinimalMarkdownProps) {
const markdownComponents = useMemo(
() => ({
a: MemoizedLink,
@ -34,12 +41,16 @@ export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({
);
return (
<ReactMarkdown
className={`w-full text-wrap break-word prose dark:prose-invert ${className}`}
components={markdownComponents}
remarkPlugins={[remarkGfm]}
>
{content}
</ReactMarkdown>
<div style={style || {}} className={`${className}`}>
<ReactMarkdown
className="prose dark:prose-invert max-w-full text-sm break-words"
components={markdownComponents}
rehypePlugins={[[rehypePrism, { ignoreMissing: true }], rehypeKatex]}
remarkPlugins={[remarkGfm, remarkMath]}
urlTransform={transformLinkUri}
>
{content}
</ReactMarkdown>
</div>
);
};
}

View File

@ -10,7 +10,7 @@ import {
} from "@/components/ui/dialog";
import { Download, XIcon, ZoomIn, ZoomOut } from "lucide-react";
import { OnyxDocument } from "@/lib/search/interfaces";
import { MinimalMarkdown } from "./MinimalMarkdown";
import MinimalMarkdown from "@/components/chat/MinimalMarkdown";
interface TextViewProps {
presentingDocument: OnyxDocument;

View File

@ -1,6 +1,6 @@
import { Quote } from "@/lib/search/interfaces";
import { ResponseSection, StatusOptions } from "./ResponseSection";
import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown";
import MinimalMarkdown from "@/components/chat/MinimalMarkdown";
const TEMP_STRING = "__$%^TEMP$%^__";

View File

@ -91,3 +91,17 @@ export const NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK =
export const NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY =
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
// Add support for custom URL protocols in markdown links
export const ALLOWED_URL_PROTOCOLS = [
"http:",
"https:",
"mailto:",
"tel:",
"slack:",
"vscode:",
"file:",
"sms:",
"spotify:",
"zoommtg:",
];

View File

@ -1,5 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { ALLOWED_URL_PROTOCOLS } from "./constants";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@ -8,3 +9,28 @@ export function cn(...inputs: ClassValue[]) {
export const truncateString = (str: string, maxLength: number) => {
return str.length > maxLength ? str.slice(0, maxLength - 1) + "..." : str;
};
/**
* Custom URL transformer function for ReactMarkdown
* Allows specific protocols to be used in markdown links
* We use this with the urlTransform prop in ReactMarkdown
*/
export function transformLinkUri(href: string) {
if (!href) return href;
const url = href.trim();
try {
const parsedUrl = new URL(url);
if (
ALLOWED_URL_PROTOCOLS.some((protocol) =>
parsedUrl.protocol.startsWith(protocol)
)
) {
return url;
}
} catch (e) {
// If it's not a valid URL with protocol, return the original href
return href;
}
return href;
}