Merge pull request #212 from lencx/dev

This commit is contained in:
lencx
2023-01-15 16:43:33 +08:00
committed by GitHub
12 changed files with 210 additions and 190 deletions

View File

@@ -90,13 +90,13 @@ jobs:
publish_dir: ./updater
force_orphan: true
publish-winget:
# Action can only be run on windows
runs-on: windows-latest
needs: [create-release, build-tauri]
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
with:
identifier: lencx.ChatGPT
token: ${{ secrets.WINGET_TOKEN }}
version: ${{ env.version }}
# publish-winget:
# # Action can only be run on windows
# runs-on: windows-latest
# needs: [create-release, build-tauri]
# steps:
# - uses: vedantmgoyal2009/winget-releaser@v1
# with:
# identifier: lencx.ChatGPT
# token: ${{ secrets.WINGET_TOKEN }}
# version: ${{ env.version }}

View File

@@ -24,7 +24,7 @@
### Windows
- [ChatGPT_0.9.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/ChatGPT_0.9.0_x64_en-US.msi):
- [ChatGPT_0.9.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/ChatGPT_0.9.1_x64_en-US.msi):
- 使用 [winget](https://winstall.app/apps/lencx.ChatGPT):
```bash
# install the latest version
@@ -34,12 +34,12 @@
winget install --id=lencx.ChatGPT -e --version 0.9.0
```
**注意:如果安装路径和应用名称相同,会导致冲突 ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.9.0))**
**注意:如果安装路径和应用名称相同,会导致冲突 ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.9.1))**
### Mac
- [ChatGPT_0.9.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/ChatGPT_0.9.0_x64.dmg)
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/ChatGPT.app.tar.gz)
- [ChatGPT_0.9.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/ChatGPT_0.9.1_x64.dmg)
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/ChatGPT.app.tar.gz)
- Homebrew \
_[Homebrew 快捷安装](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
```sh
@@ -55,8 +55,8 @@
### Linux
- [chat-gpt_0.9.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/chat-gpt_0.9.0_amd64.deb)
- [chat-gpt_0.9.0_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/chat-gpt_0.9.0_amd64.AppImage): **工作可靠,`.deb` 运行失败时可以尝试它**
- [chat-gpt_0.9.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/chat-gpt_0.9.1_amd64.deb)
- [chat-gpt_0.9.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/chat-gpt_0.9.1_amd64.AppImage): **工作可靠,`.deb` 运行失败时可以尝试它**
- 使用 [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin):
```bash
yay -S chatgpt-desktop-bin

View File

@@ -26,7 +26,7 @@
### Windows
- [ChatGPT_0.9.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/ChatGPT_0.9.0_x64_en-US.msi): Direct download installer
- [ChatGPT_0.9.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/ChatGPT_0.9.1_x64_en-US.msi): Direct download installer
- Use [winget](https://winstall.app/apps/lencx.ChatGPT):
```bash
# install the latest version
@@ -36,12 +36,12 @@
winget install --id=lencx.ChatGPT -e --version 0.9.0
```
**Note: If the installation path and application name are the same, it will lead to conflict ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.9.0))**
**Note: If the installation path and application name are the same, it will lead to conflict ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.9.1))**
### Mac
- [ChatGPT_0.9.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/ChatGPT_0.9.0_x64.dmg): Direct download installer
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/ChatGPT.app.tar.gz): Download the `.app` installer
- [ChatGPT_0.9.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/ChatGPT_0.9.1_x64.dmg): Direct download installer
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/ChatGPT.app.tar.gz): Download the `.app` installer
- Homebrew \
Or you can install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
```sh
@@ -57,8 +57,8 @@
### Linux
- [chat-gpt_0.9.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/chat-gpt_0.9.0_amd64.deb): Download `.deb` installer, advantage small size, disadvantage poor compatibility
- [chat-gpt_0.9.0_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.9.0/chat-gpt_0.9.0_amd64.AppImage): Works reliably, you can try it if `.deb` fails to run
- [chat-gpt_0.9.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/chat-gpt_0.9.1_amd64.deb): Download `.deb` installer, advantage small size, disadvantage poor compatibility
- [chat-gpt_0.9.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.9.1/chat-gpt_0.9.1_amd64.AppImage): Works reliably, you can try it if `.deb` fails to run
- Available on [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin) with the package name `chatgpt-desktop-bin`, and you can use your favourite AUR package manager to install it.
<!-- download end -->

View File

@@ -1,5 +1,9 @@
# UPDATE LOG
## v0.9.1
fix: slash command does not work
## v0.9.0
fix:

4
scripts/download.js vendored
View File

@@ -13,7 +13,9 @@ async function rewrite(filename) {
flag = true;
}
if (flag) {
content[i] = content[i].replace(/(\d+).(\d+).(\d+)/g, argv[0]);
if (!/winget install --id=lencx.ChatGPT -e --version/.test(content[i])) {
content[i] = content[i].replace(/(\d+).(\d+).(\d+)/g, argv[0]);
}
}
if (endRe.test(content[i])) {
break;

View File

@@ -87,185 +87,198 @@ $(function() {
clearInterval(window.formInterval);
}
window.formInterval = setInterval(() => {
const form = document.querySelector("form");
const form = document.querySelector("form textarea");
if (!form) return;
clearInterval(window.formInterval);
cmdTip();
}, 200);
new MutationObserver(function (mutationsList) {
for (const mutation of mutationsList) {
if (mutation.target.getAttribute('id') === '__next') {
cmdTip();
}
if (mutation.target.getAttribute('class') === 'chat-model-cmd-list') {
// The `chatgpt prompt` fill can be done by clicking on the event.
const searchDom = document.querySelector("form .chat-model-cmd-list>div");
const searchInput = document.querySelector('form textarea');
if (!searchDom) return;
searchDom.addEventListener('click', (event) => {
const item = event.target.closest("div");
if (item) {
const val = decodeURIComponent(item.getAttribute('data-prompt'));
searchInput.value = val;
document.querySelector('form textarea').focus();
initDom();
}
});
}
}
}).observe(document.body, {
childList: true,
subtree: true,
});
}, 300);
});
async function cmdTip() {
initDom();
const chatModelJson = await invoke('get_chat_model_cmd') || {};
const data = chatModelJson.data;
if (data.length <= 0) return;
const modelDom = document.createElement('div');
modelDom.classList.add('chat-model-cmd-list');
let modelDom = document.querySelector('.chat-model-cmd-list');
if (!modelDom) {
const dom = document.createElement('div');
dom.classList.add('chat-model-cmd-list');
document.querySelector('form').appendChild(dom);
modelDom = document.querySelector('.chat-model-cmd-list');
// fix: tray window
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
modelDom.style.bottom = '54px';
}
document.querySelector('form').appendChild(modelDom);
const itemDom = (v) => `<div class="cmd-item" title="${v.prompt}" data-cmd="${v.cmd}" data-prompt="${encodeURIComponent(v.prompt)}"><b title="${v.cmd}">/${v.cmd}</b><i>${v.act}</i></div>`;
const renderList = (v) => {
modelDom.innerHTML = `<div>${v.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD_PROMPT__ = v[0]?.prompt.trim();
window.__CHAT_MODEL_CMD__ = v[0]?.cmd.trim();
window.__list = modelDom.querySelectorAll('.cmd-item');
window.__index = 0;
window.__list[window.__index].classList.add('selected');
};
const setPrompt = (v = '') => {
if (v.trim()) {
window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${v.trim()}}`);
}
}
const searchInput = document.querySelector('form textarea');
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
// If more than one command appears in the search results, the first one will be used by default.
searchInput.addEventListener('keydown', (event) => {
if (!window.__CHAT_MODEL_CMD_PROMPT__) {
return;
// fix: tray window
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
modelDom.style.bottom = '54px';
}
// ------------------ Keyboard scrolling (ArrowUp | ArrowDown) --------------------------
if (event.keyCode === 38 && window.__index > 0) { // ArrowUp
window.__list[window.__index].classList.remove('selected');
window.__index = window.__index - 1;
const itemDom = (v) => `<div class="cmd-item" title="${v.prompt}" data-cmd="${v.cmd}" data-prompt="${encodeURIComponent(v.prompt)}"><b title="${v.cmd}">/${v.cmd}</b><i>${v.act}</i></div>`;
const renderList = (v) => {
initDom();
modelDom.innerHTML = `<div>${v.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD_PROMPT__ = v[0]?.prompt.trim();
window.__CHAT_MODEL_CMD__ = v[0]?.cmd.trim();
window.__list = modelDom.querySelectorAll('.cmd-item');
window.__index = 0;
window.__list[window.__index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt'));
searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`;
event.preventDefault();
}
if (event.keyCode === 40 && window.__index < window.__list.length - 1) { // ArrowDown
window.__list[window.__index].classList.remove('selected');
window.__index = window.__index + 1;
window.__list[window.__index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt'));
searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`;
event.preventDefault();
}
const containerHeight = modelDom.offsetHeight;
const itemHeight = window.__list[0].offsetHeight + 1;
const itemTop = window.__list[window.__index].offsetTop;
const itemBottom = itemTop + itemHeight;
if (itemTop < modelDom.scrollTop || itemBottom > modelDom.scrollTop + containerHeight) {
modelDom.scrollTop = itemTop;
}
// ------------------ TAB key replaces `{q}` tag content -------------------------------
// feat: https://github.com/lencx/ChatGPT/issues/54
if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) {
const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || [];
if (strGroup[1]) {
searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> ';
window.__CHAT_MODEL_STATUS__ = 1;
};
const setPrompt = (v = '') => {
if (v.trim()) {
window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${v.trim()}}`);
}
event.preventDefault();
}
const searchInput = document.querySelector('form textarea');
if (window.__CHAT_MODEL_STATUS__ === 1 && event.keyCode === 9) { // TAB
const data = searchInput.value.split('|->');
if (data[1]?.trim()) {
setPrompt(data[1]);
window.__CHAT_MODEL_STATUS__ = 2;
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
// If more than one command appears in the search results, the first one will be used by default.
function cmdKeydown(event) {
if (!window.__CHAT_MODEL_CMD_PROMPT__) {
return;
}
event.preventDefault();
}
// input text
if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) { // TAB
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_STATUS__;
event.preventDefault();
}
// ------------------ Keyboard scrolling (ArrowUp | ArrowDown) --------------------------
if (event.keyCode === 38 && window.__index > 0) { // ArrowUp
window.__list[window.__index].classList.remove('selected');
window.__index = window.__index - 1;
window.__list[window.__index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt'));
searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`;
event.preventDefault();
}
// ------------------ type in a space to complete the fill ------------------------------------
if (event.keyCode === 32) {
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
}
if (event.keyCode === 40 && window.__index < window.__list.length - 1) { // ArrowDown
window.__list[window.__index].classList.remove('selected');
window.__index = window.__index + 1;
window.__list[window.__index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt'));
searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`;
event.preventDefault();
}
// ------------------ send --------------------------------------------------------------------
if (event.keyCode === 13 && window.__CHAT_MODEL_CMD_PROMPT__) { // Enter
const data = searchInput.value.split('|->');
setPrompt(data[1]);
const containerHeight = modelDom.offsetHeight;
const itemHeight = window.__list[0].offsetHeight + 1;
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
event.preventDefault();
}
});
const itemTop = window.__list[window.__index].offsetTop;
const itemBottom = itemTop + itemHeight;
if (itemTop < modelDom.scrollTop || itemBottom > modelDom.scrollTop + containerHeight) {
modelDom.scrollTop = itemTop;
}
searchInput.addEventListener('input', () => {
if (searchInput.value === '') {
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
}
// ------------------ TAB key replaces `{q}` tag content -------------------------------
// feat: https://github.com/lencx/ChatGPT/issues/54
if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) {
const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || [];
if (window.__CHAT_MODEL_STATUS__) return;
if (strGroup[1]) {
searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> ';
window.__CHAT_MODEL_STATUS__ = 1;
} else {
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
initDom();
}
event.preventDefault();
}
const query = searchInput.value;
if (!query || !/^\//.test(query)) {
modelDom.innerHTML = '';
return;
}
if (window.__CHAT_MODEL_STATUS__ === 1 && event.keyCode === 9) { // TAB
const data = searchInput.value.split('|->');
if (data[1]?.trim()) {
setPrompt(data[1]);
window.__CHAT_MODEL_STATUS__ = 2;
}
event.preventDefault();
}
// all cmd result
if (query === '/') {
renderList(data);
return;
}
const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd));
if (result.length > 0) {
renderList(result);
} else {
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
}
}, {
capture: false,
passive: true,
once: false
});
if (window.searchInterval) {
clearInterval(window.searchInterval);
}
window.searchInterval = setInterval(() => {
// The `chatgpt prompt` fill can be done by clicking on the event.
const searchDom = document.querySelector("form .chat-model-cmd-list>div");
if (!searchDom) return;
searchDom.addEventListener('click', (event) => {
// .cmd-item
const item = event.target.closest("div");
if (item) {
const val = decodeURIComponent(item.getAttribute('data-prompt'));
searchInput.value = val;
document.querySelector('form textarea').focus();
window.__CHAT_MODEL_CMD_PROMPT__ = val;
// input text
if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) { // TAB
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_STATUS__;
event.preventDefault();
}
}, {
capture: false,
passive: true,
once: false
});
}, 200);
// ------------------ type in a space to complete the fill ------------------------------------
if (event.keyCode === 32) {
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
}
// ------------------ send --------------------------------------------------------------------
if (event.keyCode === 13 && window.__CHAT_MODEL_CMD_PROMPT__) { // Enter
const data = searchInput.value.split('|->');
setPrompt(data[1]);
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
initDom();
event.preventDefault();
}
}
searchInput.removeEventListener('keydown', cmdKeydown);
searchInput.addEventListener('keydown', cmdKeydown);
function cmdInput() {
if (searchInput.value === '') {
initDom();
}
if (window.__CHAT_MODEL_STATUS__) return;
const query = searchInput.value;
if (!query || !/^\//.test(query)) {
initDom();
return;
}
// all cmd result
if (query === '/') {
renderList(data);
return;
}
const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd));
if (result.length > 0) {
renderList(result);
} else {
initDom();
}
}
searchInput.removeEventListener('input', cmdInput);
searchInput.addEventListener('input', cmdInput);
}
}
function initDom() {
const modelDom = document.querySelector('.chat-model-cmd-list');
if (modelDom) {
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
}
}

View File

@@ -1,8 +1,7 @@
// *** Core Script - Export ***
const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral" id="download-png-button">Try Again</button>`;
$(async function () {
const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral" id="download-png-button">Try Again</button>`;
if (window.innerWidth < 767) return;
const chatConf = await invoke('get_chat_conf') || {};
if (window.buttonsInterval) {
@@ -190,7 +189,7 @@ async function handlePdf(imgData, canvas, pixelRatio) {
}
function getName() {
const id = uid().toString(36);
const id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || '';
return { filename: name ? name : id, id, pathname: 'chat.download.json' };
}

View File

@@ -16,9 +16,9 @@ $(async function () {
background: #4a4a4a;
color: white;
font-weight: bold;
padding: 5px 8px;
border-radius: 4px;
font-size: 12px;
padding: 3px 5px;
border-radius: 2px;
font-size: 10px;
cursor: pointer;
}
`;
@@ -31,6 +31,7 @@ $(async function () {
const { computePosition, flip, offset, shift } = window.FloatingUIDOM;
document.body.addEventListener('mousedown', async (e) => {
selectionMenu.style.display = 'none';
if (e.target.id === 'chagpt-selection-menu') {
await invoke('dalle2_window', { query: encodeURIComponent(window.__DALLE2_CONTENT__) });
} else {

View File

@@ -7,7 +7,7 @@
},
"package": {
"productName": "ChatGPT",
"version": "0.9.0"
"version": "0.9.1"
},
"tauri": {
"allowlist": {

View File

@@ -53,25 +53,25 @@ interface EditRowProps {
}
export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
const [isEdit, setEdit] = useState(false);
const [val, setVal] = useState(row[rowKey]);
const [val, setVal] = useState(row[rowKey] || '');
const handleEdit = () => {
setEdit(true);
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setVal(e.target.value)
};
const handleSave = () => {
setEdit(false);
row[rowKey] = val;
row[rowKey] = val?.trim();
actions?.setRecord(row, 'rowedit')
};
return isEdit
? (
<Input.TextArea
<Input
value={val}
rows={1}
autoFocus
onChange={handleChange}
{...DISABLE_AUTO_COMPLETE}
onPressEnter={handleSave}

View File

@@ -69,7 +69,6 @@ export default function Download() {
const downloadDir = await path.join(await chatRoot(), 'download');
await fs.removeDir(downloadDir, { recursive: true });
await handleRefresh();
rowReset();
message.success('All files have been cleared!');
return;
}
@@ -88,6 +87,7 @@ export default function Download() {
const handleRefresh = async () => {
await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' });
rowReset();
const data = await refreshJson();
opInit(data);
};

View File

@@ -65,7 +65,6 @@ export default function Notes() {
const notesDir = await path.join(await chatRoot(), 'notes');
await fs.removeDir(notesDir, { recursive: true });
await handleRefresh();
rowReset();
message.success('All files have been cleared!');
return;
}
@@ -83,6 +82,7 @@ export default function Notes() {
const handleRefresh = async () => {
await invoke('download_list', { pathname: CHAT_NOTES_JSON, dir: 'notes' });
rowReset();
const data = await refreshJson();
opInit(data);
};
@@ -135,6 +135,7 @@ export default function Notes() {
>
<ReactMarkdown
children={source}
linkTarget="_blank"
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '')