Skip to content

Commit

Permalink
chore: πŸ”– release new app version
Browse files Browse the repository at this point in the history
  • Loading branch information
emiliosheinz authored Mar 11, 2023
2 parents 8e6d7bf + c97de49 commit 6528145
Show file tree
Hide file tree
Showing 22 changed files with 412 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ AWS_S3_ACCESS_KEY_ID=
AWS_S3_SECRET_ACCESS_KEY=
AWS_S3_BUCKET=
AWS_CLOUD_FRONT_URL=

#OpenAI
OPENAI_API_KEY=
6 changes: 6 additions & 0 deletions additional.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module 'compute-cosine-similarity' {
export default function calculateSimilarity(
firstVector: number[],
secondVector: number[],
): number
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,21 @@
"@trpc/server": "^10.0.0",
"@vercel/analytics": "^0.1.6",
"compressorjs": "^1.1.1",
"compute-cosine-similarity": "^1.0.0",
"jotai": "^1.12.1",
"lodash": "^4.17.21",
"next": "13.1.1",
"next-auth": "^4.18.3",
"next-superjson-plugin": "^0.5.4",
"nextjs-progressbar": "^0.0.16",
"openai": "^3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.41.5",
"react-hot-toast": "^2.4.0",
"react-intersection-observer": "^9.4.2",
"react-tooltip": "^5.7.2",
"sharp": "^0.31.3",
"string-similarity": "^4.0.4",
"superjson": "1.9.1",
"tailwindcss-animation-delay": "^1.0.7",
"zod": "^3.18.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Card" ADD COLUMN "isAiPowered" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ model Card {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
studySessionBoxCards StudySessionBoxCard[]
isAiPowered Boolean @default(false)
}

model StudySession {
Expand Down
23 changes: 19 additions & 4 deletions src/components/card/card.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { PencilSquareIcon, TrashIcon } from '@heroicons/react/24/outline'

import { classNames } from '~/utils/css'

import { Tooltip } from '../tooltip'
import type { CardProps } from './card.types'

export function _Card(props: CardProps) {
const {
as,
onClick,
children,
fullWidth,
isEditable,
onDeletePress,
isAiPowered,
onEditPress,
onClick,
as,
fullWidth,
onDeletePress,
} = props

const renderEditButtons = () => {
Expand All @@ -32,6 +34,18 @@ export function _Card(props: CardProps) {
)
}

const renderAiPoweredTag = () => {
if (!isAiPowered) return null

return (
<Tooltip hint='Este card foi gerado por uma InteligΓͺncia Artificial'>
<span className='absolute right-0 bottom-0 p-2 text-lg hover:cursor-pointer'>
πŸ€–
</span>
</Tooltip>
)
}

const Container = as || 'div'

return (
Expand All @@ -45,6 +59,7 @@ export function _Card(props: CardProps) {
>
{children}
{renderEditButtons()}
{renderAiPoweredTag()}
</Container>
)
}
Expand Down
1 change: 1 addition & 0 deletions src/components/card/card.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export type CardProps = {
onClick?: () => void
as?: ReactTag
fullWidth?: boolean
isAiPowered?: boolean
}
6 changes: 5 additions & 1 deletion src/components/tooltip/tooltip.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ function _Tooltip(props: TooltipProps) {
return (
<>
{renderTooltipTrigger()}
<ReactTooltip anchorId={tooltipId} className='max-w-xs' variant='dark'>
<ReactTooltip
anchorId={tooltipId}
className='z-50 max-w-xs'
variant='dark'
>
<p className='text-left text-xs text-primary-50'>{hint}</p>
</ReactTooltip>
</>
Expand Down
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Visibility } from '@prisma/client'

import type { Option } from '~/components/radio-group'

export const MINIMUM_ACCEPTED_SIMILARITY = 0.9
export const MAX_TOPICS_PER_DECK_AND_USER = 5
export const ITEMS_PER_PAGE = 30

Expand Down
41 changes: 41 additions & 0 deletions src/contexts/create-new-deck/create-new-deck.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
CreateNewDeckContextState,
DeckWithCardsAndTopics,
FormInputValues,
GenerateAiPoweredCardsParams,
TopicInput,
} from './create-new-deck.types'
import { DeckInputFormSchema } from './create-new-deck.types'
Expand Down Expand Up @@ -73,6 +74,20 @@ 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
*/
Expand All @@ -93,6 +108,8 @@ export function CreateNewDeckContextProvider(
const [deletedCards, setDeletedCards] = useState<Array<CardInput>>([])
const [editedCards, setEditedCards] = useState<Array<CardInput>>([])

const hasDeckAiPoweredCards = cards.some(card => card.isAiPowered)

const createNewDeckForm = useForm<FormInputValues>({
resolver: zodResolver(DeckInputFormSchema),
defaultValues: {
Expand Down Expand Up @@ -246,6 +263,26 @@ export function CreateNewDeckContextProvider(
}
}

const generateAiPoweredCards = (params: GenerateAiPoweredCardsParams) => {
const { topics } = params

if (isGeneratingAiPoweredCards) return

if (topics.length === 0) {
notify.warning(
'VocΓͺ precisa cadastrar ao menos 1 tΓ³pico para gerar Cards de forma automΓ‘tica',
)
return
}

if (hasDeckAiPoweredCards) {
notify.error('Este Deck jΓ‘ possui Cards gerados por uma IA')
return
}

generateAiPoweredCardsMutation({ topics })
}

/**
* If isEditingDeck uses update function (currying) else uses the creation function
*/
Expand All @@ -270,6 +307,10 @@ export function CreateNewDeckContextProvider(

visibility,
setVisibility,

generateAiPoweredCards,
isGeneratingAiPoweredCards,
hasErrorGeneratingAiPoweredCards,
}

return (
Expand Down
10 changes: 10 additions & 0 deletions src/contexts/create-new-deck/create-new-deck.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export type CreateNewDeckContextProviderProps = {
deck?: DeckWithCardsAndTopics | null
}

export type GenerateAiPoweredCardsParams = { topics: Array<TopicInput> }

export type CreateNewDeckContextState = {
createNewDeckForm?: UseFormReturn<FormInputValues>
submitDeck: (values: FormInputValues) => Promise<void>
Expand All @@ -55,6 +57,10 @@ export type CreateNewDeckContextState = {
visibilityOptions: Array<Option<Visibility>>
visibility?: Option<Visibility>
setVisibility: Dispatch<SetStateAction<Option<Visibility> | undefined>>

generateAiPoweredCards: (params: GenerateAiPoweredCardsParams) => void
isGeneratingAiPoweredCards: boolean
hasErrorGeneratingAiPoweredCards: boolean
}

export const initialState: CreateNewDeckContextState = {
Expand All @@ -72,4 +78,8 @@ export const initialState: CreateNewDeckContextState = {
visibilityOptions: DECK_VISIBILITY_OPTIONS,
visibility: DECK_VISIBILITY_OPTIONS[0],
setVisibility: noop,

generateAiPoweredCards: noop,
isGeneratingAiPoweredCards: false,
hasErrorGeneratingAiPoweredCards: false,
}
1 change: 1 addition & 0 deletions src/env/schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const serverSchema = z.object({
AWS_S3_SECRET_ACCESS_KEY: z.string(),
AWS_S3_BUCKET: z.string(),
AWS_CLOUD_FRONT_URL: z.string().url(),
OPENAI_API_KEY: z.string(),
})

/**
Expand Down
40 changes: 39 additions & 1 deletion src/modules/decks/create/components/cards.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { useState } from 'react'
import { PlusCircleIcon } from '@heroicons/react/24/outline'

import { Card } from '~/components/card'
import { Loader } from '~/components/loader'
import { NewCardModal } from '~/components/modal/new-card/new-card-modal.component'
import type { CardFormInputValues } from '~/components/modal/new-card/new-card-modal.types'
import { Tooltip } from '~/components/tooltip'
import { useCreateNewDeckContext } from '~/contexts/create-new-deck'

type NewCardModalState = {
Expand All @@ -13,7 +15,16 @@ type NewCardModalState = {
}

export const Cards = () => {
const { cards, addCard, deleteCard, editCard } = useCreateNewDeckContext()
const {
cards,
addCard,
deleteCard,
editCard,
topics,
generateAiPoweredCards,
isGeneratingAiPoweredCards,
hasErrorGeneratingAiPoweredCards,
} = useCreateNewDeckContext()

const [newCardModalState, setNewCardModalState] = useState<NewCardModalState>(
{ isOpen: false },
Expand All @@ -40,13 +51,39 @@ export const Cards = () => {
}))
}

const renderAiCardsButton = () => {
const successContent = isGeneratingAiPoweredCards ? (
<Loader />
) : (
<span className='text-4xl'>πŸ€–</span>
)

const errorContent = (
<span className='text-4xl'>
Houve um erro ao gerar os Cards. Clique aqui para tentar novamente!
</span>
)

return (
<Card onClick={() => generateAiPoweredCards({ topics })}>
{hasErrorGeneratingAiPoweredCards ? errorContent : successContent}
<div className='absolute right-0 top-0 p-3'>
<Tooltip
hint={`AlΓ©m de criar seus prΓ³prios Flashcards manualmente vocΓͺ pode deixar que a nossa InteligΓͺncia Artificial os gere para vocΓͺ se baseando nos tΓ³picos previamente cadastrados acima. Lembre-se, a quantidade de Cards criados pode variar de acordo com o tamanho das perguntas e respostas geradas mas deve girar em torno de 3.`}
/>
</div>
</Card>
)
}

return (
<>
<h2 className='text-xl font-semibold'>Cards</h2>
<div className='w-ful flex flex-wrap gap-5'>
{cards.map((card, idx) => (
<Card
isEditable
isAiPowered={card.isAiPowered}
key={`${card.question}-${card.answer}`}
onDeletePress={() => deleteCard(idx)}
onEditPress={() => {
Expand All @@ -59,6 +96,7 @@ export const Cards = () => {
<Card onClick={() => setNewCardModalState({ isOpen: true })}>
<PlusCircleIcon className='h-12 w-12' />
</Card>
{renderAiCardsButton()}
</div>
<NewCardModal
isOpen={newCardModalState.isOpen}
Expand Down
4 changes: 3 additions & 1 deletion src/modules/decks/review/hooks/use-deck-review.hook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ export function useDeckReview(deckId: string) {
}, [cardAnswerStage, answerResult])

useEffect(() => {
if (isLastCard && cardAnswerStage === 'validation' && studySessionId) {
const isLastCardValidation = isLastCard && cardAnswerStage === 'validation'

if (isLastCardValidation && studySessionId) {
finishReviewSession({
studySessionId,
reviewedBoxIds: studySessionBoxes?.map(({ id }) => id) ?? [],
Expand Down
10 changes: 3 additions & 7 deletions src/pages/decks/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function getDeckFromDatabase(deckId: string) {
return await prisma.deck.findFirst({
where: { id: deckId },
include: {
cards: { select: { id: true, question: true } },
cards: { select: { id: true, question: true, isAiPowered: true } },
topics: true,
},
})
Expand Down Expand Up @@ -108,10 +108,6 @@ const DeckDetailsPage: NextPage<
)
}

const renderCurrentStudySessionCard = () => {
return <StudySessionCard deckId={deck.id} />
}

return (
<>
<Head>
Expand Down Expand Up @@ -141,11 +137,11 @@ const DeckDetailsPage: NextPage<
{renderTopics()}
</div>
</div>
{renderCurrentStudySessionCard()}
<StudySessionCard deckId={deck.id} />
<h2 className='text-xl font-medium text-primary-900'>Cards:</h2>
<ul className='flex w-full flex-wrap gap-5'>
{deck.cards.map(card => (
<Card as='li' key={card.id}>
<Card as='li' key={card.id} isAiPowered={card.isAiPowered}>
{card.question}
</Card>
))}
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cardsRouter } from './routers/cards'
import { decksRouter } from './routers/decks'
import { filesRouter } from './routers/files'
import { studySessionRouter } from './routers/study-session'
Expand All @@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({
files: filesRouter,
studySession: studySessionRouter,
user: userRouter,
cards: cardsRouter,
})

export type AppRouter = typeof appRouter
21 changes: 21 additions & 0 deletions src/server/api/routers/cards/cards.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from 'zod'

import { MAX_TOPICS_PER_DECK_AND_USER } from '~/constants'
import { createTRPCRouter, protectedProcedure } from '~/server/api/trpc'
import { generateFlashCards } from '~/utils/openai'
import { TopicInputSchema } from '~/utils/validators/topic'

export const cardsRouter = createTRPCRouter({
generateAiPoweredCards: protectedProcedure
.input(
z.object({
topics: z
.array(TopicInputSchema)
.min(1)
.max(MAX_TOPICS_PER_DECK_AND_USER),
}),
)
.mutation(({ input: { topics } }) => {
return generateFlashCards({ topics })
}),
})
1 change: 1 addition & 0 deletions src/server/api/routers/cards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { cardsRouter } from './cards.router'
Loading

1 comment on commit 6528145

@vercel
Copy link

@vercel vercel bot commented on 6528145 Mar 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

briskly – ./

briskly.app
briskly-emiliosheinz.vercel.app
briskly-git-main-emiliosheinz.vercel.app

Please sign in to comment.