mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Add code syntax highlighting and copy code (#520)
This commit is contained in:
parent
d8acc2441d
commit
0191f66e8d
6 changed files with 86 additions and 117 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ChatSingleMessageProps> = ({ message }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='message-container'>
|
||||
<div
|
||||
className={`message ${
|
||||
message.sender === USER_SENDER ? "user-message" : "bot-message"
|
||||
}`}
|
||||
>
|
||||
<div className="message-container">
|
||||
<div className={`message ${message.sender === USER_SENDER ? "user-message" : "bot-message"}`}>
|
||||
<div className="message-icon">
|
||||
{message.sender === USER_SENDER ? <UserIcon /> : <BotIcon />}
|
||||
</div>
|
||||
|
|
@ -40,9 +35,7 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({ message }) => {
|
|||
{message.sender === USER_SENDER ? (
|
||||
<span>{message.message}</span>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
transformLinkUri={null}
|
||||
>{message.message}</ReactMarkdown>
|
||||
<MemoizedReactMarkdown transformLinkUri={null}>{message.message}</MemoizedReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<CodeProps> = memo(({ language = 'text', value }) => {
|
||||
const { t } = useTranslation('markdown');
|
||||
export const CodeBlock: FC<CodeProps> = memo(({ language = "text", value }) => {
|
||||
const { t } = useTranslation("markdown");
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
|
|
@ -68,64 +67,22 @@ export const CodeBlock: FC<CodeProps> = 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 (
|
||||
<div className="codeblock relative font-sans text-[16px]">
|
||||
<div className="flex items-center justify-between py-1.5 px-4">
|
||||
<span className="text-xs lowercase text-white">{language}</span>
|
||||
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconCheck size={18} />
|
||||
) : (
|
||||
<IconClipboard size={18} />
|
||||
)}
|
||||
{isCopied ? t('Copied!') : t('Copy code')}
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center rounded bg-none p-1 text-xs text-white"
|
||||
onClick={downloadAsFile}
|
||||
>
|
||||
<IconDownload size={18} />
|
||||
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||
{isCopied ? t("Copied!") : t("Copy code")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={oneDark}
|
||||
customStyle={{ margin: 0 }}
|
||||
>
|
||||
<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0 }}>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
|
|
@ -135,6 +92,6 @@ CodeBlock.propTypes = {
|
|||
language: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
CodeBlock.displayName = 'CodeBlock';
|
||||
CodeBlock.displayName = "CodeBlock";
|
||||
|
||||
export default CodeBlock;
|
||||
|
|
|
|||
|
|
@ -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<Options> = memo((props) => (
|
||||
<ReactMarkdown
|
||||
{...props}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<CodeBlock language={match[1]} value={String(children).replace(/\n$/, "")} />
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
const MemoizedReactMarkdown: FC<Options> = memo(ReactMarkdown);
|
||||
export default MemoizedReactMarkdown;
|
||||
|
|
|
|||
|
|
@ -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> | 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", () => {
|
||||
|
|
|
|||
16
styles.css
16
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue