From 72c1e06f7473a25ea5c9c7bf50699e6530ac8c05 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 15 May 2024 22:40:05 +0700 Subject: [PATCH] feat: step 1 --- apps/backend/src/api/api.module.ts | 4 + .../src/api/routes/posts.controller.ts | 11 +- .../launches/generator/generator.tsx | 200 ++++++++++++++++++ .../launches/launches.component.tsx | 2 + .../src/components/onboarding/onboarding.tsx | 20 +- .../post-url-selector/post.url.selector.tsx | 110 ++++++---- .../src/database/prisma/database.module.ts | 4 + .../database/prisma/posts/posts.service.ts | 17 +- .../src/dtos/generator/generator.dto.ts | 35 +++ .../src/openai/extract.content.service.ts | 36 ++++ .../src/openai/openai.service.ts | 80 +++++++ .../src/form/select.tsx | 59 ++++-- package-lock.json | 117 ++++++++++ package.json | 1 + 14 files changed, 626 insertions(+), 70 deletions(-) create mode 100644 apps/frontend/src/components/launches/generator/generator.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/generator/generator.dto.ts create mode 100644 libraries/nestjs-libraries/src/openai/extract.content.service.ts create mode 100644 libraries/nestjs-libraries/src/openai/openai.service.ts diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index 36d45276..d81c4a6c 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -22,6 +22,8 @@ import { BillingController } from '@gitroom/backend/api/routes/billing.controlle import { NotificationsController } from '@gitroom/backend/api/routes/notifications.controller'; import { MarketplaceController } from '@gitroom/backend/api/routes/marketplace.controller'; import { MessagesController } from '@gitroom/backend/api/routes/messages.controller'; +import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; +import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service'; const authenticatedController = [ UsersController, @@ -59,6 +61,8 @@ const authenticatedController = [ providers: [ AuthService, StripeService, + OpenaiService, + ExtractContentService, AuthMiddleware, PoliciesGuard, PermissionsService, diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index ac2bdda7..c1630a46 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -22,6 +22,7 @@ import { } from '@gitroom/backend/services/auth/permissions/permissions.service'; import { ApiTags } from '@nestjs/swagger'; import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service'; +import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto'; @ApiTags('Posts') @Controller('/posts') @@ -85,10 +86,18 @@ export class PostsController { @GetOrgFromRequest() org: Organization, @Body() body: CreatePostDto ) { - return this._postsService.createPost(org.id, body); } + @Post('/generator') + @CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH]) + generatePosts( + @GetOrgFromRequest() org: Organization, + @Body() body: GeneratorDto + ) { + return this._postsService.generatePosts(org.id, body); + } + @Delete('/:group') deletePost( @GetOrgFromRequest() org: Organization, diff --git a/apps/frontend/src/components/launches/generator/generator.tsx b/apps/frontend/src/components/launches/generator/generator.tsx new file mode 100644 index 00000000..d310adb1 --- /dev/null +++ b/apps/frontend/src/components/launches/generator/generator.tsx @@ -0,0 +1,200 @@ +import React, { + ChangeEventHandler, + FC, + useCallback, + useMemo, + useState, +} from 'react'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { useRouter } from 'next/navigation'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +import { + Step, + StepSpace, +} from '@gitroom/frontend/components/onboarding/onboarding'; +import { useModals } from '@mantine/modals'; +import { Select } from '@gitroom/react/form/select'; +import { Input } from '@gitroom/react/form/input'; +import dayjs from 'dayjs'; +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; +import { classValidatorResolver } from '@hookform/resolvers/class-validator'; +import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto'; +import { Button } from '@gitroom/react/form/button'; +import { PostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; + +const FirstStep: FC<{ nextStep: () => void }> = (props) => { + const { nextStep } = props; + const fetch = useFetch(); + + const resolver = useMemo(() => { + return classValidatorResolver(GeneratorDto); + }, []); + + const form = useForm({ + mode: 'all', + resolver, + values: { + date: dayjs().week() + '_' + dayjs().year(), + url: '', + post: undefined as undefined | string, + }, + }); + + const [url, post] = form.watch(['url', 'post']); + + const list = useMemo(() => { + const currentDate = dayjs(); + const generateWeeks = [...new Array(52)].map((_, i) => { + const week = currentDate.add(i, 'week'); + return { + value: week.week() + '_' + week.year(), + label: `Week #${week.week()} (${week + .startOf('isoWeek') + .format('YYYY-MM-DD')} - ${week + .endOf('isoWeek') + .format('YYYY-MM-DD')})`, + }; + }); + + return generateWeeks; + }, []); + + const makeSelect = useCallback((post?: string) => { + form.setValue('post', post?.split?.(':')[1]?.split(')')?.[0]); + }, []); + + const onSubmit: SubmitHandler<{ + date: string; + url: string; + post: string | undefined; + }> = useCallback(async (value) => { + fetch('/posts/generator', { + method: 'POST', + body: JSON.stringify(value), + }); + // nextStep(); + }, []); + + return ( +
+ +
+ +
+
+
+ +
+
+
Or select from exising posts
+
+ {}} + onSelect={makeSelect} + date={dayjs().add(1, 'year')} + /> +
+
+
+
+ +
+
+
+ ); +}; +export const GeneratorPopup = () => { + const [step, setStep] = useState(1); + + return ( +
+

Generate Posts

+
+ + + + + +
+ {step === 1 && setStep(2)} />} +
+ ); +}; +export const GeneratorComponent = () => { + const user = useUser(); + const router = useRouter(); + const modal = useModals(); + + const generate = useCallback(async () => { + if (!user?.tier.ai) { + if ( + await deleteDialog( + 'You need to upgrade to use this feature', + 'Move to billing', + 'Payment Required' + ) + ) { + router.push('/billing'); + } + return; + } + + modal.openModal({ + title: '', + withCloseButton: false, + closeOnEscape: false, + classNames: { + modal: 'bg-transparent text-white', + }, + size: '100%', + children: , + }); + }, [user]); + + return ( + + ); +}; diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index dd3f89f0..0e9582d8 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -13,6 +13,7 @@ import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; import clsx from 'clsx'; import { useUser } from '../layout/user.context'; import { Menu } from '@gitroom/frontend/components/launches/menu/menu'; +import { GeneratorComponent } from '@gitroom/frontend/components/launches/generator/generator'; export const LaunchesComponent = () => { const fetch = useFetch(); @@ -132,6 +133,7 @@ export const LaunchesComponent = () => { ))} update(true)} /> + {sortedIntegrations?.length > 0 && }
diff --git a/apps/frontend/src/components/onboarding/onboarding.tsx b/apps/frontend/src/components/onboarding/onboarding.tsx index ea25c718..aa864d09 100644 --- a/apps/frontend/src/components/onboarding/onboarding.tsx +++ b/apps/frontend/src/components/onboarding/onboarding.tsx @@ -9,15 +9,15 @@ import { SettingsPopup } from '@gitroom/frontend/components/layout/settings.comp import { Button } from '@gitroom/react/form/button'; import { ConnectChannels } from '@gitroom/frontend/components/onboarding/connect.channels'; -const Step: FC<{ step: number; title: string; currentStep: number }> = ( +export const Step: FC<{ step: number; title: string; currentStep: number, lastStep: number }> = ( props ) => { - const { step, title, currentStep } = props; + const { step, title, currentStep, lastStep } = props; return (
- {step === currentStep && currentStep !== 4 && ( + {step === currentStep && currentStep !== lastStep && ( = ( /> )} - {(currentStep > step || currentStep == 4) && ( + {(currentStep > step || currentStep == lastStep) && ( = ( /> )} - {step > currentStep && currentStep !== 4 && ( + {step > currentStep && currentStep !== lastStep && ( = ( ); }; -const StepSpace: FC = () => { +export const StepSpace: FC = () => { return (
@@ -128,13 +128,13 @@ const Welcome: FC = () => {

Onboarding

- + - + - + - +
{step === 1 && ( <> diff --git a/apps/frontend/src/components/post-url-selector/post.url.selector.tsx b/apps/frontend/src/components/post-url-selector/post.url.selector.tsx index 16d1062e..ecb85463 100644 --- a/apps/frontend/src/components/post-url-selector/post.url.selector.tsx +++ b/apps/frontend/src/components/post-url-selector/post.url.selector.tsx @@ -14,20 +14,28 @@ import dayjs from 'dayjs'; import useSWR from 'swr'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import removeMd from 'remove-markdown'; +import clsx from 'clsx'; const postUrlEmitter = new EventEmitter(); export const ShowPostSelector = () => { const [showPostSelector, setShowPostSelector] = useState(false); const [callback, setCallback] = useState<{ - callback: (tag: string) => void; - } | null>({ callback: (tag: string) => {} } as any); + callback: (tag: string | undefined) => void; + } | null>({ + callback: (tag: string | undefined) => { + return tag; + }, + } as any); const [date, setDate] = useState(dayjs()); useEffect(() => { postUrlEmitter.on( 'show', - (params: { date: dayjs.Dayjs; callback: (url: string) => void }) => { + (params: { + date: dayjs.Dayjs; + callback: (url: string | undefined) => void; + }) => { setCallback(params); setDate(params.date); setShowPostSelector(true); @@ -76,10 +84,11 @@ export const useShowPostSelector = (day: dayjs.Dayjs) => { export const PostSelector: FC<{ onClose: () => void; - onSelect: (tag: string) => void; + onSelect: (tag: string | undefined) => void; + noModal?: boolean; date: dayjs.Dayjs; }> = (props) => { - const { onClose, onSelect, date } = props; + const { onClose, onSelect, date, noModal } = props; const fetch = useFetch(); const fetchOldPosts = useCallback(() => { return fetch( @@ -98,50 +107,75 @@ export const PostSelector: FC<{ onClose(); }, []); - const select = useCallback((id: string) => () => { - onSelect(`(post:${id})`); - onClose(); - }, []); + const [current, setCurrent] = useState(undefined); + + const select = useCallback( + (id: string) => () => { + setCurrent(current === id ? undefined : id); + onSelect(current === id ? undefined : `(post:${id})`); + onClose(); + }, + [current] + ); const { isLoading, data } = useSWR('old-posts', fetchOldPosts); return ( -
-
-
-
- -
- -
+ + + + +
+ )}
{!!data && data.length > 0 && (
{data.map((p: any) => (
diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts index c015ed8e..736c49b1 100644 --- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts +++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts @@ -25,6 +25,8 @@ import { ItemUserService } from '@gitroom/nestjs-libraries/database/prisma/marke import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service'; import { MessagesRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.repository'; import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service'; +import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service'; +import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; @Global() @Module({ @@ -57,6 +59,8 @@ import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service MessagesService, CommentsService, IntegrationManager, + ExtractContentService, + OpenaiService, EmailService, ], get exports() { diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index cf19893b..d8d26b54 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -10,6 +10,9 @@ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/n import { capitalize } from 'lodash'; import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service'; import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service'; +import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto'; +import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service'; +import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; type PostWithConditionals = Post & { integration?: Integration; @@ -24,7 +27,9 @@ export class PostsService { private _integrationManager: IntegrationManager, private _notificationService: NotificationService, private _messagesService: MessagesService, - private _stripeService: StripeService + private _stripeService: StripeService, + private _extractContentService: ExtractContentService, + private _openAiService: OpenaiService ) {} async getPostsRecursively( @@ -432,4 +437,14 @@ export class PostsService { ); } } + + async generatePosts(orgId: string, body: GeneratorDto) { + const content = await this._extractContentService.extractContent(body.url); + if (content) { + const value = await this._openAiService.extractWebsiteText(content); + return {list: value}; + } + + return []; + } } diff --git a/libraries/nestjs-libraries/src/dtos/generator/generator.dto.ts b/libraries/nestjs-libraries/src/dtos/generator/generator.dto.ts new file mode 100644 index 00000000..1af46b6f --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/generator/generator.dto.ts @@ -0,0 +1,35 @@ +import { + IsDefined, + IsInt, + IsString, + IsUrl, + ValidateIf, + ValidateNested, +} from 'class-validator'; + +class Date { + @IsInt() + week: number; + + @IsInt() + year: number; +} +export class GeneratorDto { + @IsDefined() + @ValidateNested() + date: Date; + + @IsString() + @ValidateIf((o) => !o.post) + @IsUrl( + {}, + { + message: 'Invalid URL', + } + ) + url: string; + + @ValidateIf((o) => !o.url) + @IsString() + post: string; +} diff --git a/libraries/nestjs-libraries/src/openai/extract.content.service.ts b/libraries/nestjs-libraries/src/openai/extract.content.service.ts new file mode 100644 index 00000000..e6938334 --- /dev/null +++ b/libraries/nestjs-libraries/src/openai/extract.content.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { JSDOM } from 'jsdom'; + +@Injectable() +export class ExtractContentService { + async extractContent(url: string) { + const load = await (await fetch(url)).text(); + const dom = new JSDOM(load); + const allElements = Array.from( + dom.window.document.querySelectorAll('*') + ).filter((f) => f.tagName !== 'SCRIPT'); + const findIndex = allElements.findIndex((element) => { + return ( + ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].indexOf( + element.tagName.toLowerCase() + ) > -1 + ); + }); + + if (!findIndex) { + return false; + } + + return allElements + .slice(findIndex) + .map((element) => element.textContent) + .filter((f) => { + const trim = f?.trim(); + return (trim?.length || 0) > 0 && trim !== '\n'; + }) + .map((f) => f?.trim()) + .join('') + .replace(/\n/g, ' ') + .replace(/ {2,}/g, ' '); + } +} diff --git a/libraries/nestjs-libraries/src/openai/openai.service.ts b/libraries/nestjs-libraries/src/openai/openai.service.ts new file mode 100644 index 00000000..0c9ca217 --- /dev/null +++ b/libraries/nestjs-libraries/src/openai/openai.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@nestjs/common'; +import OpenAI from 'openai'; +import { shuffle } from 'lodash'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +@Injectable() +export class OpenaiService { + async extractWebsiteText(content: string) { + const websiteContent = await openai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: + 'Your take a full website text, and extract only the article content', + }, + { + role: 'user', + content, + }, + ], + model: 'gpt-4o', + }); + + const { content: articleContent } = websiteContent.choices[0].message; + + const posts = ( + await Promise.all([ + openai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: + 'Generate a Twitter post from the content without emojis in the following JSON format: { "post": string } put it in an array with one element', + }, + { + role: 'user', + content: articleContent!, + }, + ], + n: 5, + temperature: 0.7, + model: 'gpt-4o', + }), + openai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: + 'Generate a thread for social media in the following JSON format: Array<{ "post": string }> without emojis', + }, + { + role: 'user', + content: articleContent!, + }, + ], + n: 5, + temperature: 0.7, + model: 'gpt-4o', + }), + ]) + ).flatMap((p) => p.choices); + + return shuffle( + posts.map((choice) => { + const { content } = choice.message; + const start = content?.indexOf('[')!; + const end = content?.lastIndexOf(']')!; + try { + return JSON.parse('[' + content?.slice(start + 1, end) + ']'); + } catch (e) { + console.log(content); + return []; + } + }) + ); + } +} diff --git a/libraries/react-shared-libraries/src/form/select.tsx b/libraries/react-shared-libraries/src/form/select.tsx index 61db635a..546fe9cf 100644 --- a/libraries/react-shared-libraries/src/form/select.tsx +++ b/libraries/react-shared-libraries/src/form/select.tsx @@ -1,24 +1,43 @@ -"use client"; +'use client'; -import {DetailedHTMLProps, FC, SelectHTMLAttributes, useMemo} from "react"; -import {clsx} from "clsx"; -import {useFormContext} from "react-hook-form"; +import { DetailedHTMLProps, FC, SelectHTMLAttributes, useMemo } from 'react'; +import { clsx } from 'clsx'; +import { useFormContext } from 'react-hook-form'; import interClass from '../helpers/inter.font'; +import { RegisterOptions } from 'react-hook-form/dist/types/validator'; -export const Select: FC, HTMLSelectElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => { - const {label, className, disableForm, error, ...rest} = props; - const form = useFormContext(); - const err = useMemo(() => { - if (error) return error; - if (!form || !form.formState.errors[props?.name!]) return; - return form?.formState?.errors?.[props?.name!]?.message! as string; - }, [form?.formState?.errors?.[props?.name!]?.message, error]); +export const Select: FC< + DetailedHTMLProps< + SelectHTMLAttributes, + HTMLSelectElement + > & { + error?: any; + extraForm?: RegisterOptions; + disableForm?: boolean; + label: string; + name: string; + } +> = (props) => { + const { label, className, disableForm, error, extraForm, ...rest } = props; + const form = useFormContext(); + const err = useMemo(() => { + if (error) return error; + if (!form || !form.formState.errors[props?.name!]) return; + return form?.formState?.errors?.[props?.name!]?.message! as string; + }, [form?.formState?.errors?.[props?.name!]?.message, error]); - return ( -
-
{label}
- +
{err || <> }
+
+ ); +}; diff --git a/package-lock.json b/package-lock.json index 256fce1a..875a1adb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "multer": "^1.4.5-lts.1", "nestjs-command": "^3.1.4", "next": "14.0.4", + "openai": "^4.47.1", "prisma-paginate": "^5.2.1", "react": "18.2.0", "react-dnd": "^16.0.1", @@ -13253,6 +13254,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.9.tgz", "integrity": "sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/node-forge": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", @@ -14359,6 +14369,17 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -14440,6 +14461,17 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -19593,6 +19625,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -20546,6 +20586,11 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -20555,6 +20600,26 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -22518,6 +22583,14 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -30625,6 +30698,24 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -31285,6 +31376,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.47.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.47.1.tgz", + "integrity": "sha512-WWSxhC/69ZhYWxH/OBsLEirIjUcfpQ5+ihkXKp06hmeYXgBBIUCa9IptMzYx6NdkiOCsSGYCnTIsxaic3AjRCQ==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "bin": { + "openai": "bin/cli" + } + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", @@ -39644,6 +39753,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 4cad3037..d21007ef 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "multer": "^1.4.5-lts.1", "nestjs-command": "^3.1.4", "next": "14.0.4", + "openai": "^4.47.1", "prisma-paginate": "^5.2.1", "react": "18.2.0", "react-dnd": "^16.0.1",