From 0191f66e8d3e914f2fc44ec76b463ebbc567f35e Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Wed, 21 Aug 2024 15:04:42 -0700 Subject: [PATCH] Add code syntax highlighting and copy code (#520) --- .husky/pre-commit | 3 + .../ChatComponents/ChatSingleMessage.tsx | 23 ++-- src/components/Markdown/CodeBlock.tsx | 117 ++++++------------ .../Markdown/MemoizedReactMarkdown.tsx | 24 +++- src/proxyServer.ts | 20 ++- styles.css | 16 +-- 6 files changed, 86 insertions(+), 117 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index ea4bd58c..89bec27a 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,6 @@ # .husky/pre-commit prettier $(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') --write --ignore-unknown git update-index --again + +# Add linting +npm run lint diff --git a/src/components/ChatComponents/ChatSingleMessage.tsx b/src/components/ChatComponents/ChatSingleMessage.tsx index d14f01ea..caefd116 100644 --- a/src/components/ChatComponents/ChatSingleMessage.tsx +++ b/src/components/ChatComponents/ChatSingleMessage.tsx @@ -1,9 +1,8 @@ -import { BotIcon, CheckIcon, CopyClipboardIcon, UserIcon } from '@/components/Icons'; -import ReactMarkdown from '@/components/Markdown/MemoizedReactMarkdown'; -import { USER_SENDER } from '@/constants'; -import { ChatMessage } from '@/sharedState'; -import React, { useState } from 'react'; - +import { BotIcon, CheckIcon, CopyClipboardIcon, UserIcon } from "@/components/Icons"; +import MemoizedReactMarkdown from "@/components/Markdown/MemoizedReactMarkdown"; +import { USER_SENDER } from "@/constants"; +import { ChatMessage } from "@/sharedState"; +import React, { useState } from "react"; interface ChatSingleMessageProps { message: ChatMessage; @@ -27,12 +26,8 @@ const ChatSingleMessage: React.FC = ({ message }) => { }; return ( -
-
+
+
{message.sender === USER_SENDER ? : }
@@ -40,9 +35,7 @@ const ChatSingleMessage: React.FC = ({ message }) => { {message.sender === USER_SENDER ? ( {message.message} ) : ( - {message.message} + {message.message} )}
diff --git a/src/components/Markdown/CodeBlock.tsx b/src/components/Markdown/CodeBlock.tsx index 6c895f39..a8ad5ed7 100644 --- a/src/components/Markdown/CodeBlock.tsx +++ b/src/components/Markdown/CodeBlock.tsx @@ -1,10 +1,9 @@ -import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react'; -import { useTranslation } from 'next-i18next'; -import PropTypes from 'prop-types'; -import React, { FC, memo, useState } from 'react'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; - +import { IconCheck, IconClipboard } from "@tabler/icons-react"; +import { useTranslation } from "next-i18next"; +import PropTypes from "prop-types"; +import React, { FC, memo, useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; interface CodeProps { language?: string; @@ -16,43 +15,43 @@ interface languageMap { } export const programmingLanguages: languageMap = { - javascript: '.js', - python: '.py', - java: '.java', - c: '.c', - cpp: '.cpp', - 'c++': '.cpp', - 'c#': '.cs', - ruby: '.rb', - php: '.php', - swift: '.swift', - 'objective-c': '.m', - kotlin: '.kt', - typescript: '.ts', - go: '.go', - perl: '.pl', - rust: '.rs', - scala: '.scala', - haskell: '.hs', - lua: '.lua', - shell: '.sh', - sql: '.sql', - html: '.html', - css: '.css', + javascript: ".js", + python: ".py", + java: ".java", + c: ".c", + cpp: ".cpp", + "c++": ".cpp", + "c#": ".cs", + ruby: ".rb", + php: ".php", + swift: ".swift", + "objective-c": ".m", + kotlin: ".kt", + typescript: ".ts", + go: ".go", + perl: ".pl", + rust: ".rs", + scala: ".scala", + haskell: ".hs", + lua: ".lua", + shell: ".sh", + sql: ".sql", + html: ".html", + css: ".css", // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component }; export const generateRandomString = (length: number, lowercase = false) => { - const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0 - let result = ''; + const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0 + let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return lowercase ? result.toLowerCase() : result; }; -export const CodeBlock: FC = memo(({ language = 'text', value }) => { - const { t } = useTranslation('markdown'); +export const CodeBlock: FC = memo(({ language = "text", value }) => { + const { t } = useTranslation("markdown"); const [isCopied, setIsCopied] = useState(false); const copyToClipboard = () => { @@ -68,64 +67,22 @@ export const CodeBlock: FC = memo(({ language = 'text', value }) => { }, 2000); }); }; - const downloadAsFile = () => { - const fileExtension = programmingLanguages[language] || '.file'; - const suggestedFileName = `file-${generateRandomString( - 3, - true, - )}${fileExtension}`; - const fileName = window.prompt( - t('Enter file name') || '', - suggestedFileName, - ); - if (!fileName) { - // user pressed cancel on prompt - return; - } - - const blob = new Blob([value], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.download = fileName; - link.href = url; - link.style.display = 'none'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; return (
- {language} -
-
- + {value}
@@ -135,6 +92,6 @@ CodeBlock.propTypes = { language: PropTypes.string.isRequired, value: PropTypes.string.isRequired, }; -CodeBlock.displayName = 'CodeBlock'; +CodeBlock.displayName = "CodeBlock"; export default CodeBlock; diff --git a/src/components/Markdown/MemoizedReactMarkdown.tsx b/src/components/Markdown/MemoizedReactMarkdown.tsx index 327b6f6c..00efdb28 100644 --- a/src/components/Markdown/MemoizedReactMarkdown.tsx +++ b/src/components/Markdown/MemoizedReactMarkdown.tsx @@ -1,5 +1,23 @@ -import { FC, memo } from 'react'; -import ReactMarkdown, { Options } from 'react-markdown'; +import React, { FC, memo } from "react"; +import ReactMarkdown, { Options } from "react-markdown"; +import CodeBlock from "./CodeBlock"; // Adjust the import path as necessary + +const MemoizedReactMarkdown: FC = memo((props) => ( + + ) : ( + + {children} + + ); + }, + }} + /> +)); -const MemoizedReactMarkdown: FC = memo(ReactMarkdown); export default MemoizedReactMarkdown; diff --git a/src/proxyServer.ts b/src/proxyServer.ts index 40a035fb..49e5898b 100644 --- a/src/proxyServer.ts +++ b/src/proxyServer.ts @@ -1,10 +1,11 @@ +import { ChatModelDisplayNames } from "@/constants"; +import { CopilotSettings } from "@/settings/SettingsPage"; import cors from "@koa/cors"; import Koa from "koa"; import proxy from "koa-proxies"; -import { CopilotSettings } from "@/settings/SettingsPage"; -import { ChatModelDisplayNames } from "@/constants"; // There should only be 1 running proxy server at a time so keep it in upper scope +// eslint-disable-next-line @typescript-eslint/no-explicit-any let server: any; export class ProxyServer { @@ -22,10 +23,7 @@ export class ProxyServer { getProxyURL(currentModel: string): string { if (currentModel === ChatModelDisplayNames.CLAUDE) { return "https://api.anthropic.com/"; - } else if ( - this.settings.useOpenAILocalProxy && - this.settings.openAIProxyBaseUrl - ) { + } else if (this.settings.useOpenAILocalProxy && this.settings.openAIProxyBaseUrl) { return this.settings.openAIProxyBaseUrl; } @@ -51,12 +49,12 @@ export class ProxyServer { changeOrigin: true, logs: false, rewrite: rewritePaths ? (path) => path : undefined, - }), + }) ); // Create the server and attach error handling for "EADDRINUSE" if (server?.listening) { - return + return; } server = app.listen(this.port); server.on("error", (err: NodeJS.ErrnoException) => { @@ -71,7 +69,7 @@ export class ProxyServer { this.runningUrl = proxyBaseUrl; if (this.debug) { console.log( - `Proxy server running on http://localhost:${this.port}. Proxy to ${proxyBaseUrl}`, + `Proxy server running on http://localhost:${this.port}. Proxy to ${proxyBaseUrl}` ); } }); @@ -81,9 +79,7 @@ export class ProxyServer { let waitForClose: Promise | boolean = false; if (server) { if (this.debug) { - console.log( - `Attempting to stop proxy server proxying to ${this.runningUrl}...`, - ); + console.log(`Attempting to stop proxy server proxying to ${this.runningUrl}...`); } waitForClose = new Promise((resolve) => { server.on("close", () => { diff --git a/styles.css b/styles.css index 543daf5f..62ba7de0 100644 --- a/styles.css +++ b/styles.css @@ -62,7 +62,7 @@ If your plugin does not need CSS, delete this file. flex-direction: column; width: 100%; height: 100%; - overflow: hidden; /* Fix overflow and can't scroll up */ + overflow: hidden; /* Fix overflow and can't scroll up */ } .bottom-container { @@ -211,12 +211,14 @@ If your plugin does not need CSS, delete this file. word-break: break-word; } -.message-content pre, .message-content p { +.message-content pre, +.message-content p { margin: 0; padding: 0; } -.message-content ol, .message-content ul { +.message-content ol, +.message-content ul { list-style: none; padding-left: 20px; margin: 0; @@ -238,13 +240,13 @@ If your plugin does not need CSS, delete this file. } .message-content li:has(> p) { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .message-content li:has(> p)::before { - align-self: flex-start; - margin-top: 0; /* Adjust this value to align the pseudo-element vertically */ + align-self: flex-start; + margin-top: 0; /* Adjust this value to align the pseudo-element vertically */ } .copy-message-button {