From 1e08d16e9d989ac59a9f3a90354250b35afd9841 Mon Sep 17 00:00:00 2001 From: Emilio Schaedler Heinzmann Date: Fri, 31 Mar 2023 21:04:40 -0300 Subject: [PATCH 1/5] From 13c97db2c529a2c2bdf551f822ffaffae4b40134 Mon Sep 17 00:00:00 2001 From: Emilio Schaedler Heinzmann Date: Sat, 1 Apr 2023 09:02:41 -0300 Subject: [PATCH 2/5] refactor: :recycle: uses genrate flashcards microservice --- .../create-new-deck.context.tsx | 53 ++++++++++---- src/server/api/root.ts | 2 - src/server/api/routers/cards/cards.router.ts | 11 --- src/server/api/routers/cards/index.ts | 1 - src/utils/openai/index.ts | 73 ------------------- 5 files changed, 38 insertions(+), 102 deletions(-) delete mode 100644 src/server/api/routers/cards/cards.router.ts delete mode 100644 src/server/api/routers/cards/index.ts diff --git a/src/contexts/create-new-deck/create-new-deck.context.tsx b/src/contexts/create-new-deck/create-new-deck.context.tsx index dc9c954..0849ed6 100644 --- a/src/contexts/create-new-deck/create-new-deck.context.tsx +++ b/src/contexts/create-new-deck/create-new-deck.context.tsx @@ -3,6 +3,8 @@ import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { Visibility } from '@prisma/client' +import type { QueryFunctionContext } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' import { useSetAtom } from 'jotai' import { useRouter } from 'next/router' @@ -39,6 +41,22 @@ const uploadImage = async (uploadUrl: string, image?: File) => { }) } +const fetchAiPoweredCards = async ( + context: QueryFunctionContext, +): Promise> => { + const [title, ...topics] = context.queryKey as [string] + + const params = new URLSearchParams({ title }) + for (const topic of topics) params.append('topics', topic) + + // TODO emiliosheinz: move to env variables + const url = `https://flashcards-api.briskly.app/ai-powered-flashcards?${params}` + + const response = await fetch(url) + + return response.json() +} + const isEditingDeck = ( deck?: DeckWithCardsAndTopics | null, ): deck is DeckWithCardsAndTopics => !!deck @@ -73,20 +91,6 @@ export function CreateNewDeckContextProvider( const getFileUploadConfigMutation = api.files.getFileUploadConfig.useMutation() - const { - mutate: generateAiPoweredCardsMutation, - isLoading: isGeneratingAiPoweredCards, - isError: hasErrorGeneratingAiPoweredCards, - } = api.cards.generateAiPoweredCards.useMutation({ - onSuccess: aiPoweredCards => { - setCards(prevCards => [...prevCards, ...aiPoweredCards]) - notify.success('Bip Bop, cards gerados com sucesso. Aproveite 🤖') - }, - onError: () => { - notify.error('Ocorreu um erro ao gerar os cards. Tente novamente!') - }, - }) - /** * Shared states between creation and edit */ @@ -121,6 +125,25 @@ export function CreateNewDeckContextProvider( }, }) + const { + refetch: generateAiPoweredCardsMutation, + isFetching: isGeneratingAiPoweredCards, + isError: hasErrorGeneratingAiPoweredCards, + } = useQuery( + [createNewDeckForm.getValues().title, ...topics.map(({ title }) => title)], + fetchAiPoweredCards, + { + enabled: false, + onSuccess: aiPoweredCards => { + setCards(prevCards => [...prevCards, ...aiPoweredCards]) + notify.success('Bip Bop, cards gerados com sucesso. Aproveite 🤖') + }, + onError: () => { + notify.error('Ocorreu um erro ao gerar os cards. Tente novamente!') + }, + }, + ) + const addTopic = (topic: string) => { setTopics(topics => [ ...topics.filter(({ title }) => title !== topic), @@ -282,7 +305,7 @@ export function CreateNewDeckContextProvider( return } - generateAiPoweredCardsMutation({ topics, title }) + generateAiPoweredCardsMutation() } /** diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 62fd334..eea0bcd 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,5 +1,4 @@ import { answerValidationReportsRouter } from './routers/answer-validation-reports' -import { cardsRouter } from './routers/cards' import { decksRouter } from './routers/decks' import { filesRouter } from './routers/files' import { studySessionRouter } from './routers/study-session' @@ -16,7 +15,6 @@ export const appRouter = createTRPCRouter({ files: filesRouter, studySession: studySessionRouter, user: userRouter, - cards: cardsRouter, answerValidationReports: answerValidationReportsRouter, }) diff --git a/src/server/api/routers/cards/cards.router.ts b/src/server/api/routers/cards/cards.router.ts deleted file mode 100644 index 0aaaa38..0000000 --- a/src/server/api/routers/cards/cards.router.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createTRPCRouter, protectedProcedure } from '~/server/api/trpc' -import { generateFlashCards } from '~/utils/openai' -import { DeckInputSchema } from '~/utils/validators/deck' - -export const cardsRouter = createTRPCRouter({ - generateAiPoweredCards: protectedProcedure - .input(DeckInputSchema.pick({ topics: true, title: true })) - .mutation(({ input: { topics, title } }) => { - return generateFlashCards({ topics, title }) - }), -}) diff --git a/src/server/api/routers/cards/index.ts b/src/server/api/routers/cards/index.ts deleted file mode 100644 index 384ef40..0000000 --- a/src/server/api/routers/cards/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { cardsRouter } from './cards.router' diff --git a/src/utils/openai/index.ts b/src/utils/openai/index.ts index b874d0d..f61c00c 100644 --- a/src/utils/openai/index.ts +++ b/src/utils/openai/index.ts @@ -2,15 +2,9 @@ * !DO NO USE THIS FILE IN THE CLIENT SIDE */ import calculateSimilarity from 'compute-cosine-similarity' -import random from 'lodash/random' -import shuffle from 'lodash/shuffle' import { Configuration, OpenAIApi } from 'openai' import { MINIMUM_ACCEPTED_SIMILARITY } from '~/constants' -import type { - CardInput, - TopicInput, -} from '~/contexts/create-new-deck/create-new-deck.types' import { env } from '~/env/server.mjs' const configuration = new Configuration({ @@ -30,9 +24,6 @@ const createEmbedding = (str: string) => }, ) -const trimAndRemoveDoubleQuotes = (str: string) => - str.trim().replaceAll('"', '') - /** * Embed both strings with text-embedding-ada-002 and calculate their distance with cosine similarity * Reference: https://platform.openai.com/docs/guides/embeddings/limitations-risks @@ -66,67 +57,3 @@ export async function verifyIfAnswerIsRight( return isRight } - -type GenerateFlashCardsParam = { - topics: Array - title: string -} - -export async function generateFlashCards({ - topics, - title, -}: GenerateFlashCardsParam): Promise> { - let generatedJsonString: string | undefined - - try { - const amountOfCards = 3 - const charactersPerSentence = 65 - - /** - * Selects between 1 and 3 random topics from the array of topics - * and build a string with the topics separated by 'ou' - */ - const joinedTopics = shuffle(topics) - .map(({ title }) => title) - .slice(0, random(1, 3)) - .join(' ou ') - - /** Build prompt asking OpenAI to generate a csv string */ - const prompt = `Levando em conta o contexto ${title}, gere um Array JSON de tamanho ${amountOfCards} com perguntas e respostas curtas e diretas, de no máximo ${charactersPerSentence} caracteres, sobre ${joinedTopics}. [{question: "pergunta", answer: "resposta"}, ...]` - - const response = await openai.createChatCompletion( - { - n: 1, - messages: [{ role: 'user', content: prompt }], - temperature: 0.8, - model: 'gpt-3.5-turbo', - max_tokens: amountOfCards * charactersPerSentence, - }, - { timeout: 30_000 }, - ) - - generatedJsonString = response.data.choices[0]?.message?.content - - if (!generatedJsonString) { - throw new Error('Não foi possível gerar as perguntas e respostas.') - } - - const generatedJson = JSON.parse(generatedJsonString) - - const cards: Array = generatedJson.map( - ({ question, answer }: { question: string; answer: string }) => ({ - question: trimAndRemoveDoubleQuotes(question), - validAnswers: trimAndRemoveDoubleQuotes(answer), - isAiPowered: true, - }), - ) - - return cards - } catch (error) { - /** - * Added to improve error tracking on log monitoring tools - */ - console.error(error, generatedJsonString) - throw error - } -} From 045a5262a19c06384c1de31b1813ba326c28aa8a Mon Sep 17 00:00:00 2001 From: Emilio Schaedler Heinzmann Date: Sun, 2 Apr 2023 11:50:57 -0300 Subject: [PATCH 3/5] chore: :wrench: adds briskly generate flash cards api as environment variable --- .env.example | 4 ++++ src/contexts/create-new-deck/create-new-deck.context.tsx | 9 +++++++-- src/env/schema.mjs | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 321bdf6..4200efd 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,7 @@ AWS_CLOUD_FRONT_URL= #OpenAI OPENAI_API_KEY= + +#APIS +# NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL=http://localhost:3333/ +NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL=https://flashcards-api.briskly.app/ diff --git a/src/contexts/create-new-deck/create-new-deck.context.tsx b/src/contexts/create-new-deck/create-new-deck.context.tsx index 0849ed6..086d8a2 100644 --- a/src/contexts/create-new-deck/create-new-deck.context.tsx +++ b/src/contexts/create-new-deck/create-new-deck.context.tsx @@ -9,6 +9,7 @@ import { useSetAtom } from 'jotai' import { useRouter } from 'next/router' import { DECK_VISIBILITY_OPTIONS } from '~/constants' +import { env } from '~/env/client.mjs' import { api, handleApiClientSideError } from '~/utils/api' import { fullScreenLoaderAtom } from '~/utils/atoms' import { compress } from '~/utils/image' @@ -49,11 +50,14 @@ const fetchAiPoweredCards = async ( const params = new URLSearchParams({ title }) for (const topic of topics) params.append('topics', topic) - // TODO emiliosheinz: move to env variables - const url = `https://flashcards-api.briskly.app/ai-powered-flashcards?${params}` + const url = `${env.NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL}/ai-powered-flashcards?${params}` const response = await fetch(url) + if (!response.ok) { + throw response + } + return response.json() } @@ -133,6 +137,7 @@ export function CreateNewDeckContextProvider( [createNewDeckForm.getValues().title, ...topics.map(({ title }) => title)], fetchAiPoweredCards, { + retry: false, enabled: false, onSuccess: aiPoweredCards => { setCards(prevCards => [...prevCards, ...aiPoweredCards]) diff --git a/src/env/schema.mjs b/src/env/schema.mjs index d1140b7..6f45cf1 100644 --- a/src/env/schema.mjs +++ b/src/env/schema.mjs @@ -40,7 +40,7 @@ export const serverSchema = z.object({ * To expose them to the client, prefix them with `NEXT_PUBLIC_`. */ export const clientSchema = z.object({ - // NEXT_PUBLIC_CLIENTVAR: z.string(), + NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL: z.string(), }) /** @@ -50,5 +50,6 @@ export const clientSchema = z.object({ * @type {{ [k in keyof z.infer]: z.infer[k] | undefined }} */ export const clientEnv = { - // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, + NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL: + process.env.NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL, } From 8762f25999a0401f3deeb69783823e07975b455b Mon Sep 17 00:00:00 2001 From: Emilio Schaedler Heinzmann Date: Sun, 2 Apr 2023 11:53:47 -0300 Subject: [PATCH 4/5] fix: :bug: fgix max valida answers validation message --- src/utils/validators/card.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/validators/card.ts b/src/utils/validators/card.ts index 0510dcf..705e7b9 100644 --- a/src/utils/validators/card.ts +++ b/src/utils/validators/card.ts @@ -21,7 +21,7 @@ export const CardInputSchema = z.object({ .refine( (answers: string) => answers.split(';').length <= MAX_VALID_ANSWERS_PER_CARD, - `Um Card não pode ter mais que ${10} respostas válidas`, + `Um Card não pode ter mais que ${MAX_VALID_ANSWERS_PER_CARD} respostas válidas`, ) .refine( (answers: string) => From 448117501b635bdb68128d6dca317bf98c8020e0 Mon Sep 17 00:00:00 2001 From: Emilio Schaedler Heinzmann Date: Mon, 3 Apr 2023 07:30:27 -0300 Subject: [PATCH 5/5] docs: :memo: updates env example and readme --- .env.example | 4 ++-- README.md | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 4200efd..ca2926e 100644 --- a/.env.example +++ b/.env.example @@ -35,5 +35,5 @@ AWS_CLOUD_FRONT_URL= OPENAI_API_KEY= #APIS -# NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL=http://localhost:3333/ -NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL=https://flashcards-api.briskly.app/ +# NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL=http://localhost:3333 +NEXT_PUBLIC_BRISKLY_GENERATE_FLASH_CARDS_API_URL=https://flashcards-api.briskly.app diff --git a/README.md b/README.md index 3b508ef..fdc3d87 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ ![Briskly! Your AI powered flashcards app.](/docs/images/banner.png) + +## Useful Content + +- [Storing Images in S3 from Node Server](https://www.youtube.com/watch?v=eQAIojcArRY&ab) +- [Set up a CloudFront CDN for an S3 Bucket](https://www.youtube.com/watch?v=kbI7kRWAU-w)