Skip to content

Commit

Permalink
Add Markdown File Viewer (#1384)
Browse files Browse the repository at this point in the history
* add markdown viewer

* fix "</body>"  showing as markdown content for a few seconds in markdown viewer

* fix indentation

* replace markdown library 'uiw/react-markdown-preview' with 'marked'

* update marked

* add comment to explain removal of anchor links

* fix formatting with pre-commit

---------

Co-authored-by: Pamela Fox <[email protected]>
  • Loading branch information
yuvalyaron and pamelafox authored Mar 18, 2024
1 parent 4ce59be commit 0a76219
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 5 deletions.
17 changes: 17 additions & 0 deletions app/frontend/package-lock.json

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

1 change: 1 addition & 0 deletions app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@fluentui/react-components": "^9.37.3",
"@fluentui/react-icons": "^2.0.221",
"@react-spring/web": "^9.7.3",
"marked": "^9.1.6",
"dompurify": "^3.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
23 changes: 18 additions & 5 deletions app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SupportingContent } from "../SupportingContent";
import { ChatAppResponse } from "../../api";
import { AnalysisPanelTabs } from "./AnalysisPanelTabs";
import { ThoughtProcess } from "./ThoughtProcess";
import { MarkdownViewer } from "../MarkdownViewer";
import { useMsal } from "@azure/msal-react";
import { getHeaders } from "../../api";
import { useLogin, getToken } from "../../authConfig";
Expand Down Expand Up @@ -53,6 +54,22 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh
fetchCitation();
}, []);

const renderFileViewer = () => {
if (!activeCitation) {
return null;
}

const fileExtension = activeCitation.split(".").pop()?.toLowerCase();
switch (fileExtension) {
case "png":
return <img src={citation} className={styles.citationImg} alt="Citation Image" />;
case "md":
return <MarkdownViewer src={activeCitation} />;
default:
return <iframe title="Citation" src={citation} width="100%" height={citationHeight} />;
}
};

return (
<Pivot
className={className}
Expand All @@ -78,11 +95,7 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh
headerText="Citation"
headerButtonProps={isDisabledCitationTab ? pivotItemDisabledStyle : undefined}
>
{activeCitation?.endsWith(".png") ? (
<img src={citation} className={styles.citationImg} />
) : (
<iframe title="Citation" src={citation} width="100%" height={citationHeight} />
)}
{renderFileViewer()}
</PivotItem>
</Pivot>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.downloadButton {
position: relative;
float: right;
}

.markdownViewer {
border-radius: 8px;
box-shadow:
#0000000d 0 0 0 1px,
#0000001a 0 2px 3px;
background-color: white;
margin: 20px 0;
}

.loading {
padding: 100px;
height: 100vh;
background-color: white;
}

.error {
height: 100vh;
background-color: white;
}

.markdown {
padding: 30px;
}

table {
border-collapse: collapse;
}

th,
td {
border: 1px solid #ddd;
padding: 8px;
}

tr:nth-child(even) {
background-color: #f6f8fa;
}

code {
display: block;
font-family: monospace;
padding: 10px;
background-color: #f6f8fa;
}
78 changes: 78 additions & 0 deletions app/frontend/src/components/MarkdownViewer/MarkdownViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useState, useEffect } from "react";
import { marked } from "marked";
import styles from "./MarkdownViewer.module.css";
import { Spinner, SpinnerSize, MessageBar, MessageBarType, Link, IconButton } from "@fluentui/react";

interface MarkdownViewerProps {
src: string;
}

export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ src }) => {
const [content, setContent] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);

/**
* Anchor links are not handled well by 'marked' and result in HTTP 404 errors as the URL they point to does not exist.
* This function removes them from the resulted HTML.
*/
const removeAnchorLinks = (html: string) => {
const ancorLinksRegex = /<a\s+(?:[^>]*?\s+)?href=['"](#[^"']*?)['"][^>]*?>/g;
return html.replace(ancorLinksRegex, "");
};

useEffect(() => {
const fetchMarkdown = async () => {
try {
const response = await fetch(src);

if (!response.ok) {
throw new Error("Failed loading markdown file.");
}

const markdownText = await response.text();
const parsedHtml = await marked.parse(markdownText);
const cleanedHtml = removeAnchorLinks(parsedHtml);
setContent(cleanedHtml);
} catch (error: any) {
setError(error);
} finally {
setIsLoading(false);
}
};

fetchMarkdown();
}, [src]);

return (
<div>
{isLoading ? (
<div className={`${styles.loading} ${styles.markdownViewer}`}>
<Spinner size={SpinnerSize.large} label="Loading file" />
</div>
) : error ? (
<div className={`${styles.error} ${styles.markdownViewer}`}>
<MessageBar messageBarType={MessageBarType.error} isMultiline={false}>
{error.message}
<Link href={src} download>
Download the file
</Link>
</MessageBar>
</div>
) : (
<div>
<IconButton
className={styles.downloadButton}
style={{ color: "black" }}
iconProps={{ iconName: "Save" }}
title="Save"
ariaLabel="Save"
href={src}
download
/>
<div className={`${styles.markdown} ${styles.markdownViewer}`} dangerouslySetInnerHTML={{ __html: content }} />
</div>
)}
</div>
);
};
1 change: 1 addition & 0 deletions app/frontend/src/components/MarkdownViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./MarkdownViewer";

0 comments on commit 0a76219

Please sign in to comment.