Skip to content

Commit 3a31da4

Browse files
committed
feat: separate posts with ai
1 parent fd9bfec commit 3a31da4

File tree

6 files changed

+172
-8
lines changed

6 files changed

+172
-8
lines changed

apps/backend/src/api/routes/posts.controller.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,12 @@ export class PostsController {
179179
) {
180180
return this._postsService.changeDate(org.id, id, date);
181181
}
182+
183+
@Post('/separate-posts')
184+
async separatePosts(
185+
@GetOrgFromRequest() org: Organization,
186+
@Body() body: { content: string, len: number }
187+
) {
188+
return this._postsService.separatePosts(body.content, body.len);
189+
}
182190
}

apps/frontend/src/components/launches/providers/high.order.provider.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import { InternalChannels } from '@gitroom/frontend/components/launches/internal
4646
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
4747
import { useT } from '@gitroom/react/translation/get.transation.service.client';
4848
import { useSet } from '@gitroom/frontend/components/launches/set.context';
49+
import { SeparatePost } from '@gitroom/frontend/components/launches/separate.post';
50+
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
4951

5052
// Simple component to change back to settings on after changing tab
5153
export const SetTab: FC<{
@@ -177,7 +179,8 @@ export const withProvider = function <T extends object>(
177179
// this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation
178180
const form = useValues(
179181
set?.set
180-
? set?.set?.posts?.find((p) => p?.integration?.id === props?.id)?.settings
182+
? set?.set?.posts?.find((p) => p?.integration?.id === props?.id)
183+
?.settings
181184
: existingData.settings,
182185
props.id,
183186
props.identifier,
@@ -201,6 +204,7 @@ export const withProvider = function <T extends object>(
201204
},
202205
[InPlaceValue]
203206
);
207+
204208
const merge = useCallback(() => {
205209
setInPlaceValue(
206210
InPlaceValue.reduce(
@@ -222,6 +226,20 @@ export const withProvider = function <T extends object>(
222226
)
223227
);
224228
}, [InPlaceValue]);
229+
230+
const separatePosts = useCallback(
231+
(posts: string[]) => {
232+
setInPlaceValue(
233+
posts.map((p, i) => ({
234+
content: p,
235+
id: InPlaceValue?.[i]?.id || makeId(10),
236+
image: InPlaceValue?.[i]?.image || [],
237+
}))
238+
);
239+
},
240+
[InPlaceValue]
241+
);
242+
225243
const changeImage = useCallback(
226244
(index: number) =>
227245
(newValue: {
@@ -602,11 +620,25 @@ export const withProvider = function <T extends object>(
602620
</div>
603621
</Fragment>
604622
))}
605-
{InPlaceValue.length > 1 && (
623+
<div className="flex gap-[4px]">
624+
{InPlaceValue.length > 1 && (
625+
<div>
626+
<MergePost merge={merge} />
627+
</div>
628+
)}
606629
<div>
607-
<MergePost merge={merge} />
630+
<SeparatePost
631+
changeLoading={setUploading}
632+
posts={InPlaceValue.map((p) => p.content)}
633+
len={
634+
typeof maximumCharacters === 'number'
635+
? maximumCharacters
636+
: 10000
637+
}
638+
merge={separatePosts}
639+
/>
608640
</div>
609-
)}
641+
</div>
610642
</div>
611643
</EditorWrapper>,
612644
document.querySelector('#renderEditor')!
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Button } from '@gitroom/react/form/button';
2+
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
3+
import { FC, useCallback } from 'react';
4+
import { useT } from '@gitroom/react/translation/get.transation.service.client';
5+
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
6+
export const SeparatePost: FC<{
7+
posts: string[];
8+
len: number;
9+
merge: (posts: string[]) => void;
10+
changeLoading: (loading: boolean) => void;
11+
}> = (props) => {
12+
const { len, posts } = props;
13+
const t = useT();
14+
const fetch = useFetch();
15+
16+
const notReversible = useCallback(async () => {
17+
if (
18+
await deleteDialog(
19+
'Are you sure you want to separate all posts? This action is not reversible.',
20+
'Yes'
21+
)
22+
) {
23+
props.changeLoading(true);
24+
const merge = props.posts.join('\n');
25+
const { posts } = await (
26+
await fetch('/posts/separate-posts', {
27+
method: 'POST',
28+
body: JSON.stringify({
29+
content: merge,
30+
len: props.len,
31+
}),
32+
})
33+
).json();
34+
35+
props.merge(posts);
36+
props.changeLoading(false);
37+
}
38+
}, [len, posts]);
39+
40+
return (
41+
<Button className="!h-[30px] !text-sm !bg-red-800" onClick={notReversible}>
42+
{t('separate_post', 'Separate post to multiple posts')}
43+
</Button>
44+
);
45+
};

libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import axios from 'axios';
2828
import sharp from 'sharp';
2929
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
3030
import { Readable } from 'stream';
31+
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
3132
dayjs.extend(utc);
3233

3334
type PostWithConditionals = Post & {
@@ -48,7 +49,8 @@ export class PostsService {
4849
private _integrationService: IntegrationService,
4950
private _mediaService: MediaService,
5051
private _shortLinkService: ShortLinkService,
51-
private _webhookService: WebhooksService
52+
private _webhookService: WebhooksService,
53+
private openaiService: OpenaiService,
5254
) {}
5355

5456
async getStatistics(orgId: string, id: string) {
@@ -570,7 +572,7 @@ export class PostsService {
570572
}
571573
}
572574

573-
private async postArticle(integration: Integration, posts: Post[]) {
575+
private async postArticle(integration: Integration, posts: Post[]): Promise<any> {
574576
const getIntegration = this._integrationManager.getArticlesIntegration(
575577
integration.providerIdentifier
576578
);
@@ -652,7 +654,7 @@ export class PostsService {
652654
return messageModel;
653655
}
654656

655-
async createPost(orgId: string, body: CreatePostDto) {
657+
async createPost(orgId: string, body: CreatePostDto): Promise<any[]> {
656658
const postList = [];
657659
for (const post of body.posts) {
658660
const messages = post.value.map((p) => p.content);
@@ -727,6 +729,10 @@ export class PostsService {
727729
return postList;
728730
}
729731

732+
async separatePosts(content: string, len: number) {
733+
return this.openaiService.separatePosts(content, len);
734+
}
735+
730736
async changeDate(orgId: string, id: string, date: string) {
731737
const getPostById = await this._postRepository.getPostById(id, orgId);
732738
if (

libraries/nestjs-libraries/src/openai/openai.service.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,76 @@ export class OpenaiService {
125125

126126
return this.generatePosts(articleContent!);
127127
}
128+
129+
async separatePosts(content: string, len: number) {
130+
const SeparatePostsPrompt = z.object({
131+
posts: z.array(z.string()),
132+
});
133+
134+
const SeparatePostPrompt = z.object({
135+
post: z.string().max(len),
136+
});
137+
138+
const posts =
139+
(
140+
await openai.beta.chat.completions.parse({
141+
model: 'gpt-4.1',
142+
messages: [
143+
{
144+
role: 'system',
145+
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`,
146+
},
147+
{
148+
role: 'user',
149+
content: content,
150+
},
151+
],
152+
response_format: zodResponseFormat(
153+
SeparatePostsPrompt,
154+
'separatePosts'
155+
),
156+
})
157+
).choices[0].message.parsed?.posts || [];
158+
159+
return {
160+
posts: await Promise.all(
161+
posts.map(async (post) => {
162+
if (post.length <= len) {
163+
return post;
164+
}
165+
166+
let retries = 4;
167+
while (retries) {
168+
try {
169+
return (
170+
(
171+
await openai.beta.chat.completions.parse({
172+
model: 'gpt-4.1',
173+
messages: [
174+
{
175+
role: 'system',
176+
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`,
177+
},
178+
{
179+
role: 'user',
180+
content: post,
181+
},
182+
],
183+
response_format: zodResponseFormat(
184+
SeparatePostPrompt,
185+
'separatePost'
186+
),
187+
})
188+
).choices[0].message.parsed?.post || ''
189+
);
190+
} catch (e) {
191+
retries--;
192+
}
193+
}
194+
195+
return post;
196+
})
197+
),
198+
};
199+
}
128200
}

libraries/react-shared-libraries/src/translation/locales/en/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,5 +484,6 @@
484484
"change_language": "Change Language",
485485
"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",
486486
"post_as_images_carousel": "Post as images carousel",
487-
"save_set": "Save Set"
487+
"save_set": "Save Set",
488+
"separate_post": "Separate post to multiple posts"
488489
}

0 commit comments

Comments
 (0)