From 20105d54be52c15649ce36e2cd1f1e8bc39ff882 Mon Sep 17 00:00:00 2001 From: lencx Date: Fri, 16 Dec 2022 19:58:40 +0800 Subject: [PATCH] feat: chatgpt prompts --- README.md | 1 + chat.model.md | 3 + package.json | 4 +- src-tauri/tauri.conf.json | 6 +- src/components/Tags/index.tsx | 94 +++++++++++++++++++ src/hooks/useChatModel.ts | 23 +++++ src/hooks/useColumns.ts | 44 +++++++++ src/hooks/useData.ts | 35 +++++++ src/hooks/useInit.ts | 12 +++ src/routes.tsx | 10 +- src/utils.ts | 39 ++++++++ src/view/ChatGPTPrompts/config.tsx | 23 ----- src/view/ChatGPTPrompts/index.tsx | 34 ------- src/view/General.tsx | 14 +-- src/view/LanguageModel/Form.tsx | 59 ++++++++++++ src/view/LanguageModel/config.tsx | 39 ++++++++ .../index.scss | 0 src/view/LanguageModel/index.tsx | 79 ++++++++++++++++ 18 files changed, 446 insertions(+), 73 deletions(-) create mode 100644 chat.model.md create mode 100644 src/components/Tags/index.tsx create mode 100644 src/hooks/useChatModel.ts create mode 100644 src/hooks/useColumns.ts create mode 100644 src/hooks/useData.ts create mode 100644 src/hooks/useInit.ts create mode 100644 src/utils.ts delete mode 100644 src/view/ChatGPTPrompts/config.tsx delete mode 100644 src/view/ChatGPTPrompts/index.tsx create mode 100644 src/view/LanguageModel/Form.tsx create mode 100644 src/view/LanguageModel/config.tsx rename src/view/{ChatGPTPrompts => LanguageModel}/index.scss (100%) create mode 100644 src/view/LanguageModel/index.tsx diff --git a/README.md b/README.md index faf9afc..cccb095 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ yarn build ## ❤️ Thanks - The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications. + --- diff --git a/chat.model.md b/chat.model.md new file mode 100644 index 0000000..9a80fc8 --- /dev/null +++ b/chat.model.md @@ -0,0 +1,3 @@ +# ChatGPT Model + +- [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) diff --git a/package.json b/package.json index 7ee7be4..dea3025 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.4.5" + "react-router-dom": "^6.4.5", + "uuid": "^9.0.0" }, "devDependencies": { "@tauri-apps/cli": "^1.2.2", @@ -45,6 +46,7 @@ "@types/node": "^18.7.10", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", + "@types/uuid": "^9.0.0", "@vitejs/plugin-react": "^3.0.0", "sass": "^1.56.2", "typescript": "^4.9.4", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index eac73ba..519cc7f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -11,7 +11,11 @@ }, "tauri": { "allowlist": { - "all": true + "all": true, + "fs": { + "all": true, + "scope": ["$HOME/.chatgpt/*"] + } }, "systemTray": { "iconPath": "icons/tray-icon.png", diff --git a/src/components/Tags/index.tsx b/src/components/Tags/index.tsx new file mode 100644 index 0000000..9f8d4e3 --- /dev/null +++ b/src/components/Tags/index.tsx @@ -0,0 +1,94 @@ +import { FC, useEffect, useRef, useState } from 'react'; +import { PlusOutlined } from '@ant-design/icons'; +import { Input, Tag } from 'antd'; +import type { InputRef } from 'antd'; + +import { DISABLE_AUTO_COMPLETE } from '@/utils'; + +interface TagsProps { + value?: string[]; + onChange?: (v: string[]) => void; +} + +const Tags: FC = ({ value = [], onChange }) => { + const [tags, setTags] = useState(value); + const [inputVisible, setInputVisible] = useState(false); + const [inputValue, setInputValue] = useState(''); + const inputRef = useRef(null); + + useEffect(() => { + if (inputVisible) { + inputRef.current?.focus(); + } + }, [inputVisible]); + + const handleClose = (removedTag: string) => { + const newTags = tags.filter((tag) => tag !== removedTag); + setTags(newTags); + }; + + const showInput = () => { + setInputVisible(true); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleInputConfirm = () => { + if (inputValue && tags.indexOf(inputValue) === -1) { + const val = [...tags, inputValue]; + setTags(val); + onChange && onChange(val); + } + setInputVisible(false); + setInputValue(''); + }; + + const forMap = (tag: string) => { + const tagElem = ( + { + e.preventDefault(); + handleClose(tag); + }} + > + {tag} + + ); + return ( + + {tagElem} + + ); + }; + + const tagChild = tags.map(forMap); + + return ( + <> + {tagChild} + {inputVisible && ( + + )} + {!inputVisible && ( + + New Tag + + )} + + ); +}; + +export default Tags; \ No newline at end of file diff --git a/src/hooks/useChatModel.ts b/src/hooks/useChatModel.ts new file mode 100644 index 0000000..e6f3ffe --- /dev/null +++ b/src/hooks/useChatModel.ts @@ -0,0 +1,23 @@ +import { useState } from 'react'; +import { clone } from 'lodash'; + +import { CHAT_MODEL_JSON, readJSON, writeJSON } from '@/utils'; +import useInit from '@/hooks/useInit'; + +export default function useChatModel() { + const [modelJson, setModelJson] = useState>({}); + + useInit(async () => { + const data = await readJSON(CHAT_MODEL_JSON, { name: 'ChatGPT Model', data: [] }); + setModelJson(data); + }); + + const modelSet = async (data: Record[]) => { + const oData = clone(modelJson); + oData.data = data; + await writeJSON(CHAT_MODEL_JSON, oData); + setModelJson(oData); + } + + return { modelJson, modelSet, modelData: modelJson?.data || [] } +} \ No newline at end of file diff --git a/src/hooks/useColumns.ts b/src/hooks/useColumns.ts new file mode 100644 index 0000000..ed6c19a --- /dev/null +++ b/src/hooks/useColumns.ts @@ -0,0 +1,44 @@ +import { useState, useCallback } from 'react'; + +export default function useColumns(columns: any[] = []) { + const [opType, setOpType] = useState(''); + const [opRecord, setRecord] = useState | null>(null); + const [opTime, setNow] = useState(null); + const [opExtra, setExtra] = useState(null); + + const handleRecord = useCallback((row: Record | null, type: string) => { + setOpType(type); + setRecord(row); + setNow(Date.now()); + }, []); + + const resetRecord = useCallback(() => { + setRecord(null); + setOpType(''); + setNow(Date.now()); + }, []); + + const opNew = useCallback(() => handleRecord(null, 'new'), [handleRecord]); + + const cols = columns.map((i: any) => { + if (i.render) { + const opRender = i.render; + i.render = (text: string, row: Record) => { + return opRender(text, row, { setRecord: handleRecord, setExtra }); + }; + } + return i; + }); + + return { + opTime, + opType, + opNew, + columns: cols, + opRecord, + setRecord: handleRecord, + resetRecord, + setExtra, + opExtra, + }; +} \ No newline at end of file diff --git a/src/hooks/useData.ts b/src/hooks/useData.ts new file mode 100644 index 0000000..be70c6e --- /dev/null +++ b/src/hooks/useData.ts @@ -0,0 +1,35 @@ +import { useState, useEffect } from 'react'; +import { v4 } from 'uuid'; + +const safeKey = Symbol('chat-id'); + +export default function useData(oData: any[]) { + const [opData, setData] = useState([]); + + useEffect(() => { + const nData = oData.map(i => ({ [safeKey]: v4(), ...i })); + setData(nData); + }, [oData]) + + const opAdd = (val: any) => { + const v = [val, ...opData]; + setData(v); + return v; + }; + + const opRemove = (id: string) => { + const nData = opData.filter(i => i[safeKey] !== id); + setData(nData); + return nData; + }; + + const opReplace = (id: string, data: any) => { + const nData = [...opData]; + const idx = opData.findIndex(v => v[safeKey] === id); + nData[idx] = data; + setData(nData); + return nData; + }; + + return { opSafeKey: safeKey, opReplace, opAdd, opRemove, opData }; +} \ No newline at end of file diff --git a/src/hooks/useInit.ts b/src/hooks/useInit.ts new file mode 100644 index 0000000..3443d54 --- /dev/null +++ b/src/hooks/useInit.ts @@ -0,0 +1,12 @@ +import { useRef, useEffect } from 'react'; + +// fix: Two interface requests will be made in development mode +export default function useInit(callback: () => void) { + const isInit = useRef(true); + useEffect(() => { + if (isInit.current) { + callback(); + isInit.current = false; + } + }, []) +} \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx index 3cb0bc7..e73aca8 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,13 +1,13 @@ import { useRoutes } from 'react-router-dom'; import { DesktopOutlined, - BulbOutlined + BulbOutlined, } from '@ant-design/icons'; import type { RouteObject } from 'react-router-dom'; import type { MenuProps } from 'antd'; import General from '@view/General'; -import ChatGPTPrompts from '@/view/ChatGPTPrompts'; +import LanguageModel from '@/view/LanguageModel'; export type ChatRouteObject = { label: string; @@ -24,10 +24,10 @@ export const routes: Array = [ }, }, { - path: '/chatgpt-prompts', - element: , + path: '/language-model', + element: , meta: { - label: 'ChatGPT Prompts', + label: 'Language Model', icon: , }, }, diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..782284a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,39 @@ +import { readTextFile, writeTextFile, exists } from '@tauri-apps/api/fs'; +import { homeDir, join } from '@tauri-apps/api/path'; + +export const CHAT_MODEL_JSON = 'chat.model.json'; +export const DISABLE_AUTO_COMPLETE = { + autoCapitalize: 'off', + autoComplete: 'off', + spellCheck: false +}; + +const chatRoot = async () => { + return join(await homeDir(), '.chatgpt') +} + +export const readJSON = async (path: string, defaultVal = {}) => { + const root = await chatRoot(); + const file = await join(root, path); + + if (!await exists(file)) { + writeTextFile(file, JSON.stringify({ + name: 'ChatGPT', + link: 'https://github.com/lencx/ChatGPT/blob/main/chat.model.md', + data: null, + ...defaultVal, + }, null, 2)) + } + + try { + return JSON.parse(await readTextFile(file)); + } catch(e) { + return {}; + } +} + +export const writeJSON = async (path: string, data: Record) => { + const root = await chatRoot(); + const file = await join(root, path); + await writeTextFile(file, JSON.stringify(data, null, 2)); +} \ No newline at end of file diff --git a/src/view/ChatGPTPrompts/config.tsx b/src/view/ChatGPTPrompts/config.tsx deleted file mode 100644 index c6da19c..0000000 --- a/src/view/ChatGPTPrompts/config.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Tag, Tooltip } from 'antd'; - -export const columns = [ - { - title: 'Command', - dataIndex: 'cmd', - key: 'cmd', - }, - { - title: 'Type', - dataIndex: 'type', - key: 'type', - render: (v: string) => {v} - }, - { - title: 'Content', - dataIndex: 'content', - key: 'content', - render: (v: string) => ( - {v} - ), - }, -]; diff --git a/src/view/ChatGPTPrompts/index.tsx b/src/view/ChatGPTPrompts/index.tsx deleted file mode 100644 index af40ad5..0000000 --- a/src/view/ChatGPTPrompts/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Table, Button } from 'antd'; - -import { columns } from './config'; -import './index.scss'; - -const dataSource = [ - { - cmd: 'terminal', - type: 'dev', - content: 'i want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is pwd', - }, - { - cmd: 'translator', - type: 'tools', - content: 'I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is "istanbulu cok seviyom burada olmak cok guzel"', - }, -]; - -export default function ChatGPTPrompts() { - return ( -
- - - - ) -} \ No newline at end of file diff --git a/src/view/General.tsx b/src/view/General.tsx index e2f2a71..5e7dbbe 100644 --- a/src/view/General.tsx +++ b/src/view/General.tsx @@ -7,6 +7,8 @@ import { ask } from '@tauri-apps/api/dialog'; import { relaunch } from '@tauri-apps/api/process'; import { clone, omit, isEqual } from 'lodash'; +import { DISABLE_AUTO_COMPLETE } from '@/utils'; + const OriginLabel = ({ url }: { url: string }) => { return ( @@ -15,12 +17,6 @@ const OriginLabel = ({ url }: { url: string }) => { ) } -const disableAuto = { - autoCapitalize: 'off', - autoComplete: 'off', - spellCheck: false -} - export default function General() { const [form] = Form.useForm(); const [platformInfo, setPlatform] = useState(''); @@ -81,13 +77,13 @@ export default function General() { )} } name="origin"> - + - + - + diff --git a/src/view/LanguageModel/Form.tsx b/src/view/LanguageModel/Form.tsx new file mode 100644 index 0000000..dabc195 --- /dev/null +++ b/src/view/LanguageModel/Form.tsx @@ -0,0 +1,59 @@ +import { useEffect, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react'; +import { Form, Input, Switch } from 'antd'; +import type { FormProps } from 'antd'; + +import Tags from '@comps/Tags'; +import { DISABLE_AUTO_COMPLETE } from '@/utils'; + +interface LanguageModelProps { + record?: Record | null; +} + +const initFormValue = { + act: '', + enable: true, + tags: [], + prompt: '', +}; + +const LanguageModel: ForwardRefRenderFunction = ({ record }, ref) => { + const [form] = Form.useForm(); + useImperativeHandle(ref, () => ({ form })); + + useEffect(() => { + if (record) { + form.setFieldsValue(record); + } + }, [record]); + + return ( +
+ + + + + + + + + + + + + + ) +} + +export default forwardRef(LanguageModel); diff --git a/src/view/LanguageModel/config.tsx b/src/view/LanguageModel/config.tsx new file mode 100644 index 0000000..00c57f6 --- /dev/null +++ b/src/view/LanguageModel/config.tsx @@ -0,0 +1,39 @@ +import { Tag, Switch, Tooltip, Space } from 'antd'; + +export const modelColumns = () => [ + { + title: 'Act', + dataIndex: 'act', + key: 'act', + }, + { + title: 'Tags', + dataIndex: 'tags', + key: 'tags', + render: (v: string[]) => v?.map(i => {i}), + }, + { + title: 'Enable', + dataIndex: 'enable', + key: 'enable', + render: (v: boolean = false) => , + }, + { + title: 'Prompt', + dataIndex: 'prompt', + key: 'prompt', + render: (v: string) => ( + {v} + ), + }, + { + title: 'Action', + key: 'action', + render: (_: any, row: any, actions: any) => ( + + actions.setRecord(row, 'edit')}>Edit + actions.setRecord(row, 'delete')}>Delete + + ), + } +]; diff --git a/src/view/ChatGPTPrompts/index.scss b/src/view/LanguageModel/index.scss similarity index 100% rename from src/view/ChatGPTPrompts/index.scss rename to src/view/LanguageModel/index.scss diff --git a/src/view/LanguageModel/index.tsx b/src/view/LanguageModel/index.tsx new file mode 100644 index 0000000..e650bc5 --- /dev/null +++ b/src/view/LanguageModel/index.tsx @@ -0,0 +1,79 @@ +import { useState, useRef, useEffect } from 'react'; +import { Table, Button, Modal } from 'antd'; + +import useChatModel from '@/hooks/useChatModel'; +import useColumns from '@/hooks/useColumns'; +import useData from '@/hooks/useData'; +import { modelColumns } from './config'; +import LanguageModelForm from './Form'; +import './index.scss'; + +export default function LanguageModel() { + const [isVisible, setVisible] = useState(false); + const { modelData, modelSet } = useChatModel(); + const { opData, opAdd, opRemove, opReplace, opSafeKey } = useData(modelData); + const { columns, ...opInfo } = useColumns(modelColumns()); + const formRef = useRef(null); + + const hide = () => { + setVisible(false); + opInfo.resetRecord(); + }; + + const handleOk = () => { + formRef.current?.form?.validateFields() + .then((vals: Record) => { + let data = []; + switch (opInfo.opType) { + case 'new': data = opAdd(vals); break; + case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break; + default: break; + } + modelSet(data) + hide(); + }) + }; + + useEffect(() => { + if (!opInfo.opType) return; + if (['edit', 'new'].includes(opInfo.opType)) { + setVisible(true); + } + if (['delete'].includes(opInfo.opType)) { + const data = opRemove(opInfo?.opRecord?.[opSafeKey]); + modelSet(data); + opInfo.resetRecord(); + } + }, [opInfo.opType, formRef]); + + const modalTitle = `${({ new: 'Create', edit: 'Edit' })[opInfo.opType]} Language Model`; + + return ( +
+ +
Total {total} items, + }} + /> + + + + + ) +} \ No newline at end of file