feat: step 1

This commit is contained in:
Nevo David 2024-05-15 22:40:05 +07:00
parent 3cc4ea22a9
commit 72c1e06f74
14 changed files with 626 additions and 70 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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 (
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormProvider {...form}>
<div className="flex flex-col">
<Select
label="Select a week"
name="date"
extraForm={{
setValueAs: (value) => {
const [week, year] = value.split('_');
return { week: +week, year: +year };
},
}}
>
{list.map((item) => (
<option value={item.value} key={item.value}>
{item.label}
</option>
))}
</Select>
<div className="p-[20px] border border-fifth rounded-[4px]">
<div className="flex">
<div className="flex-1">
<Input label="URL" {...form.register('url')} />
</div>
</div>
<div className="pb-[10px]">Or select from exising posts</div>
<div className="p-[16px] bg-input border-fifth border rounded-[4px] min-h-[500px]">
<PostSelector
noModal={true}
onClose={() => {}}
onSelect={makeSelect}
date={dayjs().add(1, 'year')}
/>
</div>
</div>
</div>
<div className="mt-[20px] flex justify-end">
<Button type="submit" disabled={!!(url && post)}>
{url && post ? "You can't have both URL and a POST" : 'Next'}
</Button>
</div>
</FormProvider>
</form>
);
};
export const GeneratorPopup = () => {
const [step, setStep] = useState(1);
return (
<div className="bg-sixth p-[32px] w-full max-w-[920px] mx-auto flex flex-col gap-[24px] rounded-[4px] border border-[#172034] relative">
<h1 className="text-[24px]">Generate Posts</h1>
<div className="flex">
<Step title="Generate posts" step={1} currentStep={step} lastStep={3} />
<StepSpace />
<Step title="Confirm posts" step={2} currentStep={step} lastStep={3} />
<StepSpace />
<Step title="Done" step={3} currentStep={step} lastStep={3} />
</div>
{step === 1 && <FirstStep nextStep={() => setStep(2)} />}
</div>
);
};
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: <GeneratorPopup />,
});
}, [user]);
return (
<button
className="text-white p-[8px] rounded-md bg-red-700 flex justify-center items-center gap-[5px]"
onClick={generate}
>
<svg
width="25"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0614 9.67972L16.4756 11.0939L17.8787 9.69083L16.4645 8.27662L15.0614 9.67972ZM16.4645 6.1553L20 9.69083L8.6863 21.0045L5.15076 17.469L16.4645 6.1553Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.364 5.06066L9.59619 6.82843L8.53553 5.76777L10.3033 4L11.364 5.06066ZM6.76778 6.82842L5 5.06067L6.06066 4L7.82843 5.76776L6.76778 6.82842ZM10.3033 10.364L8.53553 8.5962L9.59619 7.53554L11.364 9.3033L10.3033 10.364ZM7.82843 8.5962L6.06066 10.364L5 9.3033L6.76777 7.53554L7.82843 8.5962Z"
fill="currentColor"
/>
</svg>
Generate Posts
</button>
);
};

View File

@ -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 = () => {
))}
</div>
<AddProviderButton update={() => update(true)} />
{sortedIntegrations?.length > 0 && <GeneratorComponent />}
</div>
<div className="flex-1 flex flex-col gap-[14px]">
<Filters />

View File

@ -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 (
<div className="flex flex-col">
<div className="mb-[8px]">
<div className="w-[24px] h-[24px]">
{step === currentStep && currentStep !== 4 && (
{step === currentStep && currentStep !== lastStep && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@ -31,7 +31,7 @@ const Step: FC<{ step: number; title: string; currentStep: number }> = (
/>
</svg>
)}
{(currentStep > step || currentStep == 4) && (
{(currentStep > step || currentStep == lastStep) && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@ -45,7 +45,7 @@ const Step: FC<{ step: number; title: string; currentStep: number }> = (
/>
</svg>
)}
{step > currentStep && currentStep !== 4 && (
{step > currentStep && currentStep !== lastStep && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@ -73,7 +73,7 @@ const Step: FC<{ step: number; title: string; currentStep: number }> = (
);
};
const StepSpace: FC = () => {
export const StepSpace: FC = () => {
return (
<div className="flex-1 justify-center items-center flex px-[20px]">
<div className="h-[1px] w-full bg-white"></div>
@ -128,13 +128,13 @@ const Welcome: FC = () => {
<div className="bg-sixth p-[32px] w-full max-w-[920px] mx-auto flex flex-col gap-[24px] rounded-[4px] border border-[#172034] relative">
<h1 className="text-[24px]">Onboarding</h1>
<div className="flex">
<Step title="Profile" step={1} currentStep={step} />
<Step title="Profile" step={1} currentStep={step} lastStep={4} />
<StepSpace />
<Step title="Connect Github" step={2} currentStep={step} />
<Step title="Connect Github" step={2} currentStep={step} lastStep={4} />
<StepSpace />
<Step title="Connect Channels" step={3} currentStep={step} />
<Step title="Connect Channels" step={3} currentStep={step} lastStep={4} />
<StepSpace />
<Step title="Finish" step={4} currentStep={step} />
<Step title="Finish" step={4} currentStep={step} lastStep={4} />
</div>
{step === 1 && (
<>

View File

@ -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<string | undefined>(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 (
<div className="text-white fixed left-0 top-0 bg-black/80 z-[300] w-full h-full p-[60px] animate-fade">
<div className="flex flex-col w-full max-w-[1200px] mx-auto h-full bg-[#0B101B] border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
<div className="flex">
<div className="flex-1">
<TopTitle
title={'Select Post Before ' + date.format('DD/MM/YYYY HH:mm:ss')}
/>
</div>
<button
onClick={onCloseWithEmptyString}
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
type="button"
>
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
<div
className={
!noModal
? 'text-white fixed left-0 top-0 bg-black/80 z-[300] w-full h-full p-[60px] animate-fade'
: ''
}
>
<div
className={
!noModal
? 'flex flex-col w-full max-w-[1200px] mx-auto h-full bg-[#0B101B] border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative'
: ''
}
>
{!noModal && (
<div className="flex">
<div className="flex-1">
<TopTitle
title={
'Select Post Before ' + date.format('DD/MM/YYYY HH:mm:ss')
}
/>
</div>
<button
onClick={onCloseWithEmptyString}
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
type="button"
>
<path
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
</div>
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
>
<path
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
</div>
)}
<div className="mt-[10px]">
{!!data && data.length > 0 && (
<div className="flex flex-row flex-wrap gap-[10px]">
{data.map((p: any) => (
<div
onClick={select(p.id)}
className="cursor-pointer overflow-hidden flex gap-[20px] flex-col w-[200px] h-[200p] p-3 border border-tableBorder rounded-[8px] bg-secondary hover:bg-primary"
className={clsx(
'cursor-pointer overflow-hidden flex gap-[20px] flex-col w-[200px] h-[200p] p-3 border border-tableBorder rounded-[8px] hover:bg-primary',
current === p.id ? 'bg-primary' : 'bg-secondary'
)}
key={p.id}
>
<div className="flex gap-[10px] items-center">

View File

@ -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() {

View File

@ -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 [];
}
}

View File

@ -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;
}

View File

@ -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, ' ');
}
}

View File

@ -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 [];
}
})
);
}
}

View File

@ -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<DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, 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>,
HTMLSelectElement
> & {
error?: any;
extraForm?: RegisterOptions<any>;
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 (
<div className="flex flex-col gap-[6px]">
<div className={`${interClass} text-[14px]`}>{label}</div>
<select {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} />
<div className="text-red-400 text-[12px]">{err || <>&nbsp;</>}</div>
</div>
)
}
return (
<div className="flex flex-col gap-[6px]">
<div className={`${interClass} text-[14px]`}>{label}</div>
<select
{...(disableForm ? {} : form.register(props.name, extraForm))}
className={clsx(
'bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText',
className
)}
{...rest}
/>
<div className="text-red-400 text-[12px]">{err || <>&nbsp;</>}</div>
</div>
);
};

117
package-lock.json generated
View File

@ -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",

View File

@ -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",