From e4c3dff0b36dd621bf7aa1828c11472294de12e5 Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Thu, 18 Apr 2024 13:18:55 +0700 Subject: [PATCH] ChatGPT prompt history (#35) * ChatGPT prompt history --- src/api/api.ts | 5 + .../ai-mass-creation-form.tsx | 18 ++- .../ai-mass-creation-screen.tsx | 4 + .../cards-generated-screen.tsx | 2 +- .../previous-prompts-screen.tsx | 108 ++++++++++++++++++ .../store/ai-mass-creation-store.test.ts | 1 + .../store/ai-mass-creation-store.ts | 33 +++++- .../deck-form/store/deck-form-store.ts | 22 ++-- src/store/deck-list-store.ts | 8 +- src/translations/t.ts | 12 ++ src/ui/full-screen-loader.tsx | 11 +- 11 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 src/screens/ai-mass-creation/previous-prompts-screen.tsx diff --git a/src/api/api.ts b/src/api/api.ts index 7ee6811d..47081703 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -64,6 +64,7 @@ import { AiMassGenerateRequest, AiMassGenerateResponse, } from "../../functions/ai-mass-generate.ts"; +import { UserPreviousPromptsResponse } from "../../functions/user-previous-prompts.ts"; export const healthRequest = () => { return request("/health"); @@ -246,3 +247,7 @@ export const addCardsMultipleRequest = (body: AddCardsMultipleRequest) => { body, ); }; + +export const userPreviousPromptsRequest = () => { + return request("/user-previous-prompts"); +}; diff --git a/src/screens/ai-mass-creation/ai-mass-creation-form.tsx b/src/screens/ai-mass-creation/ai-mass-creation-form.tsx index c05385b5..9df5acca 100644 --- a/src/screens/ai-mass-creation/ai-mass-creation-form.tsx +++ b/src/screens/ai-mass-creation/ai-mass-creation-form.tsx @@ -39,7 +39,7 @@ export const AiMassCreationForm = observer(() => { text: t("how"), icon: ( ), @@ -70,6 +70,18 @@ export const AiMassCreationForm = observer(() => { store.goApiKeysScreen(); }, }, + { + text: t("ai_cards_previous_prompts"), + icon: ( + + ), + onClick: () => { + store.screen.onChange("previousPrompts"); + }, + }, ]} /> {promptForm.apiKey.isTouched && promptForm.apiKey.error && ( @@ -88,6 +100,10 @@ export const AiMassCreationForm = observer(() => { + + ); }); diff --git a/src/screens/ai-mass-creation/ai-mass-creation-screen.tsx b/src/screens/ai-mass-creation/ai-mass-creation-screen.tsx index c12d0429..25dc2f0b 100644 --- a/src/screens/ai-mass-creation/ai-mass-creation-screen.tsx +++ b/src/screens/ai-mass-creation/ai-mass-creation-screen.tsx @@ -6,6 +6,7 @@ import { HowMassCreationWorksScreen } from "./how-mass-creation-works-screen.tsx import { ApiKeysScreen } from "./api-keys-screen.tsx"; import { useMount } from "../../lib/react/use-mount.ts"; import { CardsGeneratedScreen } from "./cards-generated-screen.tsx"; +import { PreviousPromptsScreen } from "./previous-prompts-screen.tsx"; export const AiMassCreationScreen = observer(() => { const store = useAiMassCreationStore(); @@ -23,5 +24,8 @@ export const AiMassCreationScreen = observer(() => { if (store.screen.value === "cardsGenerated") { return ; } + if (store.screen.value === "previousPrompts") { + return ; + } return ; }); diff --git a/src/screens/ai-mass-creation/cards-generated-screen.tsx b/src/screens/ai-mass-creation/cards-generated-screen.tsx index f639de18..27aeb2ec 100644 --- a/src/screens/ai-mass-creation/cards-generated-screen.tsx +++ b/src/screens/ai-mass-creation/cards-generated-screen.tsx @@ -41,7 +41,7 @@ export const CardsGeneratedScreen = observer(() => { return ( diff --git a/src/screens/ai-mass-creation/previous-prompts-screen.tsx b/src/screens/ai-mass-creation/previous-prompts-screen.tsx new file mode 100644 index 00000000..f2e0c705 --- /dev/null +++ b/src/screens/ai-mass-creation/previous-prompts-screen.tsx @@ -0,0 +1,108 @@ +import { observer, useLocalStore } from "mobx-react-lite"; +import { Screen } from "../shared/screen.tsx"; +import { useAiMassCreationStore } from "./store/ai-mass-creation-store-provider.tsx"; +import { useMount } from "../../lib/react/use-mount.ts"; +import React from "react"; +import { ScreenLoader } from "../../ui/full-screen-loader.tsx"; +import { useBackButton } from "../../lib/telegram/use-back-button.tsx"; +import { Flex } from "../../ui/flex.tsx"; +import { css, cx } from "@emotion/css"; +import { theme } from "../../ui/theme.tsx"; +import { useMainButton } from "../../lib/telegram/use-main-button.tsx"; +import { TextField } from "mobx-form-lite"; +import { boolNarrow } from "../../lib/typescript/bool-narrow.ts"; +import { t } from "../../translations/t.ts"; + +export const PreviousPromptsScreen = observer(() => { + const store = useAiMassCreationStore(); + const localStore = useLocalStore(() => ({ + selectedIndex: new TextField(null), + get isMainButtonVisible() { + return localStore.selectedIndex.value !== null; + }, + })); + + useMount(() => { + store.userPreviousPromptsRequest.execute(); + }); + + useBackButton(() => { + store.screen.onChange(null); + }); + + useMainButton( + t("ai_cards_use_template"), + () => { + store.usePreviousPrompt(localStore.selectedIndex); + }, + () => localStore.isMainButtonVisible, + ); + + return ( + + + {store.userPreviousPromptsRequest.isLoading && } + {store.userPreviousPromptsRequest.result.status === "success" && ( + <> + {store.userPreviousPromptsRequest.result.data.map((log, i) => { + const secondaryFields = [ + log.payload.frontPrompt, + log.payload.backPrompt, + log.payload.examplePrompt, + ].filter(boolNarrow); + + const isSelected = localStore.selectedIndex.value === i; + + return ( +
{ + localStore.selectedIndex.onChange(i); + }} + > + +
+ {log.payload.prompt} +
+ {secondaryFields.map((field, i) => { + return ( +
+ {field} +
+ ); + })} +
+
+ ); + })} + + )} +
+
+ ); +}); diff --git a/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts b/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts index b2ed6097..ed475d27 100644 --- a/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts +++ b/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts @@ -7,6 +7,7 @@ const aiUserCredentialsCheckRequestMock = vi.hoisted(() => vi.fn()); vi.mock("../../../api/api.ts", () => { return { + userPreviousPromptsRequest: vi.fn(() => Promise.resolve()), aiUserCredentialsCheckRequest: aiUserCredentialsCheckRequestMock, upsertUserAiCredentialsRequest: vi.fn(() => Promise.resolve()), aiMassGenerateRequest: vi.fn(() => Promise.resolve()), diff --git a/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts b/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts index 896587c7..641f0093 100644 --- a/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts +++ b/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts @@ -13,6 +13,7 @@ import { aiMassGenerateRequest, aiUserCredentialsCheckRequest, upsertUserAiCredentialsRequest, + userPreviousPromptsRequest, } from "../../../api/api.ts"; import { RequestStore } from "../../../lib/mobx-request/request-store.ts"; import { screenStore } from "../../../store/screen-store.ts"; @@ -43,6 +44,8 @@ export const chatGptModels = [ type ChatGptModel = (typeof chatGptModels)[number]; +type InnerScreen = "how" | "apiKeys" | "cardsGenerated" | "previousPrompts"; + export class AiMassCreationStore { upsertUserAiCredentialsRequest = new RequestStore( upsertUserAiCredentialsRequest, @@ -50,8 +53,9 @@ export class AiMassCreationStore { isApiKeysSetRequest = new RequestStore(aiUserCredentialsCheckRequest); aiMassGenerateRequest = new RequestStore(aiMassGenerateRequest); addCardsMultipleRequest = new RequestStore(addCardsMultipleRequest); + userPreviousPromptsRequest = new RequestStore(userPreviousPromptsRequest); - screen = new TextField<"how" | "apiKeys" | "cardsGenerated" | null>(null); + screen = new TextField(null); forceUpdateApiKey = new BooleanToggle(false); promptForm = { @@ -64,6 +68,7 @@ export class AiMassCreationStore { backPrompt: new TextField("", { validate: validators.required(t("validation_required")), }), + examplePrompt: new TextField(""), // A field to just show error on submit apiKey: new TextField("", { validate: () => { @@ -86,7 +91,11 @@ export class AiMassCreationStore { }; massCreationForm?: { - cards: ListField<{ front: string; back: string }>; + cards: ListField<{ + front: string; + back: string; + example: string | null | undefined; + }>; }; constructor() { @@ -159,6 +168,22 @@ export class AiMassCreationStore { return this.massCreationForm.cards.value.length > 1; } + usePreviousPrompt(index: TextField) { + assert( + this.userPreviousPromptsRequest.result.status === "success", + "Invalid status", + ); + assert(index.value !== null, "Empty index"); + const log = this.userPreviousPromptsRequest.result.data[index.value]; + assert(log, "Invalid log index"); + this.promptForm.prompt.onChange(log.payload.prompt); + this.promptForm.frontPrompt.onChange(log.payload.frontPrompt); + this.promptForm.backPrompt.onChange(log.payload.backPrompt); + const examplePrompt = log.payload.examplePrompt || ""; + this.promptForm.examplePrompt.onChange(examplePrompt); + this.screen.onChange(null); + } + submitApiKeysForm() { if (!isFormValid(this.apiKeysForm)) { formTouchAll(this.apiKeysForm); @@ -187,6 +212,7 @@ export class AiMassCreationStore { prompt: this.promptForm.prompt.value, frontPrompt: this.promptForm.frontPrompt.value, backPrompt: this.promptForm.backPrompt.value, + examplePrompt: this.promptForm.examplePrompt.value, }); if (result.status === "error") { @@ -197,10 +223,11 @@ export class AiMassCreationStore { const innerResult = result.data; if (innerResult.data) { this.massCreationForm = { - cards: new ListField<{ front: string; back: string }>( + cards: new ListField( innerResult.data.cards.map((card) => ({ front: card.front, back: card.back, + example: card.example, })), ), }; diff --git a/src/screens/deck-form/store/deck-form-store.ts b/src/screens/deck-form/store/deck-form-store.ts index abddb011..ecc38435 100644 --- a/src/screens/deck-form/store/deck-form-store.ts +++ b/src/screens/deck-form/store/deck-form-store.ts @@ -468,17 +468,19 @@ export class DeckFormStore implements CardFormStoreInterface { return; } - const selectedCard = this.cardForm; - if (!selectedCard) { - return; - } - assert(this.form, "markCardAsRemoved: form is empty"); - if (!selectedCard.id) { - return; - } - this.form.cardsToRemoveIds.push(selectedCard.id); + runInAction(() => { + const selectedCard = this.cardForm; + if (!selectedCard) { + return; + } + assert(this.form, "markCardAsRemoved: form is empty"); + if (!selectedCard.id) { + return; + } - deckListStore.isFullScreenLoaderVisible = true; + this.form.cardsToRemoveIds.push(selectedCard.id); + deckListStore.isFullScreenLoaderVisible = true; + }); this.onDeckSave( action(() => { diff --git a/src/store/deck-list-store.ts b/src/store/deck-list-store.ts index 881efe04..e6b15c6e 100644 --- a/src/store/deck-list-store.ts +++ b/src/store/deck-list-store.ts @@ -131,7 +131,9 @@ export class DeckListStore { } hapticImpact("heavy"); - this.isFullScreenLoaderVisible = true; + runInAction(() => { + this.isFullScreenLoaderVisible = true; + }); duplicateDeckRequest(deckId) .then(() => { screenStore.go({ type: "main" }); @@ -150,7 +152,9 @@ export class DeckListStore { } hapticImpact("heavy"); - this.isFullScreenLoaderVisible = true; + runInAction(() => { + this.isFullScreenLoaderVisible = true; + }); duplicateFolderRequest(folderId) .then(() => { screenStore.go({ type: "main" }); diff --git a/src/translations/t.ts b/src/translations/t.ts index dc555cec..1664aa28 100644 --- a/src/translations/t.ts +++ b/src/translations/t.ts @@ -3,6 +3,7 @@ import { getUserLanguage } from "./get-user-language.ts"; const en = { folder_form_no_decks: "No decks in the folder", + cards_add: "Add cards", card_next: "Next", card_previous: "Previous", user_stats_empty_text: "Study more cards to see the data", @@ -228,6 +229,7 @@ const en = { user_settings_updated: "Settings have been updated", ai_cards_generate: "Generate cards", ai_cards_title: "Generate cards with AI", + ai_cards_previous_prompts: "Prompt history", ai_cards_prompt: "Prompt", ai_cards_prompt_front: "Card front description", ai_cards_prompt_back: "Card back description", @@ -243,6 +245,7 @@ const en = { ai_cards_confirm_delete: "Are you sure you want to delete this card?", ai_cards_validation_key_required: "API key is required", ai_cards_added: "Cards have been added", + ai_cards_use_template: "Use template", understood: "Understood", }; @@ -474,6 +477,8 @@ const ru: Translation = { ai_cards_prompt_back: "Обратная сторона карточки", ai_cards_prompt_front: "Лицевая сторона карточки", ai_cards_prompt: "Описание колоды", + ai_cards_previous_prompts: "История запросов", + ai_cards_use_template: "Использовать шаблон", understood: "Понятно", ai_cards_gpt_model: "Модель", ai_cards_gpt_grab_key: "Получите ключ на", @@ -487,6 +492,7 @@ const ru: Translation = { ai_cards_by_ai: "Сгенерированные карточки", ai_cards_confirm_delete: "Удалить эту карточку?", ai_cards_title: "Создание карточек с помощью ИИ", + cards_add: "Добавить карточки", }; const es: Translation = { @@ -724,6 +730,8 @@ const es: Translation = { ai_cards_gpt_grab_key: "Obtén una clave en", ai_cards_gpt_model: "Modelo", ai_cards_prompt: "Descripción del mazo", + ai_cards_use_template: "Usar plantilla", + ai_cards_previous_prompts: "Historial de solicitudes", ai_cards_prompt_front: "Cara de la tarjeta", ai_cards_prompt_back: "Dorso de la tarjeta", ai_cards_api_keys: "Clave de API", @@ -733,9 +741,11 @@ const es: Translation = { ai_cards_validation_key_required: "Se requiere una clave de API", ai_cards_added: "Tarjetas añadidas", error: "Error", + cards_add: "Añadir tarjeta", }; const ptBr: Translation = { + cards_add: "Adicionar cartão", error: "Erro", user_settings_updated: "Configurações atualizadas", error_solving: "Estamos resolvendo o problema", @@ -976,6 +986,8 @@ const ptBr: Translation = { ai_cards_gpt_grab_key: "Obtenha uma chave em", ai_cards_gpt_model: "Modelo", ai_cards_prompt: "Descrição do baralho", + ai_cards_previous_prompts: "Histórico de solicitações", + ai_cards_use_template: "Usar modelo", ai_cards_gpt_dashboard: "painel da OpenAI", ai_cards_prompt_back: "Verso do cartão", ai_cards_prompt_front: "Frente do cartão", diff --git a/src/ui/full-screen-loader.tsx b/src/ui/full-screen-loader.tsx index 9881adb5..08cfc143 100644 --- a/src/ui/full-screen-loader.tsx +++ b/src/ui/full-screen-loader.tsx @@ -2,12 +2,15 @@ import { css } from "@emotion/css"; import { theme } from "./theme.tsx"; import React from "react"; -export const FullScreenLoader = () => { +type Props = { height?: string }; + +export const FullScreenLoader = (props: Props) => { + const height = props.height ?? "100vh"; return (
{
); }; + +export const ScreenLoader = () => { + return ; +};