feat: prompts import and export

Fixes #71
This commit is contained in:
Jing Hua 2023-03-31 23:33:47 +08:00
parent 2c89c9325b
commit e14f44b8c9
6 changed files with 169 additions and 1 deletions

View file

@ -45,6 +45,7 @@
"i18next-http-backend": "^2.1.1", "i18next-http-backend": "^2.1.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"match-sorter": "^6.3.1", "match-sorter": "^6.3.1",
"papaparse": "^5.4.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^12.2.0", "react-i18next": "^12.2.0",
@ -59,6 +60,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/papaparse": "^5.3.7",
"@types/react": "^18.0.27", "@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
"@types/react-scroll-to-bottom": "^4.2.0", "@types/react-scroll-to-bottom": "^4.2.0",

View file

@ -0,0 +1,27 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import useStore from '@store/store';
import { exportPrompts } from '@utils/prompt';
const ExportPrompt = () => {
const { t } = useTranslation();
const prompts = useStore.getState().prompts;
return (
<div className='mt-4'>
<div className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
{t('export')}
</div>
<button
className='btn btn-small btn-primary'
onClick={() => {
exportPrompts(prompts);
}}
>
{t('export')}
</button>
</div>
);
};
export default ExportPrompt;

View file

@ -0,0 +1,84 @@
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import useStore from '@store/store';
import { importPromptCSV } from '@utils/prompt';
const ImportPrompt = () => {
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const [alert, setAlert] = useState<{
message: string;
success: boolean;
} | null>(null);
const handleFileUpload = () => {
if (!inputRef || !inputRef.current) return;
const file = inputRef.current.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const csvString = event.target?.result as string;
try {
const results = importPromptCSV(csvString);
const prompts = useStore.getState().prompts;
const setPrompts = useStore.getState().setPrompts;
const newPrompts = results.map((data) => {
const columns = Object.values(data);
return {
id: uuidv4(),
name: columns[0],
prompt: columns[1],
};
});
setPrompts(prompts.concat(newPrompts));
setAlert({ message: 'Succesfully imported!', success: true });
} catch (error: unknown) {
setAlert({ message: (error as Error).message, success: false });
}
};
reader.readAsText(file);
}
};
return (
<div>
<label className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
{t('import')}
</label>
<input
className='w-full text-sm file:p-2 text-gray-800 file:text-gray-700 dark:text-gray-300 dark:file:text-gray-200 rounded-md cursor-pointer focus:outline-none bg-gray-50 file:bg-gray-100 dark:bg-gray-800 dark:file:bg-gray-700 file:border-0 border border-gray-300 dark:border-gray-600 placeholder-gray-900 dark:placeholder-gray-300 file:cursor-pointer'
type='file'
ref={inputRef}
/>
<button
className='btn btn-small btn-primary mt-3'
onClick={handleFileUpload}
>
{t('import')}
</button>
{alert && (
<div
className={`relative py-2 px-3 w-full mt-3 border rounded-md text-gray-600 dark:text-gray-100 text-sm whitespace-pre-wrap ${
alert.success
? 'border-green-500 bg-green-500/10'
: 'border-red-500 bg-red-500/10'
}`}
>
{alert.message}
</div>
)}
</div>
);
};
export default ImportPrompt;

View file

@ -7,6 +7,8 @@ import { Prompt } from '@type/prompt';
import PlusIcon from '@icon/PlusIcon'; import PlusIcon from '@icon/PlusIcon';
import CrossIcon from '@icon/CrossIcon'; import CrossIcon from '@icon/CrossIcon';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import ImportPrompt from './ImportPrompt';
import ExportPrompt from './ExportPrompt';
const PromptLibraryMenu = () => { const PromptLibraryMenu = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -31,8 +33,10 @@ const PromptLibraryMenuPopUp = ({
const { t } = useTranslation(); const { t } = useTranslation();
const setPrompts = useStore((state) => state.setPrompts); const setPrompts = useStore((state) => state.setPrompts);
const prompts = useStore((state) => state.prompts);
const [_prompts, _setPrompts] = useState<Prompt[]>( const [_prompts, _setPrompts] = useState<Prompt[]>(
JSON.parse(JSON.stringify(useStore.getState().prompts)) JSON.parse(JSON.stringify(prompts))
); );
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
@ -74,6 +78,10 @@ const PromptLibraryMenuPopUp = ({
e.target.style.maxHeight = '2.5rem'; e.target.style.maxHeight = '2.5rem';
}; };
useEffect(() => {
_setPrompts(prompts);
}, [prompts]);
return ( return (
<PopupModal <PopupModal
title={t('promptLibrary') as string} title={t('promptLibrary') as string}
@ -81,6 +89,10 @@ const PromptLibraryMenuPopUp = ({
handleConfirm={handleSave} handleConfirm={handleSave}
> >
<div className='p-6 border-b border-gray-200 dark:border-gray-600 w-[90vw] max-w-full text-sm text-gray-900 dark:text-gray-300'> <div className='p-6 border-b border-gray-200 dark:border-gray-600 w-[90vw] max-w-full text-sm text-gray-900 dark:text-gray-300'>
<div className='border px-4 py-2 rounded border-gray-200 dark:border-gray-600'>
<ImportPrompt />
<ExportPrompt />
</div>
<div className='flex flex-col p-2 max-w-full' ref={container}> <div className='flex flex-col p-2 max-w-full' ref={container}>
<div className='flex font-bold border-b border-gray-500/50 mb-1 p-1'> <div className='flex font-bold border-b border-gray-500/50 mb-1 p-1'>
<div className='sm:w-1/4 max-sm:flex-1'>{t('name')}</div> <div className='sm:w-1/4 max-sm:flex-1'>{t('name')}</div>

31
src/utils/prompt.ts Normal file
View file

@ -0,0 +1,31 @@
import { Prompt } from '@type/prompt';
import { getToday } from './date';
import Papa from 'papaparse';
export const importPromptCSV = (csvString: string, header: boolean = true) => {
const results = Papa.parse(csvString, {
header,
delimiter: ',',
newline: '\n',
skipEmptyLines: true,
});
return results.data as Record<string, string>[];
};
export const exportPrompts = (prompts: Prompt[]) => {
const csvString = Papa.unparse(
prompts.map((prompt) => ({ name: prompt.name, prompt: prompt.prompt }))
);
const blob = new Blob([csvString], {
type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${getToday()}.csv`;
link.click();
link.remove();
};

View file

@ -609,6 +609,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.23.tgz#b6e934fe427eb7081d0015aad070acb3373c3c90" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.23.tgz#b6e934fe427eb7081d0015aad070acb3373c3c90"
integrity sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g== integrity sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==
"@types/papaparse@^5.3.7":
version "5.3.7"
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.7.tgz#8d3bf9e62ac2897df596f49d9ca59a15451aa247"
integrity sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==
dependencies:
"@types/node" "*"
"@types/parse-json@^4.0.0": "@types/parse-json@^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -2992,6 +2999,11 @@ p-cancelable@^2.0.0:
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==
papaparse@^5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.4.1.tgz#f45c0f871853578bd3a30f92d96fdcfb6ebea127"
integrity sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"