Merge pull request #175 from gitroomhq/feat/chatgpt
Added a generate post with ChatGPT
This commit is contained in:
commit
8b794be985
|
|
@ -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,8 @@ 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';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
|
||||
@ApiTags('Posts')
|
||||
@Controller('/posts')
|
||||
|
|
@ -85,10 +87,27 @@ export class PostsController {
|
|||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: CreatePostDto
|
||||
) {
|
||||
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
|
||||
@Post('/generator/draft')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH])
|
||||
generatePostsDraft(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: CreateGeneratedPostsDto
|
||||
) {
|
||||
return this._postsService.generatePostsDraft(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,
|
||||
|
|
|
|||
|
|
@ -283,4 +283,8 @@ html {
|
|||
|
||||
.editor * {
|
||||
color: white;
|
||||
}
|
||||
|
||||
:empty + .existing-empty {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
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';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const ThirdStep: FC<{ week: number; year: number }> = (props) => {
|
||||
const { week, year } = props;
|
||||
|
||||
const gotToPosts = useCallback(() => {
|
||||
window.location.href = `/launches?week=${week}&year=${year}`;
|
||||
}, [week, year]);
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[20px] mb-[20px] flex flex-col items-center justify-center text-center mt-[20px] gap-[20px]">
|
||||
<img src="/success.svg" alt="success" />
|
||||
Your posts have been scheduled as drafts.
|
||||
<br />
|
||||
<Button onClick={gotToPosts}>Click here to see them</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SecondStep: FC<{
|
||||
posts: Array<Array<{ post: string }>>;
|
||||
url: string;
|
||||
postId?: string;
|
||||
nextStep: (params: { week: number; year: number }) => void;
|
||||
}> = (props) => {
|
||||
const { posts, nextStep, url, postId } = props;
|
||||
const fetch = useFetch();
|
||||
const [selected, setSelected] = useState<Array<string>>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
values: {
|
||||
date: dayjs().week() + '_' + dayjs().year(),
|
||||
},
|
||||
});
|
||||
|
||||
const addPost = useCallback(
|
||||
(index: string) => () => {
|
||||
if (selected.includes(index)) {
|
||||
setSelected(selected.filter((i) => i !== index));
|
||||
return;
|
||||
}
|
||||
setSelected([...selected, index]);
|
||||
},
|
||||
[selected]
|
||||
);
|
||||
|
||||
const list = useMemo(() => {
|
||||
const currentDate = dayjs();
|
||||
return [...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')})`,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createPosts: SubmitHandler<{
|
||||
date: any;
|
||||
}> = useCallback(
|
||||
async (values) => {
|
||||
setLoading(true);
|
||||
await fetch('/posts/generator/draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
posts: posts
|
||||
.filter((_, index) => selected.includes(String(index)))
|
||||
.map((po) => ({ list: po })),
|
||||
url,
|
||||
postId: postId ? `(post:${postId})` : undefined,
|
||||
year: values.date.year,
|
||||
week: values.date.week,
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
nextStep({
|
||||
week: values.date.week,
|
||||
year: values.date.year,
|
||||
});
|
||||
},
|
||||
[selected, postId, url]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(createPosts)}>
|
||||
<FormProvider {...form}>
|
||||
<div className={loading ? 'opacity-75' : ''}>
|
||||
<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="text-[20px] mb-[20px]">
|
||||
Click on the posts you would like to schedule.
|
||||
<br />
|
||||
They will be saved as drafts and you can edit them later.
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-[25px] select-none cursor-pointer">
|
||||
{posts.map((post, index) => (
|
||||
<div
|
||||
onClick={addPost(String(index))}
|
||||
className={clsx(
|
||||
'flex flex-col h-[200px] border rounded-[4px] group hover:border-white relative',
|
||||
selected.includes(String(index))
|
||||
? 'border-white'
|
||||
: 'border-fifth'
|
||||
)}
|
||||
key={post[0].post}
|
||||
>
|
||||
{post.length > 1 && (
|
||||
<div className="bg-[#612AD5] absolute -left-[15px] -top-[15px] z-[100] p-[3px] rounded-[10px]">
|
||||
a thread
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 relative h-full w-full group-hover:bg-black',
|
||||
selected.includes(String(index)) && 'bg-black'
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-0 top-0 w-full h-full p-[16px]">
|
||||
<div className="w-full h-full overflow-hidden text-ellipsis group-hover:bg-black resize-none outline-none">
|
||||
{post[0].post.split('\n\n')[0]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-[20px] flex justify-end">
|
||||
<Button type="submit" disabled={!selected.length} loading={loading}>
|
||||
Create posts
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const FirstStep: FC<{
|
||||
nextStep: (
|
||||
posts: Array<Array<{ post: string }>>,
|
||||
url: string,
|
||||
postId?: string
|
||||
) => void;
|
||||
}> = (props) => {
|
||||
const { nextStep } = props;
|
||||
const fetch = useFetch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(GeneratorDto);
|
||||
}, []);
|
||||
|
||||
const form = useForm({
|
||||
mode: 'all',
|
||||
resolver,
|
||||
values: {
|
||||
url: '',
|
||||
post: undefined as undefined | string,
|
||||
},
|
||||
});
|
||||
|
||||
const [url, post] = form.watch(['url', 'post']);
|
||||
|
||||
const makeSelect = useCallback(
|
||||
(post?: string) => {
|
||||
form.setValue('post', post?.split?.(':')[1]?.split(')')?.[0]);
|
||||
|
||||
if (!post && !url) {
|
||||
form.setError('url', {
|
||||
message: 'You need to select a post or a URL',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (post && url) {
|
||||
form.setError('url', {
|
||||
message: 'You can only have a URL or a post',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
form.setError('url', {
|
||||
message: '',
|
||||
});
|
||||
},
|
||||
[post, url]
|
||||
);
|
||||
|
||||
const onSubmit: SubmitHandler<{
|
||||
url: string;
|
||||
post: string | undefined;
|
||||
}> = useCallback(async (value) => {
|
||||
setLoading(true);
|
||||
const data = await (
|
||||
await fetch('/posts/generator', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(value),
|
||||
})
|
||||
).json();
|
||||
nextStep(data.list, value.url, value.post);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={loading ? 'pointer-events-none select-none opacity-75' : ''}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex flex-col">
|
||||
<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="flex flex-col-reverse">
|
||||
<div className="p-[16px] bg-input border-fifth border rounded-[4px] min-h-[500px] empty:hidden">
|
||||
<PostSelector
|
||||
noModal={true}
|
||||
onClose={() => null}
|
||||
onSelect={makeSelect}
|
||||
date={dayjs().add(1, 'year')}
|
||||
only="article"
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-[10px] existing-empty">
|
||||
Or select from exising posts
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[20px] flex justify-end">
|
||||
<Button type="submit" disabled={!!(url && post)} loading={loading}>
|
||||
{url && post ? "You can't have both URL and a POST" : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
export const GeneratorPopup = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const modals = useModals();
|
||||
const [posts, setPosts] = useState<
|
||||
| {
|
||||
posts: Array<Array<{ post: string }>>;
|
||||
url: string;
|
||||
postId?: string;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
const [yearAndWeek, setYearAndWeek] = useState<{
|
||||
year: number;
|
||||
week: number;
|
||||
} | null>(null);
|
||||
|
||||
const closeAll = useCallback(() => {
|
||||
modals.closeAll();
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<button
|
||||
onClick={closeAll}
|
||||
className="outline-none absolute right-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root 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"
|
||||
>
|
||||
<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>
|
||||
<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={(posts, url: string, postId?: string) => {
|
||||
setPosts({
|
||||
posts,
|
||||
url,
|
||||
postId,
|
||||
});
|
||||
setStep(2);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<SecondStep
|
||||
{...posts!}
|
||||
nextStep={(e) => {
|
||||
setYearAndWeek(e);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<ThirdStep week={yearAndWeek?.week!} year={yearAndWeek?.year!} />
|
||||
)}
|
||||
</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,
|
||||
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] outline-none"
|
||||
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 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import {
|
||||
executeCommand,
|
||||
|
|
@ -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,12 @@ export const useShowPostSelector = (day: dayjs.Dayjs) => {
|
|||
|
||||
export const PostSelector: FC<{
|
||||
onClose: () => void;
|
||||
onSelect: (tag: string) => void;
|
||||
onSelect: (tag: string | undefined) => void;
|
||||
only?: 'article' | 'social';
|
||||
noModal?: boolean;
|
||||
date: dayjs.Dayjs;
|
||||
}> = (props) => {
|
||||
const { onClose, onSelect, date } = props;
|
||||
const { onClose, onSelect, only, date, noModal } = props;
|
||||
const fetch = useFetch();
|
||||
const fetchOldPosts = useCallback(() => {
|
||||
return fetch(
|
||||
|
|
@ -98,78 +108,116 @@ export const PostSelector: FC<{
|
|||
onClose();
|
||||
}, []);
|
||||
|
||||
const select = useCallback((id: string) => () => {
|
||||
onSelect(`(post:${id})`);
|
||||
onClose();
|
||||
}, []);
|
||||
const [current, setCurrent] = useState<string | undefined>(undefined);
|
||||
|
||||
const { isLoading, data } = useSWR('old-posts', fetchOldPosts);
|
||||
const select = useCallback(
|
||||
(id: string) => () => {
|
||||
setCurrent(current === id ? undefined : id);
|
||||
onSelect(current === id ? undefined : `(post:${id})`);
|
||||
onClose();
|
||||
},
|
||||
[current]
|
||||
);
|
||||
|
||||
const { data: loadData } = useSWR('old-posts', fetchOldPosts);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!only) {
|
||||
return loadData;
|
||||
}
|
||||
return loadData?.filter((p: any) => p.integration.type === only);
|
||||
}, [loadData, only]);
|
||||
|
||||
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"
|
||||
<>
|
||||
{!noModal ||
|
||||
(data?.length > 0 && (
|
||||
<div
|
||||
className={
|
||||
!noModal
|
||||
? 'text-white fixed left-0 top-0 bg-black/80 z-[300] w-full h-full p-[60px] animate-fade'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
<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'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<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"
|
||||
key={p.id}
|
||||
>
|
||||
<div className="flex gap-[10px] items-center">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={p.integration.picture}
|
||||
className="w-[32px] h-[32px] rounded-full"
|
||||
/>
|
||||
<img
|
||||
className="w-[20px] h-[20px] rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
src={
|
||||
`/icons/platforms/` +
|
||||
p?.integration?.providerIdentifier +
|
||||
'.png'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>{p.integration.name}</div>
|
||||
{!noModal && (
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TopTitle
|
||||
title={
|
||||
'Select Post Before ' +
|
||||
date.format('DD/MM/YYYY HH:mm:ss')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">{removeMd(p.content)}</div>
|
||||
<div>Status: {p.state}</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"
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
)}
|
||||
{!!data && data.length > 0 && (
|
||||
<div className="mt-[10px]">
|
||||
<div className="flex flex-row flex-wrap gap-[10px]">
|
||||
{data.map((p: any) => (
|
||||
<div
|
||||
onClick={select(p.id)}
|
||||
className={clsx(
|
||||
'cursor-pointer overflow-hidden flex gap-[20px] flex-col w-[200px] h-[200px] text-ellipsis 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">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={p.integration.picture}
|
||||
className="w-[32px] h-[32px] rounded-full"
|
||||
/>
|
||||
<img
|
||||
className="w-[20px] h-[20px] rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
src={
|
||||
`/icons/platforms/` +
|
||||
p?.integration?.providerIdentifier +
|
||||
'.png'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>{p.integration.name}</div>
|
||||
</div>
|
||||
<div className="flex-1">{removeMd(p.content)}</div>
|
||||
<div>Status: {p.state}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export class PostsRepository {
|
|||
name: true,
|
||||
providerIdentifier: true,
|
||||
picture: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -62,9 +63,10 @@ export class PostsRepository {
|
|||
getPosts(orgId: string, query: GetPostsDto) {
|
||||
const date = dayjs().year(query.year).isoWeek(query.week);
|
||||
|
||||
const startDate = date.startOf('isoWeek').toDate();
|
||||
const endDate = date.endOf('isoWeek').toDate();
|
||||
const startDate = date.startOf('week').toDate();
|
||||
const endDate = date.endOf('week').toDate();
|
||||
|
||||
console.log(startDate, endDate);
|
||||
return this._post.model.post.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
|
|
|
|||
|
|
@ -7,9 +7,15 @@ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integ
|
|||
import { Integration, Post, Media, From } from '@prisma/client';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { capitalize } from 'lodash';
|
||||
import { capitalize, chunk, shuffle } 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';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
type PostWithConditionals = Post & {
|
||||
integration?: Integration;
|
||||
|
|
@ -24,7 +30,10 @@ export class PostsService {
|
|||
private _integrationManager: IntegrationManager,
|
||||
private _notificationService: NotificationService,
|
||||
private _messagesService: MessagesService,
|
||||
private _stripeService: StripeService
|
||||
private _stripeService: StripeService,
|
||||
private _extractContentService: ExtractContentService,
|
||||
private _openAiService: OpenaiService,
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
|
||||
async getPostsRecursively(
|
||||
|
|
@ -257,7 +266,11 @@ export class PostsService {
|
|||
throw new Error('You can not add a post to this publication');
|
||||
}
|
||||
const getOrgByOrder = await this._messagesService.getOrgByOrder(order);
|
||||
const submit = await this._postRepository.submit(id, order, getOrgByOrder?.messageGroup?.buyerOrganizationId!);
|
||||
const submit = await this._postRepository.submit(
|
||||
id,
|
||||
order,
|
||||
getOrgByOrder?.messageGroup?.buyerOrganizationId!
|
||||
);
|
||||
const messageModel = await this._messagesService.createNewMessage(
|
||||
submit?.submittedForOrder?.messageGroupId || '',
|
||||
From.SELLER,
|
||||
|
|
@ -432,4 +445,101 @@ export class PostsService {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPostContent(postId: string) {
|
||||
const post = await this._postRepository.getPostById(postId);
|
||||
if (!post) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return post.content;
|
||||
}
|
||||
|
||||
async generatePosts(orgId: string, body: GeneratorDto) {
|
||||
const content = body.url
|
||||
? await this._extractContentService.extractContent(body.url)
|
||||
: await this.loadPostContent(body.post);
|
||||
|
||||
const value = body.url
|
||||
? await this._openAiService.extractWebsiteText(content!)
|
||||
: await this._openAiService.generatePosts(content!);
|
||||
return { list: value };
|
||||
}
|
||||
|
||||
async generatePostsDraft(orgId: string, body: CreateGeneratedPostsDto) {
|
||||
const getAllIntegrations = (
|
||||
await this._integrationService.getIntegrationsList(orgId)
|
||||
).filter((f) => !f.disabled && f.providerIdentifier !== 'reddit');
|
||||
|
||||
// const posts = chunk(body.posts, getAllIntegrations.length);
|
||||
const allDates = dayjs()
|
||||
.isoWeek(body.week)
|
||||
.year(body.year)
|
||||
.startOf('isoWeek');
|
||||
|
||||
const dates = [...new Array(7)].map((_, i) => {
|
||||
return allDates.add(i, 'day').format('YYYY-MM-DD');
|
||||
});
|
||||
|
||||
const findTime = (): string => {
|
||||
const totalMinutes = Math.floor(Math.random() * 144) * 10;
|
||||
|
||||
// Convert total minutes to hours and minutes
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
// Format hours and minutes to always be two digits
|
||||
const formattedHours = hours.toString().padStart(2, '0');
|
||||
const formattedMinutes = minutes.toString().padStart(2, '0');
|
||||
const randomDate =
|
||||
shuffle(dates)[0] + 'T' + `${formattedHours}:${formattedMinutes}:00`;
|
||||
|
||||
if (dayjs(randomDate).isBefore(dayjs())) {
|
||||
return findTime();
|
||||
}
|
||||
|
||||
return randomDate;
|
||||
};
|
||||
|
||||
for (const integration of getAllIntegrations) {
|
||||
for (const toPost of body.posts) {
|
||||
const group = makeId(10);
|
||||
const randomDate = findTime();
|
||||
|
||||
await this.createPost(orgId, {
|
||||
type: 'draft',
|
||||
date: randomDate,
|
||||
order: '',
|
||||
posts: [
|
||||
{
|
||||
group,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
},
|
||||
settings: {
|
||||
subtitle: '',
|
||||
title: '',
|
||||
tags: [],
|
||||
subreddit: [],
|
||||
},
|
||||
value: [
|
||||
...toPost.list.map((l) => ({
|
||||
id: '',
|
||||
content: l.post,
|
||||
image: [],
|
||||
})),
|
||||
{
|
||||
id: '',
|
||||
content: `Check out the full story here:\n${
|
||||
body.postId || body.url
|
||||
}`,
|
||||
image: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsDefined,
|
||||
IsNumber,
|
||||
IsString,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class InnerPost {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
post: string;
|
||||
}
|
||||
|
||||
class PostGroup {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => InnerPost)
|
||||
@IsDefined()
|
||||
list: InnerPost[];
|
||||
}
|
||||
|
||||
export class CreateGeneratedPostsDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PostGroup)
|
||||
@IsDefined()
|
||||
posts: PostGroup[];
|
||||
|
||||
@IsNumber()
|
||||
@IsDefined()
|
||||
week: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsDefined()
|
||||
year: number;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
@ValidateIf((o) => !o.url)
|
||||
url: string;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
@ValidateIf((o) => !o.url)
|
||||
postId: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
IsDefined,
|
||||
IsInt,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class GeneratorDto {
|
||||
@IsString()
|
||||
@ValidateIf((o) => !o.post)
|
||||
@IsUrl(
|
||||
{},
|
||||
{
|
||||
message: 'Invalid URL',
|
||||
}
|
||||
)
|
||||
url: string;
|
||||
|
||||
@ValidateIf((o) => !o.url)
|
||||
@IsString()
|
||||
post: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
function findDepth(element: Element) {
|
||||
let depth = 0;
|
||||
let elementer = element;
|
||||
while (elementer.parentNode) {
|
||||
depth++;
|
||||
// @ts-ignore
|
||||
elementer = elementer.parentNode;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ExtractContentService {
|
||||
async extractContent(url: string) {
|
||||
const load = await (await fetch(url)).text();
|
||||
const dom = new JSDOM(load);
|
||||
|
||||
// only element that has a title
|
||||
const allTitles = Array.from(dom.window.document.querySelectorAll('*'))
|
||||
.filter((f) => {
|
||||
return (
|
||||
f.querySelector('h1') ||
|
||||
f.querySelector('h2') ||
|
||||
f.querySelector('h3') ||
|
||||
f.querySelector('h4') ||
|
||||
f.querySelector('h5') ||
|
||||
f.querySelector('h6')
|
||||
);
|
||||
})
|
||||
.reverse();
|
||||
|
||||
const findTheOneWithMostTitles = allTitles.reduce(
|
||||
(all, current) => {
|
||||
const depth = findDepth(current);
|
||||
const calculate = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].reduce(
|
||||
(total, tag) => {
|
||||
if (current.querySelector(tag)) {
|
||||
return total + 1;
|
||||
}
|
||||
return total;
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
if (calculate > all.total) {
|
||||
return { total: calculate, depth, element: current };
|
||||
}
|
||||
|
||||
if (depth > all.depth) {
|
||||
return { total: calculate, depth, element: current };
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
{ total: 0, depth: 0, element: null as Element | null }
|
||||
);
|
||||
|
||||
return findTheOneWithMostTitles?.element?.textContent?.replace(/\n/g, ' ').replace(/ {2,}/g, ' ');
|
||||
//
|
||||
// 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,90 @@
|
|||
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 generatePosts(content: string) {
|
||||
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: content!,
|
||||
},
|
||||
],
|
||||
n: 5,
|
||||
temperature: 1,
|
||||
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: content!,
|
||||
},
|
||||
],
|
||||
n: 5,
|
||||
temperature: 1,
|
||||
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)
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/ {2,}/g, ' ') +
|
||||
']'
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(content);
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
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;
|
||||
|
||||
return this.generatePosts(articleContent!);
|
||||
}
|
||||
}
|
||||
|
|
@ -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