Skip to content

Commit

Permalink
ChatGPT prompt history (#35)
Browse files Browse the repository at this point in the history
* ChatGPT prompt history
  • Loading branch information
kubk authored Apr 18, 2024
1 parent 98620b5 commit e4c3dff
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 19 deletions.
5 changes: 5 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HealthResponse>("/health");
Expand Down Expand Up @@ -246,3 +247,7 @@ export const addCardsMultipleRequest = (body: AddCardsMultipleRequest) => {
body,
);
};

export const userPreviousPromptsRequest = () => {
return request<UserPreviousPromptsResponse>("/user-previous-prompts");
};
18 changes: 17 additions & 1 deletion src/screens/ai-mass-creation/ai-mass-creation-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const AiMassCreationForm = observer(() => {
text: t("how"),
icon: (
<FilledIcon
backgroundColor={theme.icons.turquoise}
backgroundColor={theme.icons.violet}
icon={"mdi-help"}
/>
),
Expand Down Expand Up @@ -70,6 +70,18 @@ export const AiMassCreationForm = observer(() => {
store.goApiKeysScreen();
},
},
{
text: t("ai_cards_previous_prompts"),
icon: (
<FilledIcon
backgroundColor={theme.icons.turquoise}
icon={"mdi-history"}
/>
),
onClick: () => {
store.screen.onChange("previousPrompts");
},
},
]}
/>
{promptForm.apiKey.isTouched && promptForm.apiKey.error && (
Expand All @@ -88,6 +100,10 @@ export const AiMassCreationForm = observer(() => {
<Label isRequired text={t("ai_cards_prompt_back")}>
<Input field={promptForm.backPrompt} />
</Label>

<Label text={t("card_field_example_title")}>
<Input field={promptForm.examplePrompt} />
</Label>
</Screen>
);
});
4 changes: 4 additions & 0 deletions src/screens/ai-mass-creation/ai-mass-creation-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -23,5 +24,8 @@ export const AiMassCreationScreen = observer(() => {
if (store.screen.value === "cardsGenerated") {
return <CardsGeneratedScreen />;
}
if (store.screen.value === "previousPrompts") {
return <PreviousPromptsScreen />;
}
return <AiMassCreationForm />;
});
2 changes: 1 addition & 1 deletion src/screens/ai-mass-creation/cards-generated-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const CardsGeneratedScreen = observer(() => {

return (
<Screen
title={"Add cards"}
title={t("cards_add")}
subtitle={
screen.deckTitle ? (
<div className={css({ textAlign: "center", fontSize: 14 })}>
Expand Down
108 changes: 108 additions & 0 deletions src/screens/ai-mass-creation/previous-prompts-screen.tsx
Original file line number Diff line number Diff line change
@@ -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<number | null>(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 (
<Screen title={t("ai_cards_previous_prompts")}>
<Flex direction={"column"} gap={4}>
{store.userPreviousPromptsRequest.isLoading && <ScreenLoader />}
{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 (
<div
className={cx(
css({
backgroundColor: theme.bgColor,
borderRadius: theme.borderRadius,
padding: 12,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
cursor: "pointer",
}),
isSelected &&
css({
border: `2px solid ${theme.buttonColor}`,
}),
)}
key={i}
onClick={() => {
localStore.selectedIndex.onChange(i);
}}
>
<Flex direction={"column"} gap={4}>
<div
className={css({ maxHeight: 120, overflow: "hidden" })}
>
{log.payload.prompt}
</div>
{secondaryFields.map((field, i) => {
return (
<div
key={i}
className={css({
fontSize: 14,
color: theme.hintColor,
borderTop: `1px solid ${theme.divider}`,
paddingTop: 4,
})}
>
{field}
</div>
);
})}
</Flex>
</div>
);
})}
</>
)}
</Flex>
</Screen>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
33 changes: 30 additions & 3 deletions src/screens/ai-mass-creation/store/ai-mass-creation-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -43,15 +44,18 @@ export const chatGptModels = [

type ChatGptModel = (typeof chatGptModels)[number];

type InnerScreen = "how" | "apiKeys" | "cardsGenerated" | "previousPrompts";

export class AiMassCreationStore {
upsertUserAiCredentialsRequest = new RequestStore(
upsertUserAiCredentialsRequest,
);
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<InnerScreen | null>(null);
forceUpdateApiKey = new BooleanToggle(false);

promptForm = {
Expand All @@ -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: () => {
Expand All @@ -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() {
Expand Down Expand Up @@ -159,6 +168,22 @@ export class AiMassCreationStore {
return this.massCreationForm.cards.value.length > 1;
}

usePreviousPrompt(index: TextField<number | null>) {
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);
Expand Down Expand Up @@ -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") {
Expand All @@ -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,
})),
),
};
Expand Down
22 changes: 12 additions & 10 deletions src/screens/deck-form/store/deck-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
8 changes: 6 additions & 2 deletions src/store/deck-list-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ export class DeckListStore {
}

hapticImpact("heavy");
this.isFullScreenLoaderVisible = true;
runInAction(() => {
this.isFullScreenLoaderVisible = true;
});
duplicateDeckRequest(deckId)
.then(() => {
screenStore.go({ type: "main" });
Expand All @@ -150,7 +152,9 @@ export class DeckListStore {
}

hapticImpact("heavy");
this.isFullScreenLoaderVisible = true;
runInAction(() => {
this.isFullScreenLoaderVisible = true;
});
duplicateFolderRequest(folderId)
.then(() => {
screenStore.go({ type: "main" });
Expand Down
Loading

0 comments on commit e4c3dff

Please sign in to comment.