feat: separate posts with ai
This commit is contained in:
parent
fd9bfecb65
commit
3a31da4b41
|
|
@ -179,4 +179,12 @@ export class PostsController {
|
|||
) {
|
||||
return this._postsService.changeDate(org.id, id, date);
|
||||
}
|
||||
|
||||
@Post('/separate-posts')
|
||||
async separatePosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: { content: string, len: number }
|
||||
) {
|
||||
return this._postsService.separatePosts(body.content, body.len);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ import { InternalChannels } from '@gitroom/frontend/components/launches/internal
|
|||
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useSet } from '@gitroom/frontend/components/launches/set.context';
|
||||
import { SeparatePost } from '@gitroom/frontend/components/launches/separate.post';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
// Simple component to change back to settings on after changing tab
|
||||
export const SetTab: FC<{
|
||||
|
|
@ -177,7 +179,8 @@ export const withProvider = function <T extends object>(
|
|||
// this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation
|
||||
const form = useValues(
|
||||
set?.set
|
||||
? set?.set?.posts?.find((p) => p?.integration?.id === props?.id)?.settings
|
||||
? set?.set?.posts?.find((p) => p?.integration?.id === props?.id)
|
||||
?.settings
|
||||
: existingData.settings,
|
||||
props.id,
|
||||
props.identifier,
|
||||
|
|
@ -201,6 +204,7 @@ export const withProvider = function <T extends object>(
|
|||
},
|
||||
[InPlaceValue]
|
||||
);
|
||||
|
||||
const merge = useCallback(() => {
|
||||
setInPlaceValue(
|
||||
InPlaceValue.reduce(
|
||||
|
|
@ -222,6 +226,20 @@ export const withProvider = function <T extends object>(
|
|||
)
|
||||
);
|
||||
}, [InPlaceValue]);
|
||||
|
||||
const separatePosts = useCallback(
|
||||
(posts: string[]) => {
|
||||
setInPlaceValue(
|
||||
posts.map((p, i) => ({
|
||||
content: p,
|
||||
id: InPlaceValue?.[i]?.id || makeId(10),
|
||||
image: InPlaceValue?.[i]?.image || [],
|
||||
}))
|
||||
);
|
||||
},
|
||||
[InPlaceValue]
|
||||
);
|
||||
|
||||
const changeImage = useCallback(
|
||||
(index: number) =>
|
||||
(newValue: {
|
||||
|
|
@ -602,11 +620,25 @@ export const withProvider = function <T extends object>(
|
|||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
{InPlaceValue.length > 1 && (
|
||||
<div className="flex gap-[4px]">
|
||||
{InPlaceValue.length > 1 && (
|
||||
<div>
|
||||
<MergePost merge={merge} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<MergePost merge={merge} />
|
||||
<SeparatePost
|
||||
changeLoading={setUploading}
|
||||
posts={InPlaceValue.map((p) => p.content)}
|
||||
len={
|
||||
typeof maximumCharacters === 'number'
|
||||
? maximumCharacters
|
||||
: 10000
|
||||
}
|
||||
merge={separatePosts}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</EditorWrapper>,
|
||||
document.querySelector('#renderEditor')!
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { Button } from '@gitroom/react/form/button';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
export const SeparatePost: FC<{
|
||||
posts: string[];
|
||||
len: number;
|
||||
merge: (posts: string[]) => void;
|
||||
changeLoading: (loading: boolean) => void;
|
||||
}> = (props) => {
|
||||
const { len, posts } = props;
|
||||
const t = useT();
|
||||
const fetch = useFetch();
|
||||
|
||||
const notReversible = useCallback(async () => {
|
||||
if (
|
||||
await deleteDialog(
|
||||
'Are you sure you want to separate all posts? This action is not reversible.',
|
||||
'Yes'
|
||||
)
|
||||
) {
|
||||
props.changeLoading(true);
|
||||
const merge = props.posts.join('\n');
|
||||
const { posts } = await (
|
||||
await fetch('/posts/separate-posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: merge,
|
||||
len: props.len,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
props.merge(posts);
|
||||
props.changeLoading(false);
|
||||
}
|
||||
}, [len, posts]);
|
||||
|
||||
return (
|
||||
<Button className="!h-[30px] !text-sm !bg-red-800" onClick={notReversible}>
|
||||
{t('separate_post', 'Separate post to multiple posts')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
@ -28,6 +28,7 @@ import axios from 'axios';
|
|||
import sharp from 'sharp';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { Readable } from 'stream';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
dayjs.extend(utc);
|
||||
|
||||
type PostWithConditionals = Post & {
|
||||
|
|
@ -48,7 +49,8 @@ export class PostsService {
|
|||
private _integrationService: IntegrationService,
|
||||
private _mediaService: MediaService,
|
||||
private _shortLinkService: ShortLinkService,
|
||||
private _webhookService: WebhooksService
|
||||
private _webhookService: WebhooksService,
|
||||
private openaiService: OpenaiService,
|
||||
) {}
|
||||
|
||||
async getStatistics(orgId: string, id: string) {
|
||||
|
|
@ -570,7 +572,7 @@ export class PostsService {
|
|||
}
|
||||
}
|
||||
|
||||
private async postArticle(integration: Integration, posts: Post[]) {
|
||||
private async postArticle(integration: Integration, posts: Post[]): Promise<any> {
|
||||
const getIntegration = this._integrationManager.getArticlesIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
|
@ -652,7 +654,7 @@ export class PostsService {
|
|||
return messageModel;
|
||||
}
|
||||
|
||||
async createPost(orgId: string, body: CreatePostDto) {
|
||||
async createPost(orgId: string, body: CreatePostDto): Promise<any[]> {
|
||||
const postList = [];
|
||||
for (const post of body.posts) {
|
||||
const messages = post.value.map((p) => p.content);
|
||||
|
|
@ -727,6 +729,10 @@ export class PostsService {
|
|||
return postList;
|
||||
}
|
||||
|
||||
async separatePosts(content: string, len: number) {
|
||||
return this.openaiService.separatePosts(content, len);
|
||||
}
|
||||
|
||||
async changeDate(orgId: string, id: string, date: string) {
|
||||
const getPostById = await this._postRepository.getPostById(id, orgId);
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -125,4 +125,76 @@ export class OpenaiService {
|
|||
|
||||
return this.generatePosts(articleContent!);
|
||||
}
|
||||
|
||||
async separatePosts(content: string, len: number) {
|
||||
const SeparatePostsPrompt = z.object({
|
||||
posts: z.array(z.string()),
|
||||
});
|
||||
|
||||
const SeparatePostPrompt = z.object({
|
||||
post: z.string().max(len),
|
||||
});
|
||||
|
||||
const posts =
|
||||
(
|
||||
await openai.beta.chat.completions.parse({
|
||||
model: 'gpt-4.1',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an assistant that take a social media post and break it to a thread, each post must be minimum ${len - 10} and maximum ${len} characters, keeping the exact wording and break lines, however make sure you split posts based on context`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: content,
|
||||
},
|
||||
],
|
||||
response_format: zodResponseFormat(
|
||||
SeparatePostsPrompt,
|
||||
'separatePosts'
|
||||
),
|
||||
})
|
||||
).choices[0].message.parsed?.posts || [];
|
||||
|
||||
return {
|
||||
posts: await Promise.all(
|
||||
posts.map(async (post) => {
|
||||
if (post.length <= len) {
|
||||
return post;
|
||||
}
|
||||
|
||||
let retries = 4;
|
||||
while (retries) {
|
||||
try {
|
||||
return (
|
||||
(
|
||||
await openai.beta.chat.completions.parse({
|
||||
model: 'gpt-4.1',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an assistant that take a social media post and shrink it to be maximum ${len} characters, keeping the exact wording and break lines`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: post,
|
||||
},
|
||||
],
|
||||
response_format: zodResponseFormat(
|
||||
SeparatePostPrompt,
|
||||
'separatePost'
|
||||
),
|
||||
})
|
||||
).choices[0].message.parsed?.post || ''
|
||||
);
|
||||
} catch (e) {
|
||||
retries--;
|
||||
}
|
||||
}
|
||||
|
||||
return post;
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -484,5 +484,6 @@
|
|||
"change_language": "Change Language",
|
||||
"that_a_wrap": "That's a wrap!\n\nIf you enjoyed this thread:\n\n1. Follow me @{{username}} for more of these\n2. RT the tweet below to share this thread with your audience\n",
|
||||
"post_as_images_carousel": "Post as images carousel",
|
||||
"save_set": "Save Set"
|
||||
"save_set": "Save Set",
|
||||
"separate_post": "Separate post to multiple posts"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue