From c6a48b931a8c71cccd9bf2887005ce8ba91e61c3 Mon Sep 17 00:00:00 2001 From: Jonghakseo Date: Sun, 2 Jul 2023 20:02:40 +0900 Subject: [PATCH 1/6] add: options page setting --- src/pages/options/Options.css | 8 --- src/pages/options/Options.tsx | 8 --- src/pages/options/index.tsx | 4 +- src/pages/options/src/App.tsx | 19 ++++++ src/shared/component/FontProvider.tsx | 23 +++++++ src/shared/component/ResetStyleProvider.tsx | 75 +++++++++++++++++++++ 6 files changed, 119 insertions(+), 18 deletions(-) delete mode 100644 src/pages/options/Options.css delete mode 100644 src/pages/options/Options.tsx create mode 100644 src/pages/options/src/App.tsx create mode 100644 src/shared/component/FontProvider.tsx create mode 100644 src/shared/component/ResetStyleProvider.tsx diff --git a/src/pages/options/Options.css b/src/pages/options/Options.css deleted file mode 100644 index 08a2ee4..0000000 --- a/src/pages/options/Options.css +++ /dev/null @@ -1,8 +0,0 @@ -.OptionsContainer { - width: 100%; - height: 50vh; - font-size: 2rem; - display: flex; - align-items: center; - justify-content: center; -} diff --git a/src/pages/options/Options.tsx b/src/pages/options/Options.tsx deleted file mode 100644 index 28d8fe9..0000000 --- a/src/pages/options/Options.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import "@pages/options/Options.css"; - -const Options: React.FC = () => { - return
Options
; -}; - -export default Options; diff --git a/src/pages/options/index.tsx b/src/pages/options/index.tsx index a6959b6..05da71e 100644 --- a/src/pages/options/index.tsx +++ b/src/pages/options/index.tsx @@ -1,6 +1,6 @@ import React from "react"; import { createRoot } from "react-dom/client"; -import Options from "@pages/options/Options"; +import App from "@pages/options/src/App"; import "@pages/options/index.css"; import refreshOnUpdate from "virtual:reload-on-update-in-view"; @@ -12,7 +12,7 @@ function init() { throw new Error("Can not find #app-container"); } const root = createRoot(appContainer); - root.render(); + root.render(); } init(); diff --git a/src/pages/options/src/App.tsx b/src/pages/options/src/App.tsx new file mode 100644 index 0000000..6f5112a --- /dev/null +++ b/src/pages/options/src/App.tsx @@ -0,0 +1,19 @@ +import { ChakraProvider, theme, ThemeProvider } from "@chakra-ui/react"; +import { FC } from "react"; +import ResetStyleProvider from "@src/shared/component/ResetStyleProvider"; +import FontProvider from "@src/shared/component/FontProvider"; + +const App: FC = () => { + return ( + + + + {/* TODO router */} + Options + + + + ); +}; + +export default App; diff --git a/src/shared/component/FontProvider.tsx b/src/shared/component/FontProvider.tsx new file mode 100644 index 0000000..d454db2 --- /dev/null +++ b/src/shared/component/FontProvider.tsx @@ -0,0 +1,23 @@ +import { ReactNode, useEffect } from "react"; + +export default function FontProvider({ children }: { children: ReactNode }) { + useEffect(() => { + const linkNode = document.createElement("link"); + linkNode.type = "text/css"; + linkNode.rel = "stylesheet"; + linkNode.href = + "https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap"; + document.head.appendChild(linkNode); + }, []); + + return ( + <> + + {children} + + ); +} diff --git a/src/shared/component/ResetStyleProvider.tsx b/src/shared/component/ResetStyleProvider.tsx new file mode 100644 index 0000000..f2179f4 --- /dev/null +++ b/src/shared/component/ResetStyleProvider.tsx @@ -0,0 +1,75 @@ +import { ReactNode } from "react"; + +export default function ResetStyleProvider({ + children, +}: { + children: ReactNode; +}) { + return ( + <> + + {children} + + ); +} From 50dfc8b933cbf833c2aaa2497fdafb51dcdecb18 Mon Sep 17 00:00:00 2001 From: Jonghakseo Date: Sun, 2 Jul 2023 20:02:49 +0900 Subject: [PATCH 2/6] fix: message type name --- src/global.d.ts | 66 ++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/global.d.ts b/src/global.d.ts index c1d5ae7..096ee66 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -56,109 +56,109 @@ declare global { content: string; }; - type AddNewSlotMessage = { + type AddNewSlot = { type: "AddNewSlot"; input: Slot; data?: "success"; }; - type SelectSlotMessage = { + type SelectSlot = { type: "SelectSlot"; input: string; data?: "success"; }; - type UpdateSlotMessage = { + type UpdateSlot = { type: "UpdateSlot"; input: Slot; data?: "success"; }; - type DeleteSlotMessage = { + type DeleteSlot = { type: "DeleteSlot"; input: string; data?: "success"; }; - type RequestOnetimeChatGPTMessage = { + type RequestOnetimeChatGPT = { type: "RequestOnetimeChatGPT"; input: string; data?: { result: string }; }; - type RequestGenerateChatGPTPromptMessage = { + type RequestGenerateChatGPTPrompt = { type: "RequestGenerateChatGPTPrompt"; input: string; data?: { result: string }; }; - type RequestOngoingChatGPTMessage = { + type RequestOngoingChatGPT = { type: "RequestOngoingChatGPT"; input: ChatCompletionRequestMessage[]; data?: { result: string }; }; - type RequestInitialDragGPTMessage = { + type RequestInitialDragGPT = { type: "RequestInitialDragGPTStream"; input?: string; data?: { result: string; chunk?: string; isDone?: boolean }; }; - type RequestDragGPTMessage = { + type RequestDragGPT = { type: "RequestDragGPTStream"; input?: ChatCompletionRequestMessage[]; data?: { result: string; chunk?: string; isDone?: boolean }; }; - type RequestQuickChatGPTMessage = { + type RequestQuickChatGPT = { type: "RequestQuickChatGPTStream"; input?: ChatCompletionRequestMessage[]; data?: { result: string; chunk?: string; isDone?: boolean }; }; - type SaveAPIKeyMessage = { + type SaveAPIKey = { type: "SaveAPIKey"; input: string; data?: "success"; }; - type ResetAPIKeyMessage = { + type ResetAPIKey = { type: "ResetAPIKey"; input?: never; data?: "success"; }; - type GetAPIKeyMessage = { + type GetAPIKey = { type: "GetAPIKey"; input?: never; data?: string; }; - type GetSlotsMessage = { + type GetSlots = { type: "GetSlots"; input?: never; data?: Slot[]; }; - type GetQuickChatHistoryMessage = { + type GetQuickChatHistory = { type: "GetQuickChatHistory"; input?: never; data?: Chat[]; }; - type ResetQuickChatHistoryMessage = { + type ResetQuickChatHistory = { type: "ResetQuickChatHistory"; input?: never; data?: "success"; }; - type ErrorMessage = { + type Error = { type: "Error"; input?: never; error: Error; }; type Message = - | RequestInitialDragGPTMessage - | RequestQuickChatGPTMessage - | RequestDragGPTMessage - | RequestOngoingChatGPTMessage - | ResetQuickChatHistoryMessage - | GetQuickChatHistoryMessage - | AddNewSlotMessage - | UpdateSlotMessage - | GetSlotsMessage - | GetAPIKeyMessage - | ResetAPIKeyMessage - | SelectSlotMessage - | DeleteSlotMessage - | RequestOnetimeChatGPTMessage - | RequestGenerateChatGPTPromptMessage - | SaveAPIKeyMessage; + | RequestInitialDragGPT + | RequestQuickChatGPT + | RequestDragGPT + | RequestOngoingChatGPT + | ResetQuickChatHistory + | GetQuickChatHistory + | AddNewSlot + | UpdateSlot + | GetSlots + | GetAPIKey + | ResetAPIKey + | SelectSlot + | DeleteSlot + | RequestOnetimeChatGPT + | RequestGenerateChatGPTPrompt + | SaveAPIKey; type RequestMessage = Omit; type ResponseMessage = Omit; From 195a2c31ec41faf68f7d70a38fa03a424cfeb030 Mon Sep 17 00:00:00 2001 From: Jonghakseo Date: Sun, 2 Jul 2023 20:02:56 +0900 Subject: [PATCH 3/6] add: dragChatHistoryStorage --- .../lib/storage/dragChatHistoryStorage.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/pages/background/lib/storage/dragChatHistoryStorage.ts diff --git a/src/pages/background/lib/storage/dragChatHistoryStorage.ts b/src/pages/background/lib/storage/dragChatHistoryStorage.ts new file mode 100644 index 0000000..8b37ed6 --- /dev/null +++ b/src/pages/background/lib/storage/dragChatHistoryStorage.ts @@ -0,0 +1,27 @@ +import { ILocalStorage, LocalStorage } from "@src/chrome/localStorage"; + +export class DragChatHistoryStorage { + private static DRAG_CHAT_HISTORY = "DRAG_CHAT_HISTORY"; + static storage: ILocalStorage = new LocalStorage(); + + static async getChatHistories(): Promise { + try { + const chatHistories = await this.storage.load(this.DRAG_CHAT_HISTORY); + if (Array.isArray(chatHistories)) { + return chatHistories as Chat[]; + } + } catch (e) { + return []; + } + return []; + } + + static async resetChatHistories(): Promise { + await this.storage.save(this.DRAG_CHAT_HISTORY, []); + } + + static async pushChatHistories(chatOrChats: Chat | Chat[]): Promise { + const chats = await this.getChatHistories(); + await this.storage.save(this.DRAG_CHAT_HISTORY, chats.concat(chatOrChats)); + } +} From 513cd1a329c43667c18d1b3424c9204d93bdd145 Mon Sep 17 00:00:00 2001 From: Jonghakseo Date: Sun, 6 Aug 2023 14:26:40 +0900 Subject: [PATCH 4/6] fix: save all chat history --- src/global.d.ts | 13 ++++-- src/pages/background/index.ts | 17 ++++++-- src/pages/background/lib/infra/chatGPT.ts | 19 ++++++--- .../lib/storage/chatHistoryStorage.ts | 40 +++++++++++++++++++ .../lib/storage/dragChatHistoryStorage.ts | 27 ------------- .../messageBox/ResponseMessageBox.tsx | 26 ++++++++---- src/pages/popup/pages/QuickChattingPage.tsx | 28 +++++++------ src/shared/hook/useGeneratedId.ts | 15 +++++++ src/shared/services/getGPTResponseAsStream.ts | 9 ++--- .../exhaustiveMatchingGuard.ts | 0 src/shared/utils/generateId.ts | 5 +++ src/shared/xState/streamChatStateMachine.ts | 2 +- 12 files changed, 135 insertions(+), 66 deletions(-) create mode 100644 src/pages/background/lib/storage/chatHistoryStorage.ts delete mode 100644 src/pages/background/lib/storage/dragChatHistoryStorage.ts create mode 100644 src/shared/hook/useGeneratedId.ts rename src/shared/{ts-util => ts-utils}/exhaustiveMatchingGuard.ts (100%) create mode 100644 src/shared/utils/generateId.ts diff --git a/src/global.d.ts b/src/global.d.ts index 8ec14c4..39b9bb9 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,5 +1,4 @@ import type Chrome from "chrome"; -import type { ChatCompletionRequestMessage } from "openai"; declare namespace chrome { export default Chrome; @@ -88,7 +87,7 @@ declare global { }; type RequestOngoingChatGPT = { type: "RequestOngoingChatGPT"; - input: ChatCompletionRequestMessage[]; + input: Chat[]; data?: { result: string }; }; type RequestInitialDragGPT = { @@ -98,13 +97,13 @@ declare global { }; type RequestDragGPT = { type: "RequestDragGPTStream"; - input?: ChatCompletionRequestMessage[]; + input?: { chats: Chat[]; sessionId: string }; data?: { result: string; chunk?: string; isDone?: boolean }; }; type RequestQuickChatGPT = { type: "RequestQuickChatGPTStream"; input?: { - messages: ChatCompletionRequestMessage[]; + messages: Chat[]; isGpt4: boolean; }; data?: { result: string; chunk?: string; isDone?: boolean }; @@ -139,6 +138,11 @@ declare global { input?: never; data?: "success"; }; + type SaveChatHistory = { + type: "SaveChatHistory"; + input: { chats: Chat[]; sessionId: string }; + data?: "success"; + }; type Error = { type: "Error"; input?: never; @@ -151,6 +155,7 @@ declare global { | RequestDragGPT | RequestOngoingChatGPT | ResetQuickChatHistory + | SaveChatHistory | GetQuickChatHistory | AddNewSlot | UpdateSlot diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index e4d78f3..a12a113 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -9,9 +9,10 @@ import { sendMessageToClient, } from "@src/chrome/message"; import { QuickChatHistoryStorage } from "@pages/background/lib/storage/quickChatHistoryStorage"; -import { exhaustiveMatchingGuard } from "@src/shared/ts-util/exhaustiveMatchingGuard"; +import { exhaustiveMatchingGuard } from "@src/shared/ts-utils/exhaustiveMatchingGuard"; import { createNewChatGPTSlot } from "@src/shared/slot/createNewChatGPTSlot"; import { PROMPT_GENERATE_PROMPT } from "@src/constant/promptGeneratePrompt"; +import { ChatHistoryStorage } from "@pages/background/lib/storage/chatHistoryStorage"; reloadOnUpdate("pages/background"); @@ -166,9 +167,10 @@ chrome.runtime.onConnect.addListener((port) => { } case "RequestDragGPTStream": { const apiKey = await ApiKeyStorage.getApiKey(); + const slot = await SlotStorage.getSelectedSlot(); const response = await chatGPT({ - chats: message.input, - slot: { type: "ChatGPT" }, + chats: message.input?.chats, + slot: { type: slot.type }, apiKey, onDelta: (chunk) => { sendResponse({ @@ -223,6 +225,15 @@ chrome.runtime.onConnect.addListener((port) => { sendResponse({ type: "ResetQuickChatHistory", data: "success" }); break; } + case "SaveChatHistory": { + await ChatHistoryStorage.pushChatHistories( + message.input.sessionId, + message.input.chats + ); + console.log(await ChatHistoryStorage.getChatHistories()); + sendResponse({ type: "SaveChatHistory", data: "success" }); + break; + } default: { exhaustiveMatchingGuard(message); } diff --git a/src/pages/background/lib/infra/chatGPT.ts b/src/pages/background/lib/infra/chatGPT.ts index 64fb37a..c9b3922 100644 --- a/src/pages/background/lib/infra/chatGPT.ts +++ b/src/pages/background/lib/infra/chatGPT.ts @@ -18,7 +18,7 @@ export async function chatGPT({ onDelta, }: { slot: ChatGPTSlot; - chats?: ChatCompletionRequestMessage[]; + chats?: Chat[]; input?: string; apiKey: string; onDelta?: (chunk: string) => unknown; @@ -32,7 +32,7 @@ export async function chatGPT({ }); } if (hasChats(chats)) { - messages.push(...chats); + messages.push(...convertChatsToMessages(chats)); } if (input) { messages.push({ role: "user", content: input }); @@ -131,8 +131,17 @@ async function requestApi(apiKey: string, body: CreateChatCompletionRequest) { }); } -function hasChats( - chats?: ChatCompletionRequestMessage[] -): chats is ChatCompletionRequestMessage[] { +function hasChats(chats?: Chat[]): chats is Chat[] { return chats !== undefined && chats.length > 0; } + +function convertChatsToMessages(chats: Chat[]): ChatCompletionRequestMessage[] { + return chats + .filter((chat) => chat.role !== "error") + .map((chat) => { + return { + role: chat.role === "user" ? "user" : "assistant", + content: chat.content, + }; + }); +} diff --git a/src/pages/background/lib/storage/chatHistoryStorage.ts b/src/pages/background/lib/storage/chatHistoryStorage.ts new file mode 100644 index 0000000..6d97ee5 --- /dev/null +++ b/src/pages/background/lib/storage/chatHistoryStorage.ts @@ -0,0 +1,40 @@ +import { ILocalStorage, LocalStorage } from "@src/chrome/localStorage"; + +type ChatHistories = Record; + +const EMPTY_CHAT_HISTORIES: ChatHistories = {}; + +export class ChatHistoryStorage { + private static CHAT_HISTORY_KEY = "CHAT_HISTORY"; + static storage: ILocalStorage = new LocalStorage(); + + static async getChatHistories(): Promise { + try { + return (await this.storage.load(this.CHAT_HISTORY_KEY)) as ChatHistories; + } catch (e) { + return EMPTY_CHAT_HISTORIES; + } + } + + static async getChatHistory(sessionId: string): Promise { + const chatHistories = await this.getChatHistories(); + return chatHistories[sessionId] || []; + } + + static async resetChatHistories(): Promise { + await this.storage.save(this.CHAT_HISTORY_KEY, EMPTY_CHAT_HISTORIES); + } + + static async pushChatHistories( + sessionId: string, + chatOrChats: Chat | Chat[] + ): Promise { + const chats = await this.getChatHistories(); + await this.storage.save(this.CHAT_HISTORY_KEY, { + ...chats, + [sessionId]: chats[sessionId] + ? chats[sessionId].concat(chatOrChats) + : [chatOrChats], + }); + } +} diff --git a/src/pages/background/lib/storage/dragChatHistoryStorage.ts b/src/pages/background/lib/storage/dragChatHistoryStorage.ts deleted file mode 100644 index 8b37ed6..0000000 --- a/src/pages/background/lib/storage/dragChatHistoryStorage.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ILocalStorage, LocalStorage } from "@src/chrome/localStorage"; - -export class DragChatHistoryStorage { - private static DRAG_CHAT_HISTORY = "DRAG_CHAT_HISTORY"; - static storage: ILocalStorage = new LocalStorage(); - - static async getChatHistories(): Promise { - try { - const chatHistories = await this.storage.load(this.DRAG_CHAT_HISTORY); - if (Array.isArray(chatHistories)) { - return chatHistories as Chat[]; - } - } catch (e) { - return []; - } - return []; - } - - static async resetChatHistories(): Promise { - await this.storage.save(this.DRAG_CHAT_HISTORY, []); - } - - static async pushChatHistories(chatOrChats: Chat | Chat[]): Promise { - const chats = await this.getChatHistories(); - await this.storage.save(this.DRAG_CHAT_HISTORY, chats.concat(chatOrChats)); - } -} diff --git a/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx b/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx index 08020b2..d2b7192 100644 --- a/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx +++ b/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx @@ -6,7 +6,6 @@ import { FormEventHandler } from "react"; import { HStack, Input, Text, VStack } from "@chakra-ui/react"; import DraggableBox from "@pages/content/src/ContentScriptApp/components/DraggableBox"; import { useMachine } from "@xstate/react"; -import { ChatCompletionRequestMessage } from "openai"; import ChatText from "@src/shared/component/ChatText"; import AssistantChat from "@src/shared/component/AssistantChat"; import UserChat from "@src/shared/component/UserChat"; @@ -16,6 +15,8 @@ import { t } from "@src/chrome/i18n"; import { DragHandleIcon } from "@chakra-ui/icons"; import streamChatStateMachine from "@src/shared/xState/streamChatStateMachine"; import { getDragGPTResponseAsStream } from "@src/shared/services/getGPTResponseAsStream"; +import { sendMessageToBackgroundAsync } from "@src/chrome/message"; +import useGeneratedId from "@src/shared/hook/useGeneratedId"; type ResponseMessageBoxProps = Omit< MessageBoxProps, @@ -29,18 +30,27 @@ export default function ResponseMessageBox({ onClose, ...restProps }: ResponseMessageBoxProps) { + const { id: sessionId } = useGeneratedId(); const [state, send] = useMachine(streamChatStateMachine, { services: { - getChatHistoryFromBackground: () => Promise.resolve(initialChats), + getChatHistoryFromBackground: async () => { + void sendMessageToBackgroundAsync({ + type: "SaveChatHistory", + input: { sessionId, chats: initialChats }, + }); + return initialChats; + }, getGPTResponse: (context) => { return getDragGPTResponseAsStream({ - messages: context.chats.filter( - (chat) => chat.role !== "error" - ) as ChatCompletionRequestMessage[], - onDelta: (chunk) => { - send("RECEIVE_ING", { data: chunk }); + input: { chats: context.chats, sessionId }, + onDelta: (chunk) => send("RECEIVE_ING", { data: chunk }), + onFinish: (result) => { + void sendMessageToBackgroundAsync({ + type: "SaveChatHistory", + input: { sessionId, chats: context.chats }, + }); + send("RECEIVE_DONE", { data: result }); }, - onFinish: (result) => send("RECEIVE_DONE", { data: result }), }); }, }, diff --git a/src/pages/popup/pages/QuickChattingPage.tsx b/src/pages/popup/pages/QuickChattingPage.tsx index feb6b5d..b66ea9e 100644 --- a/src/pages/popup/pages/QuickChattingPage.tsx +++ b/src/pages/popup/pages/QuickChattingPage.tsx @@ -1,21 +1,18 @@ import { Button, - FormControl, FormLabel, HStack, Switch, - Text, Textarea, VStack, } from "@chakra-ui/react"; import StyledButton from "@pages/popup/components/StyledButton"; import { useMachine } from "@xstate/react"; -import { ChatCompletionRequestMessage } from "openai"; import { sendMessageToBackground, sendMessageToBackgroundAsync, } from "@src/chrome/message"; -import { FormEventHandler, KeyboardEventHandler, useState } from "react"; +import { FormEventHandler, KeyboardEventHandler, useId } from "react"; import UserChat from "@src/shared/component/UserChat"; import ChatText from "@src/shared/component/ChatText"; import AssistantChat from "@src/shared/component/AssistantChat"; @@ -25,12 +22,9 @@ import { useCopyClipboard } from "@src/shared/hook/useCopyClipboard"; import streamChatStateMachine from "@src/shared/xState/streamChatStateMachine"; import { getQuickGPTResponseAsStream } from "@src/shared/services/getGPTResponseAsStream"; import { COLORS } from "@src/constant/style"; +import generateId from "@src/shared/utils/generateId"; +import useGeneratedId from "@src/shared/hook/useGeneratedId"; -async function getChatHistoryFromBackground() { - return await sendMessageToBackgroundAsync({ - type: "GetQuickChatHistory", - }); -} function resetChatHistoriesFromBackground() { sendMessageToBackground({ message: { @@ -46,15 +40,18 @@ type QuickChattingPageProps = { export default function QuickChattingPage({ onClickBackButton, }: QuickChattingPageProps) { + const { id: sessionId, regenerate: regenerateSessionId } = useGeneratedId(); const [state, send] = useMachine(streamChatStateMachine, { services: { - getChatHistoryFromBackground, + getChatHistoryFromBackground: () => { + return sendMessageToBackgroundAsync({ + type: "GetQuickChatHistory", + }); + }, getGPTResponse: (context) => { return getQuickGPTResponseAsStream({ isGpt4: context.isGpt4, - messages: context.chats.filter( - (chat) => chat.role !== "error" - ) as ChatCompletionRequestMessage[], + messages: context.chats, onDelta: (chunk) => { send("RECEIVE_ING", { data: chunk }); }, @@ -65,7 +62,12 @@ export default function QuickChattingPage({ actions: { exitChatting: onClickBackButton, resetChatData: (context) => { + void sendMessageToBackgroundAsync({ + type: "SaveChatHistory", + input: { chats: context.chats, sessionId }, + }); context.chats = []; + regenerateSessionId(); resetChatHistoriesFromBackground(); }, }, diff --git a/src/shared/hook/useGeneratedId.ts b/src/shared/hook/useGeneratedId.ts new file mode 100644 index 0000000..2508a88 --- /dev/null +++ b/src/shared/hook/useGeneratedId.ts @@ -0,0 +1,15 @@ +import { useRef } from "react"; +import generateId from "@src/shared/utils/generateId"; + +export default function useGeneratedId() { + const idRef = useRef(generateId()); + + const regenerate = () => { + idRef.current = generateId(); + }; + + return { + id: idRef.current, + regenerate, + }; +} diff --git a/src/shared/services/getGPTResponseAsStream.ts b/src/shared/services/getGPTResponseAsStream.ts index 0bc3b54..16bc3db 100644 --- a/src/shared/services/getGPTResponseAsStream.ts +++ b/src/shared/services/getGPTResponseAsStream.ts @@ -1,4 +1,3 @@ -import { ChatCompletionRequestMessage } from "openai"; import { sendMessageToBackground } from "@src/chrome/message"; export async function getQuickGPTResponseAsStream({ @@ -7,7 +6,7 @@ export async function getQuickGPTResponseAsStream({ onDelta, onFinish, }: { - messages: ChatCompletionRequestMessage[]; + messages: Chat[]; isGpt4: boolean; onDelta: (chunk: string) => unknown; onFinish: (result: string) => unknown; @@ -33,11 +32,11 @@ export async function getQuickGPTResponseAsStream({ } export async function getDragGPTResponseAsStream({ - messages, + input, onDelta, onFinish, }: { - messages: ChatCompletionRequestMessage[]; + input: { chats: Chat[]; sessionId: string }; onDelta: (chunk: string) => unknown; onFinish: (result: string) => unknown; }) { @@ -46,7 +45,7 @@ export async function getDragGPTResponseAsStream({ const { disconnect } = sendMessageToBackground({ message: { type: "RequestDragGPTStream", - input: messages, + input, }, handleSuccess: (response) => { if (response.isDone || !response.chunk) { diff --git a/src/shared/ts-util/exhaustiveMatchingGuard.ts b/src/shared/ts-utils/exhaustiveMatchingGuard.ts similarity index 100% rename from src/shared/ts-util/exhaustiveMatchingGuard.ts rename to src/shared/ts-utils/exhaustiveMatchingGuard.ts diff --git a/src/shared/utils/generateId.ts b/src/shared/utils/generateId.ts new file mode 100644 index 0000000..e150d0b --- /dev/null +++ b/src/shared/utils/generateId.ts @@ -0,0 +1,5 @@ +export default function generateId() { + return Number( + String(new Date().getTime() + Math.random()).replace(".", "") + ).toString(36); +} diff --git a/src/shared/xState/streamChatStateMachine.ts b/src/shared/xState/streamChatStateMachine.ts index c060b19..63d68fa 100644 --- a/src/shared/xState/streamChatStateMachine.ts +++ b/src/shared/xState/streamChatStateMachine.ts @@ -47,7 +47,7 @@ const streamChatStateMachine = createMachine( init: { invoke: { src: "getChatHistoryFromBackground", - onDone: { target: "idle", actions: "setChats" }, + onDone: { target: "idle", actions: ["setChats"] }, onError: { target: "idle" }, }, }, From 7f080f9c9e6de40b365f8ef4e010d93073171e54 Mon Sep 17 00:00:00 2001 From: Jonghakseo Date: Sun, 6 Aug 2023 16:59:11 +0900 Subject: [PATCH 5/6] wip: options page --- src/chrome/message.ts | 2 +- src/global.d.ts | 11 ++- src/pages/background/index.ts | 9 ++- .../lib/storage/chatHistoryStorage.ts | 29 ++++--- .../content/src/ContentScriptApp/App.tsx | 6 +- .../components/GPTRequestButton.tsx | 4 +- .../messageBox/ResponseMessageBox.tsx | 79 ++++++++----------- src/pages/options/src/App.tsx | 36 ++++++--- .../layout/ChatHistoryMainLayout.tsx | 24 ++++++ src/pages/options/src/pages/Main.tsx | 44 +++++++++++ src/pages/popup/components/StyledButton.tsx | 5 +- src/pages/popup/pages/QuickChattingPage.tsx | 48 +++-------- src/pages/popup/style/StyleProvider.tsx | 16 +++- src/shared/component/AssistantChat.tsx | 10 ++- src/shared/component/UserChat.tsx | 10 ++- src/shared/hook/useBackgroundMessage.tsx | 44 +++++++++++ src/shared/hook/useGeneratedId.ts | 4 +- 17 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 src/pages/options/src/components/layout/ChatHistoryMainLayout.tsx create mode 100644 src/pages/options/src/pages/Main.tsx create mode 100644 src/shared/hook/useBackgroundMessage.tsx diff --git a/src/chrome/message.ts b/src/chrome/message.ts index d792b3b..b360415 100644 --- a/src/chrome/message.ts +++ b/src/chrome/message.ts @@ -1,4 +1,4 @@ -type GetDataType = Exclude< +export type GetDataType = Exclude< Extract< Message, { diff --git a/src/global.d.ts b/src/global.d.ts index 39b9bb9..fa6fd89 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,4 +1,5 @@ import type Chrome from "chrome"; +import { ChatHistories } from "@pages/background/lib/storage/chatHistoryStorage"; declare namespace chrome { export default Chrome; @@ -140,10 +141,15 @@ declare global { }; type SaveChatHistory = { type: "SaveChatHistory"; - input: { chats: Chat[]; sessionId: string }; + input: { chats: Chat[]; sessionId: string; type: "Quick" | "Drag" }; data?: "success"; }; - type Error = { + type GetAllChatHistory = { + type: "GetAllChatHistory"; + input?: never; + data?: ChatHistories; + }; + type ErrorMessage = { type: "Error"; input?: never; error: Error; @@ -156,6 +162,7 @@ declare global { | RequestOngoingChatGPT | ResetQuickChatHistory | SaveChatHistory + | GetAllChatHistory | GetQuickChatHistory | AddNewSlot | UpdateSlot diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index a12a113..f9535d6 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -228,12 +228,17 @@ chrome.runtime.onConnect.addListener((port) => { case "SaveChatHistory": { await ChatHistoryStorage.pushChatHistories( message.input.sessionId, - message.input.chats + message.input.chats, + message.input.type ); - console.log(await ChatHistoryStorage.getChatHistories()); sendResponse({ type: "SaveChatHistory", data: "success" }); break; } + case "GetAllChatHistory": { + const allChatHistories = await ChatHistoryStorage.getChatHistories(); + sendResponse({ type: "GetAllChatHistory", data: allChatHistories }); + break; + } default: { exhaustiveMatchingGuard(message); } diff --git a/src/pages/background/lib/storage/chatHistoryStorage.ts b/src/pages/background/lib/storage/chatHistoryStorage.ts index 6d97ee5..9ce4199 100644 --- a/src/pages/background/lib/storage/chatHistoryStorage.ts +++ b/src/pages/background/lib/storage/chatHistoryStorage.ts @@ -1,6 +1,12 @@ import { ILocalStorage, LocalStorage } from "@src/chrome/localStorage"; -type ChatHistories = Record; +type SessionId = string; +type SessionHistories = { + history: Chat[]; + updatedAt: number; + type?: "Quick" | "Drag"; +}; +export type ChatHistories = Record; const EMPTY_CHAT_HISTORIES: ChatHistories = {}; @@ -16,9 +22,9 @@ export class ChatHistoryStorage { } } - static async getChatHistory(sessionId: string): Promise { + static async getChatHistory(sessionId: string): Promise { const chatHistories = await this.getChatHistories(); - return chatHistories[sessionId] || []; + return chatHistories[sessionId] || { history: [], updatedAt: 0 }; } static async resetChatHistories(): Promise { @@ -27,14 +33,19 @@ export class ChatHistoryStorage { static async pushChatHistories( sessionId: string, - chatOrChats: Chat | Chat[] + chatOrChats: Chat | Chat[], + type?: "Quick" | "Drag" ): Promise { - const chats = await this.getChatHistories(); + const chatHistories = await this.getChatHistories(); + const sessionHistories = await this.getChatHistory(sessionId); await this.storage.save(this.CHAT_HISTORY_KEY, { - ...chats, - [sessionId]: chats[sessionId] - ? chats[sessionId].concat(chatOrChats) - : [chatOrChats], + ...chatHistories, + [sessionId]: { + ...sessionHistories, + history: sessionHistories.history.concat(chatOrChats), + updatedAt: Date.now(), + type: type ? type : sessionHistories.type, + }, }); } } diff --git a/src/pages/content/src/ContentScriptApp/App.tsx b/src/pages/content/src/ContentScriptApp/App.tsx index 46d0cd1..4711280 100644 --- a/src/pages/content/src/ContentScriptApp/App.tsx +++ b/src/pages/content/src/ContentScriptApp/App.tsx @@ -2,13 +2,17 @@ import DragGPT from "@pages/content/src/ContentScriptApp/DragGPT"; import EmotionCacheProvider from "@pages/content/src/ContentScriptApp/emotion/EmotionCacheProvider"; import ResetStyleProvider from "@pages/content/src/ContentScriptApp/emotion/ResetStyleProvider"; import FontProvider from "@pages/content/src/ContentScriptApp/emotion/FontProvider"; +import { CSSReset, theme, ThemeProvider } from "@chakra-ui/react"; export default function App() { return ( - + + + + diff --git a/src/pages/content/src/ContentScriptApp/components/GPTRequestButton.tsx b/src/pages/content/src/ContentScriptApp/components/GPTRequestButton.tsx index ab054ff..406164a 100644 --- a/src/pages/content/src/ContentScriptApp/components/GPTRequestButton.tsx +++ b/src/pages/content/src/ContentScriptApp/components/GPTRequestButton.tsx @@ -79,9 +79,9 @@ export default function GPTRequestButton({ {...restProps} > {loading ? ( - + ) : ( - + )} diff --git a/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx b/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx index d2b7192..d141b35 100644 --- a/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx +++ b/src/pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox.tsx @@ -3,7 +3,7 @@ import MessageBox, { } from "@pages/content/src/ContentScriptApp/components/messageBox/MessageBox"; import StyledButton from "@pages/popup/components/StyledButton"; import { FormEventHandler } from "react"; -import { HStack, Input, Text, VStack } from "@chakra-ui/react"; +import { Button, HStack, Input, Text, VStack } from "@chakra-ui/react"; import DraggableBox from "@pages/content/src/ContentScriptApp/components/DraggableBox"; import { useMachine } from "@xstate/react"; import ChatText from "@src/shared/component/ChatText"; @@ -30,13 +30,13 @@ export default function ResponseMessageBox({ onClose, ...restProps }: ResponseMessageBoxProps) { - const { id: sessionId } = useGeneratedId(); + const { id: sessionId } = useGeneratedId("drag_"); const [state, send] = useMachine(streamChatStateMachine, { services: { getChatHistoryFromBackground: async () => { void sendMessageToBackgroundAsync({ type: "SaveChatHistory", - input: { sessionId, chats: initialChats }, + input: { sessionId, chats: initialChats, type: "Drag" }, }); return initialChats; }, @@ -47,7 +47,7 @@ export default function ResponseMessageBox({ onFinish: (result) => { void sendMessageToBackgroundAsync({ type: "SaveChatHistory", - input: { sessionId, chats: context.chats }, + input: { sessionId, chats: context.chats, type: "Drag" }, }); send("RECEIVE_DONE", { data: result }); }, @@ -100,13 +100,13 @@ export default function ResponseMessageBox({ display="flex" alignItems="center" width="100%" - height={24} + height="24px" color="white" fontWeight="bold" cursor="move" className={DraggableBox.handlerClassName} > - + {t("responseMessageBox_responseTitle")} } @@ -121,30 +121,16 @@ export default function ResponseMessageBox({ overflowY="scroll" > {chats.map((chat, index) => ( - + ))} } footer={ - // TODO refactor - - - {isCopied - ? t("responseMessageBox_copyButtonText_copied") - : t("responseMessageBox_copyButtonText_copy")} - - {isReceiving && ( - - {t("responseMessageBox_stopButtonText")} - - )} - + + @@ -153,11 +139,31 @@ export default function ResponseMessageBox({ onKeyDown={(e) => e.stopPropagation()} /> - + + + + + {isReceiving && ( + + )} - + } {...restProps} /> @@ -165,20 +171,7 @@ export default function ResponseMessageBox({ } // TODO refactor -const ChatBox = ({ - chat, - isLastAndResponse, -}: { - chat: Chat; - isLastAndResponse: boolean; -}) => { - if (isLastAndResponse) { - return ( - - {chat.content} - - ); - } +export const ChatBox = ({ chat }: { chat: Chat }) => { if (chat.role === "error") { return ( @@ -189,11 +182,9 @@ const ChatBox = ({ if (chat.role === "assistant") { return ( - // {chat.content} - // ); } diff --git a/src/pages/options/src/App.tsx b/src/pages/options/src/App.tsx index 6f5112a..e5a2369 100644 --- a/src/pages/options/src/App.tsx +++ b/src/pages/options/src/App.tsx @@ -1,18 +1,36 @@ -import { ChakraProvider, theme, ThemeProvider } from "@chakra-ui/react"; -import { FC } from "react"; -import ResetStyleProvider from "@src/shared/component/ResetStyleProvider"; +import { + ChakraProvider, + ColorModeScript, + CSSReset, + DarkMode, + extendTheme, + Spinner, + ThemeConfig, +} from "@chakra-ui/react"; +import { FC, Suspense } from "react"; import FontProvider from "@src/shared/component/FontProvider"; +import OptionMainPage from "@pages/options/src/pages/Main"; + +const config: ThemeConfig = { + initialColorMode: "dark", + useSystemColorMode: false, +}; + +const theme = extendTheme({ config }); const App: FC = () => { return ( - - - + + + + {/* TODO router */} - Options + }> + + - - + + ); }; diff --git a/src/pages/options/src/components/layout/ChatHistoryMainLayout.tsx b/src/pages/options/src/components/layout/ChatHistoryMainLayout.tsx new file mode 100644 index 0000000..a3dd4ae --- /dev/null +++ b/src/pages/options/src/components/layout/ChatHistoryMainLayout.tsx @@ -0,0 +1,24 @@ +import styled from "@emotion/styled"; + +const Container = styled.main` + display: grid; + gap: 12px; + grid-template-columns: 300px auto; +`; + +type ChatHistoryMainLayoutProps = { + Nav: React.ReactNode; + ChatHistory: React.ReactNode; +}; + +export default function ChatHistoryMainLayout({ + Nav, + ChatHistory, +}: ChatHistoryMainLayoutProps) { + return ( + + +
{ChatHistory}
+
+ ); +} diff --git a/src/pages/options/src/pages/Main.tsx b/src/pages/options/src/pages/Main.tsx new file mode 100644 index 0000000..d950158 --- /dev/null +++ b/src/pages/options/src/pages/Main.tsx @@ -0,0 +1,44 @@ +import useBackgroundMessage from "@src/shared/hook/useBackgroundMessage"; +import ChatHistoryMainLayout from "@pages/options/src/components/layout/ChatHistoryMainLayout"; +import { Menu, MenuGroup, MenuItem, Text, VStack } from "@chakra-ui/react"; +import { useState } from "react"; +import ChatText from "@src/shared/component/ChatText"; +import { ChatBox } from "@pages/content/src/ContentScriptApp/components/messageBox/ResponseMessageBox"; + +export default function OptionMainPage() { + const chatHistories = useBackgroundMessage({ + type: "GetAllChatHistory", + }); + const [selectedSessionId, setSelectedSessionId] = useState(""); + return ( + + + {Object.entries(chatHistories).map(([id, chats]) => { + return ( + setSelectedSessionId(id)}> +
+ + {chats.type} + + + {new Date(chats.updatedAt).toLocaleString()} + +
+
+ ); + })} +
+ + } + ChatHistory={ + + {chatHistories[selectedSessionId]?.history.map((chat, index) => ( + + ))} + + } + /> + ); +} diff --git a/src/pages/popup/components/StyledButton.tsx b/src/pages/popup/components/StyledButton.tsx index 9adb51c..deb7149 100644 --- a/src/pages/popup/components/StyledButton.tsx +++ b/src/pages/popup/components/StyledButton.tsx @@ -58,10 +58,13 @@ type StyledButtonProps = ButtonProps; const StyledButton = ({ ...restProps }: StyledButtonProps) => { return (