feat: step 1
This commit is contained in:
parent
3cc4ea22a9
commit
72c1e06f74
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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, ' ');
|
||||
}
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 || <> </>}</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 || <> </>}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue