diff --git a/.env.example b/.env.example
index bf7ac43..321bdf6 100644
--- a/.env.example
+++ b/.env.example
@@ -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=
diff --git a/additional.d.ts b/additional.d.ts
new file mode 100644
index 0000000..3158084
--- /dev/null
+++ b/additional.d.ts
@@ -0,0 +1,6 @@
+declare module 'compute-cosine-similarity' {
+ export default function calculateSimilarity(
+ firstVector: number[],
+ secondVector: number[],
+ ): number
+}
diff --git a/package.json b/package.json
index a4792fd..bcd551a 100644
--- a/package.json
+++ b/package.json
@@ -30,12 +30,14 @@
"@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",
@@ -43,7 +45,6 @@
"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"
diff --git a/prisma/migrations/20230305201901_adds_is_ai_powered_flag_to_card/migration.sql b/prisma/migrations/20230305201901_adds_is_ai_powered_flag_to_card/migration.sql
new file mode 100644
index 0000000..9709a0a
--- /dev/null
+++ b/prisma/migrations/20230305201901_adds_is_ai_powered_flag_to_card/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Card" ADD COLUMN "isAiPowered" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index bdeb88c..da1da52 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -96,6 +96,7 @@ model Card {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
studySessionBoxCards StudySessionBoxCard[]
+ isAiPowered Boolean @default(false)
}
model StudySession {
diff --git a/src/components/card/card.component.tsx b/src/components/card/card.component.tsx
index 65f8378..5487271 100644
--- a/src/components/card/card.component.tsx
+++ b/src/components/card/card.component.tsx
@@ -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 = () => {
@@ -32,6 +34,18 @@ export function _Card(props: CardProps) {
)
}
+ const renderAiPoweredTag = () => {
+ if (!isAiPowered) return null
+
+ return (
+
+
+ 🤖
+
+
+ )
+ }
+
const Container = as || 'div'
return (
@@ -45,6 +59,7 @@ export function _Card(props: CardProps) {
>
{children}
{renderEditButtons()}
+ {renderAiPoweredTag()}
)
}
diff --git a/src/components/card/card.types.ts b/src/components/card/card.types.ts
index 69bf9dc..eed2e22 100644
--- a/src/components/card/card.types.ts
+++ b/src/components/card/card.types.ts
@@ -8,4 +8,5 @@ export type CardProps = {
onClick?: () => void
as?: ReactTag
fullWidth?: boolean
+ isAiPowered?: boolean
}
diff --git a/src/components/tooltip/tooltip.component.tsx b/src/components/tooltip/tooltip.component.tsx
index 79f9177..fb25b87 100644
--- a/src/components/tooltip/tooltip.component.tsx
+++ b/src/components/tooltip/tooltip.component.tsx
@@ -32,7 +32,11 @@ function _Tooltip(props: TooltipProps) {
return (
<>
{renderTooltipTrigger()}
-
+
{hint}
>
diff --git a/src/constants/index.ts b/src/constants/index.ts
index aec4e7a..aed920f 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -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
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 22afff5..e2abaee 100644
--- a/src/contexts/create-new-deck/create-new-deck.context.tsx
+++ b/src/contexts/create-new-deck/create-new-deck.context.tsx
@@ -19,6 +19,7 @@ import type {
CreateNewDeckContextState,
DeckWithCardsAndTopics,
FormInputValues,
+ GenerateAiPoweredCardsParams,
TopicInput,
} from './create-new-deck.types'
import { DeckInputFormSchema } from './create-new-deck.types'
@@ -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
*/
@@ -93,6 +108,8 @@ export function CreateNewDeckContextProvider(
const [deletedCards, setDeletedCards] = useState>([])
const [editedCards, setEditedCards] = useState>([])
+ const hasDeckAiPoweredCards = cards.some(card => card.isAiPowered)
+
const createNewDeckForm = useForm({
resolver: zodResolver(DeckInputFormSchema),
defaultValues: {
@@ -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
*/
@@ -270,6 +307,10 @@ export function CreateNewDeckContextProvider(
visibility,
setVisibility,
+
+ generateAiPoweredCards,
+ isGeneratingAiPoweredCards,
+ hasErrorGeneratingAiPoweredCards,
}
return (
diff --git a/src/contexts/create-new-deck/create-new-deck.types.ts b/src/contexts/create-new-deck/create-new-deck.types.ts
index fecf80a..b222b05 100644
--- a/src/contexts/create-new-deck/create-new-deck.types.ts
+++ b/src/contexts/create-new-deck/create-new-deck.types.ts
@@ -39,6 +39,8 @@ export type CreateNewDeckContextProviderProps = {
deck?: DeckWithCardsAndTopics | null
}
+export type GenerateAiPoweredCardsParams = { topics: Array }
+
export type CreateNewDeckContextState = {
createNewDeckForm?: UseFormReturn
submitDeck: (values: FormInputValues) => Promise
@@ -55,6 +57,10 @@ export type CreateNewDeckContextState = {
visibilityOptions: Array