Merge pull request #833 from gitroomhq/feat/3rdparties
Add Integrations
This commit is contained in:
commit
9a74d756b4
|
|
@ -34,6 +34,7 @@ import { AutopostController } from '@gitroom/backend/api/routes/autopost.control
|
|||
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
|
||||
import { McpController } from '@gitroom/backend/api/routes/mcp.controller';
|
||||
import { SetsController } from '@gitroom/backend/api/routes/sets.controller';
|
||||
import { ThirdPartyController } from '@gitroom/backend/api/routes/third-party.controller';
|
||||
|
||||
const authenticatedController = [
|
||||
UsersController,
|
||||
|
|
@ -52,6 +53,7 @@ const authenticatedController = [
|
|||
SignatureController,
|
||||
AutopostController,
|
||||
SetsController,
|
||||
ThirdPartyController,
|
||||
];
|
||||
@Module({
|
||||
imports: [UploadModule],
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class PostsController {
|
|||
private _starsService: StarsService,
|
||||
private _messagesService: MessagesService,
|
||||
private _agentGraphService: AgentGraphService,
|
||||
private _shortLinkService: ShortLinkService
|
||||
private _shortLinkService: ShortLinkService,
|
||||
) {}
|
||||
|
||||
@Get('/:id/statistics')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ThirdPartyManager } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.manager';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
|
||||
@ApiTags('Third Party')
|
||||
@Controller('/third-party')
|
||||
export class ThirdPartyController {
|
||||
private storage = UploadFactory.createStorage();
|
||||
|
||||
constructor(
|
||||
private _thirdPartyManager: ThirdPartyManager,
|
||||
private _mediaService: MediaService,
|
||||
) {}
|
||||
|
||||
@Get('/list')
|
||||
async getThirdPartyList() {
|
||||
return this._thirdPartyManager.getAllThirdParties();
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
async getSavedThirdParty(@GetOrgFromRequest() organization: Organization) {
|
||||
return Promise.all(
|
||||
(
|
||||
await this._thirdPartyManager.getAllThirdPartiesByOrganization(
|
||||
organization.id
|
||||
)
|
||||
).map((thirdParty) => {
|
||||
const { description, fields, position, title, identifier } =
|
||||
this._thirdPartyManager.getThirdPartyByName(thirdParty.identifier);
|
||||
return {
|
||||
...thirdParty,
|
||||
title,
|
||||
position,
|
||||
fields,
|
||||
description,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
deleteById(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._thirdPartyManager.deleteIntegration(organization.id, id);
|
||||
}
|
||||
|
||||
@Post('/:id/submit')
|
||||
async generate(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() data: any
|
||||
) {
|
||||
const thirdParty = await this._thirdPartyManager.getIntegrationById(
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
|
||||
if (!thirdParty) {
|
||||
throw new HttpException('Integration not found', 404);
|
||||
}
|
||||
|
||||
const thirdPartyInstance = this._thirdPartyManager.getThirdPartyByName(
|
||||
thirdParty.identifier
|
||||
);
|
||||
|
||||
if (!thirdPartyInstance) {
|
||||
throw new HttpException('Invalid identifier', 400);
|
||||
}
|
||||
|
||||
const loadedData = await thirdPartyInstance?.instance?.sendData(
|
||||
AuthService.fixedDecryption(thirdParty.apiKey),
|
||||
data
|
||||
);
|
||||
|
||||
const file = await this.storage.uploadSimple(loadedData);
|
||||
return this._mediaService.saveFile(organization.id, file.split('/').pop(), file);
|
||||
}
|
||||
|
||||
@Post('/function/:id/:functionName')
|
||||
async callFunction(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string,
|
||||
@Param('functionName') functionName: string,
|
||||
@Body() data: any
|
||||
) {
|
||||
const thirdParty = await this._thirdPartyManager.getIntegrationById(
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
|
||||
if (!thirdParty) {
|
||||
throw new HttpException('Integration not found', 404);
|
||||
}
|
||||
|
||||
const thirdPartyInstance = this._thirdPartyManager.getThirdPartyByName(
|
||||
thirdParty.identifier
|
||||
);
|
||||
|
||||
if (!thirdPartyInstance) {
|
||||
throw new HttpException('Invalid identifier', 400);
|
||||
}
|
||||
|
||||
return thirdPartyInstance?.instance?.[functionName](
|
||||
AuthService.fixedDecryption(thirdParty.apiKey),
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/:identifier')
|
||||
async addApiKey(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('identifier') identifier: string,
|
||||
@Body('api') api: string
|
||||
) {
|
||||
const thirdParty = this._thirdPartyManager.getThirdPartyByName(identifier);
|
||||
if (!thirdParty) {
|
||||
throw new HttpException('Invalid identifier', 400);
|
||||
}
|
||||
|
||||
const connect = await thirdParty.instance.checkConnection(api);
|
||||
if (!connect) {
|
||||
throw new HttpException('Invalid API key', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const save = await this._thirdPartyManager.saveIntegration(
|
||||
organization.id,
|
||||
identifier,
|
||||
api,
|
||||
{
|
||||
name: connect.name,
|
||||
username: connect.username,
|
||||
id: connect.id,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: save.id,
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new HttpException('Integration Already Exists', 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/t
|
|||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
import { McpModule } from '@gitroom/backend/mcp/mcp.module';
|
||||
import { ThirdPartyModule } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -22,6 +23,7 @@ import { McpModule } from '@gitroom/backend/mcp/mcp.module';
|
|||
PublicApiModule,
|
||||
AgentModule,
|
||||
McpModule,
|
||||
ThirdPartyModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
import { ThirdPartyComponent } from '@gitroom/frontend/components/third-parties/third-party.component';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { Metadata } from 'next';
|
||||
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
|
||||
export const metadata: Metadata = {
|
||||
title: `${
|
||||
isGeneralServerSide() ? 'Postiz Integrations' : 'Gitroom Integrations'
|
||||
}`,
|
||||
description: '',
|
||||
};
|
||||
export default async function Index() {
|
||||
return <ThirdPartyComponent />;
|
||||
}
|
||||
|
|
@ -558,7 +558,6 @@ export const AddEditModal: FC<{
|
|||
|
||||
// @ts-ignore
|
||||
for (const item of clipboardItems) {
|
||||
console.log(item);
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
|
|
@ -779,6 +778,7 @@ Here are the things you can do:
|
|||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<MultiMediaComponent
|
||||
allData={value}
|
||||
text={p.content}
|
||||
label={t('attachments', 'Attachments')}
|
||||
description=""
|
||||
|
|
|
|||
|
|
@ -392,7 +392,6 @@ export const withProvider = function <T extends object>(
|
|||
|
||||
// @ts-ignore
|
||||
for (const item of clipboardItems) {
|
||||
console.log(item);
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
|
|
@ -567,6 +566,7 @@ export const withProvider = function <T extends object>(
|
|||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<MultiMediaComponent
|
||||
allData={InPlaceValue}
|
||||
text={val.content}
|
||||
label="Attachments"
|
||||
description=""
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ export const Subreddit: FC<{
|
|||
<div className="w-full h-[10px] bg-input rounded-tr-[8px] rounded-tl-[8px]" />
|
||||
<div className="flex flex-col text-nowrap">
|
||||
<MultiMediaComponent
|
||||
allData={[]}
|
||||
text=""
|
||||
description=""
|
||||
name="media"
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ export const useMenuItems = () => {
|
|||
icon: 'plugs',
|
||||
path: '/plugs',
|
||||
},
|
||||
{
|
||||
name: t('integrations', 'Integrations'),
|
||||
icon: 'integrations',
|
||||
path: '/third-party',
|
||||
},
|
||||
{
|
||||
name: t('billing', 'Billing'),
|
||||
icon: 'billing',
|
||||
|
|
@ -80,7 +85,7 @@ export const TopMenu: FC = () => {
|
|||
const menuItems = useMenuItems();
|
||||
return (
|
||||
<div className="flex flex-col h-full animate-normalFadeDown order-3 md:order-2 col-span-2 md:col-span-1">
|
||||
<ul className="gap-0 md:gap-5 flex flex-1 items-center text-[18px]">
|
||||
<ul className="gap-0 md:gap-1 flex flex-1 items-center text-[18px]">
|
||||
{menuItems
|
||||
.filter((f) => {
|
||||
if (f.hide) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import Image from 'next/image';
|
|||
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { ThirdPartyMedia } from '@gitroom/frontend/components/third-parties/third-party.media';
|
||||
const Polonto = dynamic(
|
||||
() => import('@gitroom/frontend/components/launches/polonto')
|
||||
);
|
||||
|
|
@ -452,6 +453,14 @@ export const MediaBox: FC<{
|
|||
export const MultiMediaComponent: FC<{
|
||||
label: string;
|
||||
description: string;
|
||||
allData: {
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{
|
||||
id: string;
|
||||
path: string;
|
||||
}>;
|
||||
}[];
|
||||
value?: Array<{
|
||||
path: string;
|
||||
id: string;
|
||||
|
|
@ -471,7 +480,7 @@ export const MultiMediaComponent: FC<{
|
|||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onOpen, onClose, name, error, text, onChange, value } = props;
|
||||
const { onOpen, onClose, name, error, text, onChange, value, allData } = props;
|
||||
const user = useUser();
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
|
|
@ -598,6 +607,8 @@ export const MultiMediaComponent: FC<{
|
|||
</div>
|
||||
</Button>
|
||||
|
||||
<ThirdPartyMedia allData={allData} onChange={changeMedia} />
|
||||
|
||||
{!!user?.tier?.ai && (
|
||||
<AiImage value={text} onChange={changeMedia} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
import { thirdPartyWrapper } from '@gitroom/frontend/components/third-parties/third-party.wrapper';
|
||||
import {
|
||||
useThirdPartyFunction,
|
||||
useThirdPartyFunctionSWR,
|
||||
useThirdPartySubmit,
|
||||
} from '@gitroom/frontend/components/third-parties/third-party.function';
|
||||
import { useThirdParty } from '@gitroom/frontend/components/third-parties/third-party.media';
|
||||
import { useForm, FormProvider, SubmitHandler } from 'react-hook-form';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import clsx from 'clsx';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { object, string } from 'zod';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
|
||||
const aspectRatio = [
|
||||
{ key: 'portrait', value: 'Portrait' },
|
||||
{ key: 'story', value: 'Story' },
|
||||
];
|
||||
|
||||
const generateCaptions = [
|
||||
{ key: 'yes', value: 'Yes' },
|
||||
{ key: 'no', value: 'No' },
|
||||
];
|
||||
|
||||
const SelectAvatarComponent: FC<{
|
||||
avatarList: any[];
|
||||
onChange: (id: string) => void;
|
||||
}> = (props) => {
|
||||
const [current, setCurrent] = useState<any>({});
|
||||
const { avatarList, onChange } = props;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-[10px] justify-items-center justify-center">
|
||||
{avatarList?.map((p) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setCurrent(p.avatar_id === current?.avatar_id ? undefined : p);
|
||||
onChange(p.avatar_id === current?.avatar_id ? {} : p.avatar_id);
|
||||
}}
|
||||
key={p.avatar_id}
|
||||
className={clsx(
|
||||
'w-full h-full p-[20px] min-h-[100px] text-[14px] hover:bg-input transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer',
|
||||
current?.avatar_id === p.avatar_id
|
||||
? 'bg-input border border-red-500'
|
||||
: 'bg-third'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src={p.preview_image_url}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>{p.avatar_name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectVoiceComponent: FC<{
|
||||
voiceList: any[];
|
||||
onChange: (id: string) => void;
|
||||
}> = (props) => {
|
||||
const [current, setCurrent] = useState<any>({});
|
||||
const { voiceList, onChange } = props;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-[10px] justify-items-center justify-center">
|
||||
{voiceList?.map((p) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setCurrent(p.voice_id === current?.voice_id ? undefined : p);
|
||||
onChange(p.voice_id === current?.voice_id ? {} : p.voice_id);
|
||||
}}
|
||||
key={p.avatar_id}
|
||||
className={clsx(
|
||||
'w-full h-full p-[20px] min-h-[100px] text-[14px] hover:bg-input transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer',
|
||||
current?.voice_id === p.voice_id
|
||||
? 'bg-input border border-red-500'
|
||||
: 'bg-third'
|
||||
)}
|
||||
>
|
||||
<div className="text-[14px] text-balance whitespace-pre-line">{p.name}</div>
|
||||
<div className="text-[12px]">{p.language}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HeygenProviderComponent = () => {
|
||||
const thirdParty = useThirdParty();
|
||||
const load = useThirdPartyFunction('EVERYTIME');
|
||||
const { data } = useThirdPartyFunctionSWR('LOAD_ONCE', 'avatars');
|
||||
const { data: voices } = useThirdPartyFunctionSWR('LOAD_ONCE', 'voices');
|
||||
const send = useThirdPartySubmit();
|
||||
const [hideVoiceGenerator, setHideVoiceGenerator] = useState(false);
|
||||
const [voiceLoading, setVoiceLoading] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
values: {
|
||||
voice: '',
|
||||
avatar: '',
|
||||
aspect_ratio: '',
|
||||
captions: '',
|
||||
selectedVoice: '',
|
||||
},
|
||||
mode: 'all',
|
||||
resolver: zodResolver(
|
||||
object({
|
||||
voice: string().min(20, 'Voice must be at least 20 characters long'),
|
||||
avatar: string().min(1, 'Avatar is required'),
|
||||
selectedVoice: string().min(1, 'Voice is required'),
|
||||
aspect_ratio: string().min(1, 'Aspect ratio is required'),
|
||||
captions: string().min(1, 'Captions is required'),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const generateVoice = useCallback(async () => {
|
||||
if (
|
||||
!(await deleteDialog('Are you sure? it will delete the current text'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVoiceLoading(true);
|
||||
|
||||
form.setValue(
|
||||
'voice',
|
||||
(
|
||||
await load('generateVoice', {
|
||||
text: thirdParty.data.map((p) => p.content).join('\n'),
|
||||
})
|
||||
).voice
|
||||
);
|
||||
|
||||
setVoiceLoading(false);
|
||||
setHideVoiceGenerator(true);
|
||||
}, [thirdParty]);
|
||||
|
||||
const submit: SubmitHandler<{ voice: string; avatar: string }> = useCallback(
|
||||
async (params) => {
|
||||
thirdParty.onChange(await send(params));
|
||||
thirdParty.close();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{form.formState.isSubmitting && (
|
||||
<div className="fixed left-0 top-0 w-full leading-[50px] pt-[200px] h-screen bg-black/90 z-50 flex flex-col justify-center items-center text-center text-3xl">
|
||||
Grab a coffee and relax, this may take a while...
|
||||
<br />
|
||||
You can also track the progress directly in HeyGen Dashboard.
|
||||
<br />
|
||||
DO NOT CLOSE THIS WINDOW!
|
||||
<br />
|
||||
<LoadingComponent width={200} height={200} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(submit)}
|
||||
className="w-full flex flex-col"
|
||||
>
|
||||
<Select label="Aspect Ratio" {...form.register('aspect_ratio')}>
|
||||
<option value="">--SELECT--</option>
|
||||
{aspectRatio.map((p) => (
|
||||
<option key={p.key} value={p.key}>
|
||||
{p.value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select label="Generate Captions" {...form.register('captions')}>
|
||||
<option value="">--SELECT--</option>
|
||||
{generateCaptions.map((p) => (
|
||||
<option key={p.key} value={p.key}>
|
||||
{p.value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<div className="text-lg mb-3">Voice to generate</div>
|
||||
{!hideVoiceGenerator && (
|
||||
<Button onClick={generateVoice} loading={voiceLoading}>
|
||||
Generate Voice From My Post Text
|
||||
</Button>
|
||||
)}
|
||||
<Textarea label="" {...form.register('voice')} />
|
||||
{!!data?.length && (
|
||||
<>
|
||||
<div className="text-lg my-3">Select Avatar</div>
|
||||
<SelectAvatarComponent
|
||||
avatarList={data}
|
||||
onChange={(id: string) => form.setValue('avatar', id)}
|
||||
/>
|
||||
<div className="text-red-400 text-[12px] mb-3">
|
||||
{form?.formState?.errors?.avatar?.message || ''}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!!voices?.length && (
|
||||
<>
|
||||
<div className="text-lg my-3">Select Voice</div>
|
||||
<SelectVoiceComponent
|
||||
voiceList={voices}
|
||||
onChange={(id: string) => form.setValue('selectedVoice', id)}
|
||||
/>
|
||||
<div className="text-red-400 text-[12px] mb-3">
|
||||
{form?.formState?.errors?.selectedVoice?.message || ''}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button type="submit">Generate Video</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default thirdPartyWrapper('heygen', HeygenProviderComponent);
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { ThirdPartyListComponent } from '@gitroom/frontend/components/third-parties/third-party.list.component';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
|
||||
export const ThirdPartyMenuComponent: FC<{
|
||||
reload: () => void;
|
||||
tParty: { id: string };
|
||||
}> = (props) => {
|
||||
const { tParty, reload } = props;
|
||||
const fetch = useFetch();
|
||||
const [show, setShow] = useState(false);
|
||||
const t = useT();
|
||||
const toaster = useToaster();
|
||||
|
||||
const changeShow = () => {
|
||||
setShow((prev) => !prev);
|
||||
};
|
||||
|
||||
const deleteChannel = (id: string) => async () => {
|
||||
setShow(false);
|
||||
if (
|
||||
!(await deleteDialog('Are you sure you want to delete this integration?'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/third-party/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toaster.show('Integration deleted successfully', 'success');
|
||||
reload();
|
||||
} else {
|
||||
const error = await res.json();
|
||||
console.error('Error deleting integration:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cursor-pointer relative select-none" onClick={changeShow}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.125 12C13.125 12.2225 13.059 12.44 12.9354 12.625C12.8118 12.81 12.6361 12.9542 12.4305 13.0394C12.225 13.1245 11.9988 13.1468 11.7805 13.1034C11.5623 13.06 11.3618 12.9528 11.2045 12.7955C11.0472 12.6382 10.94 12.4377 10.8966 12.2195C10.8532 12.0012 10.8755 11.775 10.9606 11.5695C11.0458 11.3639 11.19 11.1882 11.375 11.0646C11.56 10.941 11.7775 10.875 12 10.875C12.2984 10.875 12.5845 10.9935 12.7955 11.2045C13.0065 11.4155 13.125 11.7016 13.125 12ZM12 6.75C12.2225 6.75 12.44 6.68402 12.625 6.5604C12.81 6.43679 12.9542 6.26109 13.0394 6.05552C13.1245 5.84995 13.1468 5.62375 13.1034 5.40552C13.06 5.1873 12.9528 4.98684 12.7955 4.82951C12.6382 4.67217 12.4377 4.56503 12.2195 4.52162C12.0012 4.47821 11.775 4.50049 11.5695 4.58564C11.3639 4.67078 11.1882 4.81498 11.0646 4.99998C10.941 5.18499 10.875 5.4025 10.875 5.625C10.875 5.92337 10.9935 6.20952 11.2045 6.4205C11.4155 6.63147 11.7016 6.75 12 6.75ZM12 17.25C11.7775 17.25 11.56 17.316 11.375 17.4396C11.19 17.5632 11.0458 17.7389 10.9606 17.9445C10.8755 18.15 10.8532 18.3762 10.8966 18.5945C10.94 18.8127 11.0472 19.0132 11.2045 19.1705C11.3618 19.3278 11.5623 19.435 11.7805 19.4784C11.9988 19.5218 12.225 19.4995 12.4305 19.4144C12.6361 19.3292 12.8118 19.185 12.9354 19C13.059 18.815 13.125 18.5975 13.125 18.375C13.125 18.0766 13.0065 17.7905 12.7955 17.5795C12.5845 17.3685 12.2984 17.25 12 17.25Z"
|
||||
fill="#506490"
|
||||
/>
|
||||
</svg>
|
||||
{show && (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`absolute top-[100%] start-0 p-[8px] px-[20px] bg-fifth flex flex-col gap-[16px] z-[100] rounded-[8px] border border-tableBorder ${interClass} text-nowrap`}
|
||||
>
|
||||
<div
|
||||
className="flex gap-[12px] items-center"
|
||||
onClick={deleteChannel(tParty.id)}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M13.5 3H11V2.5C11 2.10218 10.842 1.72064 10.5607 1.43934C10.2794 1.15804 9.89782 1 9.5 1H6.5C6.10218 1 5.72064 1.15804 5.43934 1.43934C5.15804 1.72064 5 2.10218 5 2.5V3H2.5C2.36739 3 2.24021 3.05268 2.14645 3.14645C2.05268 3.24021 2 3.36739 2 3.5C2 3.63261 2.05268 3.75979 2.14645 3.85355C2.24021 3.94732 2.36739 4 2.5 4H3V13C3 13.2652 3.10536 13.5196 3.29289 13.7071C3.48043 13.8946 3.73478 14 4 14H12C12.2652 14 12.5196 13.8946 12.7071 13.7071C12.8946 13.5196 13 13.2652 13 13V4H13.5C13.6326 4 13.7598 3.94732 13.8536 3.85355C13.9473 3.75979 14 3.63261 14 3.5C14 3.36739 13.9473 3.24021 13.8536 3.14645C13.7598 3.05268 13.6326 3 13.5 3ZM6 2.5C6 2.36739 6.05268 2.24021 6.14645 2.14645C6.24021 2.05268 6.36739 2 6.5 2H9.5C9.63261 2 9.75979 2.05268 9.85355 2.14645C9.94732 2.24021 10 2.36739 10 2.5V3H6V2.5ZM12 13H4V4H12V13ZM7 6.5V10.5C7 10.6326 6.94732 10.7598 6.85355 10.8536C6.75979 10.9473 6.63261 11 6.5 11C6.36739 11 6.24021 10.9473 6.14645 10.8536C6.05268 10.7598 6 10.6326 6 10.5V6.5C6 6.36739 6.05268 6.24021 6.14645 6.14645C6.24021 6.05268 6.36739 6 6.5 6C6.63261 6 6.75979 6.05268 6.85355 6.14645C6.94732 6.24021 7 6.36739 7 6.5ZM10 6.5V10.5C10 10.6326 9.94732 10.7598 9.85355 10.8536C9.75979 10.9473 9.63261 11 9.5 11C9.36739 11 9.24021 10.9473 9.14645 10.8536C9.05268 10.7598 9 10.6326 9 10.5V6.5C9 6.36739 9.05268 6.24021 9.14645 6.14645C9.24021 6.05268 9.36739 6 9.5 6C9.63261 6 9.75979 6.05268 9.85355 6.14645C9.94732 6.24021 10 6.36739 10 6.5Z"
|
||||
fill="#F97066"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px]">
|
||||
{t('delete_integration', 'Delete Integration')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThirdPartyComponent = () => {
|
||||
const t = useT();
|
||||
const fetch = useFetch();
|
||||
|
||||
const integrations = useCallback(async () => {
|
||||
return (await fetch('/third-party')).json();
|
||||
}, []);
|
||||
|
||||
const { data, isLoading, mutate } = useSWR('third-party', integrations);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="outline-none w-full h-full grid grid-cols[1fr] md:grid-cols-[220px_minmax(0,1fr)] gap-[30px] scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
|
||||
<div className="bg-third p-[16px] flex flex-col gap-[24px] min-h-[100%]">
|
||||
<h2 className="text-[20px]">{t('integrations')}</h2>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<div className={clsx('gap-[16px] flex flex-col relative')}>
|
||||
{!isLoading && !data?.length ? (
|
||||
<div>No Integrations Yet</div>
|
||||
) : (
|
||||
data?.map((p: any) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={clsx('flex gap-[8px] items-center')}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth'
|
||||
)}
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={p.title}
|
||||
>
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/third-party/${p.identifier}.png`}
|
||||
src={`/icons/third-party/${p.identifier}.png`}
|
||||
className="rounded-full"
|
||||
alt={p.title}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
// @ts-ignore
|
||||
role="Handle"
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden'
|
||||
)}
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={p.title}
|
||||
>
|
||||
{p.name}
|
||||
</div>
|
||||
<ThirdPartyMenuComponent reload={mutate} tParty={p} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ThirdPartyListComponent reload={mutate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { useThirdParty } from '@gitroom/frontend/components/third-parties/third-party.media';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export const useThirdPartySubmit = () => {
|
||||
const thirdParty = useThirdParty();
|
||||
const fetch = useFetch();
|
||||
|
||||
return useCallback(async (data?: any) => {
|
||||
if (!thirdParty.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/third-party/${thirdParty.id}/submit`, {
|
||||
body: JSON.stringify(data),
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useThirdPartyFunction = (type: 'EVERYTIME' | 'ONCE') => {
|
||||
const thirdParty = useThirdParty();
|
||||
const data = useRef<any>(undefined);
|
||||
const fetch = useFetch();
|
||||
|
||||
return useCallback(
|
||||
async (functionName: string, sendData?: any) => {
|
||||
if (data.current && type === 'ONCE') {
|
||||
return data.current;
|
||||
}
|
||||
|
||||
data.current = await (
|
||||
await fetch(`/third-party/function/${thirdParty.id}/${functionName}`, {
|
||||
...(data ? { body: JSON.stringify(sendData) } : {}),
|
||||
method: 'POST',
|
||||
})
|
||||
).json();
|
||||
|
||||
return data.current;
|
||||
},
|
||||
[thirdParty, data]
|
||||
);
|
||||
};
|
||||
|
||||
export const useThirdPartyFunctionSWR = (
|
||||
type: 'SWR' | 'LOAD_ONCE',
|
||||
functionName: string,
|
||||
data?: any
|
||||
) => {
|
||||
const thirdParty = useThirdParty();
|
||||
const fetch = useFetch();
|
||||
|
||||
const callBack = useCallback(
|
||||
async (functionName: string, data?: any) => {
|
||||
return (
|
||||
await fetch(`/third-party/function/${thirdParty.id}/${functionName}`, {
|
||||
...(data ? { body: JSON.stringify(data) } : {}),
|
||||
method: 'POST',
|
||||
})
|
||||
).json();
|
||||
},
|
||||
[thirdParty]
|
||||
);
|
||||
|
||||
return useSWR<any>(
|
||||
`function-${thirdParty.id}-${functionName}`,
|
||||
() => {
|
||||
// @ts-ignore
|
||||
return callBack(functionName, { ...data });
|
||||
},
|
||||
{
|
||||
...(type === 'LOAD_ONCE'
|
||||
? {
|
||||
revalidateOnMount: true,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshInterval: 0,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
revalidateIfStale: false,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
'use client';
|
||||
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { string } from 'yup';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export const ApiModal: FC<{
|
||||
identifier: string;
|
||||
title: string;
|
||||
update: () => void;
|
||||
}> = (props) => {
|
||||
const { title, identifier, update } = props;
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const modal = useModals();
|
||||
const toaster = useToaster();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const closePopup = useCallback(() => {
|
||||
modal.closeAll();
|
||||
}, []);
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (closePopup) {
|
||||
return closePopup();
|
||||
}
|
||||
modal.closeAll();
|
||||
}, []);
|
||||
|
||||
const submit = useCallback(async (data: FieldValues) => {
|
||||
setLoading(true);
|
||||
const add = await fetch(`/third-party/${identifier}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
api: data.api,
|
||||
}),
|
||||
});
|
||||
|
||||
if (add.ok) {
|
||||
toaster.show('Integration added successfully', 'success');
|
||||
if (closePopup) {
|
||||
closePopup();
|
||||
} else {
|
||||
modal.closeAll();
|
||||
}
|
||||
router.refresh();
|
||||
if (update) update();
|
||||
return;
|
||||
}
|
||||
|
||||
const {message} = await add.json();
|
||||
|
||||
methods.setError('api', {
|
||||
message,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
}, [props]);
|
||||
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative">
|
||||
<TopTitle title={`Add API key for ${title}`} />
|
||||
<button
|
||||
onClick={close}
|
||||
className="outline-none absolute end-[20px] top-[20px] 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>
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className="gap-[8px] flex flex-col"
|
||||
onSubmit={methods.handleSubmit(submit)}
|
||||
>
|
||||
<div className="pt-[10px]">
|
||||
<Input label="API Key" name="api" />
|
||||
</div>
|
||||
<div>
|
||||
<Button loading={loading} type="submit">{t('add_integration', 'Add Integration')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThirdPartyListComponent: FC<{reload: () => void}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const modals = useModals();
|
||||
const { reload } = props;
|
||||
|
||||
const integrationsList = useCallback(async () => {
|
||||
return (await fetch('/third-party/list')).json();
|
||||
}, []);
|
||||
|
||||
const { data } = useSWR('third-party-list', integrationsList);
|
||||
|
||||
const addApiKey = useCallback((title: string, identifier: string) => () => {
|
||||
modals.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
children: (
|
||||
<ApiModal identifier={identifier} title={title} update={reload} />
|
||||
),
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-[10px] justify-items-center justify-center">
|
||||
{data?.map((p: any) => (
|
||||
<div
|
||||
onClick={addApiKey(p.title, p.identifier)}
|
||||
key={p.identifier}
|
||||
className="w-full h-full p-[20px] min-h-[100px] text-[14px] bg-third hover:bg-input transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
className="w-[32px] h-[32px]"
|
||||
src={`/icons/third-party/${p.identifier}.png`}
|
||||
/>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-left text-lg">
|
||||
{p.title}
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-left">{p.description}</div>
|
||||
<div className="w-full flex">
|
||||
<Button className="w-full">Add</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import clsx from 'clsx';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import './providers/heygen.provider';
|
||||
import { thirdPartyList } from '@gitroom/frontend/components/third-parties/third-party.wrapper';
|
||||
|
||||
const ThirdPartyContext = createContext({
|
||||
id: '',
|
||||
name: '',
|
||||
title: '',
|
||||
identifier: '',
|
||||
description: '',
|
||||
close: () => {},
|
||||
onChange: (data: any) => {},
|
||||
fields: [],
|
||||
data: [
|
||||
{
|
||||
content: '',
|
||||
id: '',
|
||||
image: [
|
||||
{
|
||||
id: '',
|
||||
path: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
export const useThirdParty = () => React.useContext(ThirdPartyContext);
|
||||
const EmptyComponent: FC = () => null;
|
||||
|
||||
export const ThirdPartyPopup: FC<{
|
||||
closeModal: () => void;
|
||||
thirdParties: any[];
|
||||
onChange: (data: any) => void;
|
||||
allData: {
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{
|
||||
id: string;
|
||||
path: string;
|
||||
}>;
|
||||
}[];
|
||||
}> = (props) => {
|
||||
const { closeModal, thirdParties, allData, onChange } = props;
|
||||
const [thirdParty, setThirdParty] = useState<any>(null);
|
||||
|
||||
const Component = useMemo(() => {
|
||||
if (!thirdParty) {
|
||||
return EmptyComponent;
|
||||
}
|
||||
|
||||
return (
|
||||
thirdPartyList.find((p) => p.identifier === thirdParty.identifier)
|
||||
?.Component || EmptyComponent
|
||||
);
|
||||
}, [thirdParty]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setThirdParty(null);
|
||||
closeModal();
|
||||
}, [setThirdParty, closeModal]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="removeEditor fixed start-0 top-0 bg-primary/80 z-[300] w-full min-h-full animate-fade bg-black/30"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<div
|
||||
className="max-w-[1000px] w-full h-full bg-sixth border-tableBorder border-2 rounded-xl relative mx-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="pb-[20px] px-[20px] w-full h-full">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex-1">
|
||||
<TopTitle title="Integrations" />
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="outline-none z-[300] absolute end-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary 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>
|
||||
<div className={clsx('flex flex-wrap flex-col gap-[10px] pt-[20px]')}>
|
||||
{!thirdParty && (
|
||||
<div className="grid grid-cols-4 gap-[10px] justify-items-center justify-center">
|
||||
{thirdParties.map((p: any) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setThirdParty(p);
|
||||
}}
|
||||
key={p.identifier}
|
||||
className="w-full h-full p-[20px] min-h-[100px] text-[14px] bg-third hover:bg-input transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
className="w-[32px] h-[32px]"
|
||||
src={`/icons/third-party/${p.identifier}.png`}
|
||||
/>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-left text-lg">
|
||||
{p.title}: {p.name}
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-left">
|
||||
{p.description}
|
||||
</div>
|
||||
<div className="w-full flex">
|
||||
<Button className="w-full">Use</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{thirdParty && (
|
||||
<>
|
||||
<div>
|
||||
<div
|
||||
className="cursor-pointer float-left"
|
||||
onClick={() => setThirdParty(null)}
|
||||
>
|
||||
{'<'} Back
|
||||
</div>
|
||||
</div>
|
||||
<ThirdPartyContext.Provider
|
||||
value={{ ...thirdParty, data: allData, close, onChange }}
|
||||
>
|
||||
<Component />
|
||||
</ThirdPartyContext.Provider>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThirdPartyMedia: FC<{
|
||||
onChange: (data: any) => void;
|
||||
allData: {
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{
|
||||
id: string;
|
||||
path: string;
|
||||
}>;
|
||||
}[];
|
||||
}> = (props) => {
|
||||
const { allData, onChange } = props;
|
||||
const t = useT();
|
||||
const fetch = useFetch();
|
||||
const [popup, setPopup] = useState(false);
|
||||
|
||||
const thirdParties = useCallback(async () => {
|
||||
return (await (await fetch('/third-party')).json()).filter(
|
||||
(f: any) => f.position === 'media'
|
||||
);
|
||||
}, []);
|
||||
|
||||
const { data, isLoading, mutate } = useSWR('third-party', thirdParties);
|
||||
|
||||
if (isLoading || !data.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup && (
|
||||
<ThirdPartyPopup
|
||||
thirdParties={data}
|
||||
closeModal={() => setPopup(false)}
|
||||
allData={allData}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
className={clsx(
|
||||
'relative ms-[10px] !px-[10px] rounded-[4px] mb-[10px] gap-[8px] !text-primary justify-center items-center flex border border-dashed border-customColor21 bg-input'
|
||||
)}
|
||||
onClick={() => setPopup(true)}
|
||||
>
|
||||
<div className={clsx('flex gap-[5px] items-center')}>
|
||||
<div>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M29.7081 8.29257C29.6152 8.19959 29.5049 8.12583 29.3835 8.07551C29.2621 8.02518 29.132 7.99928 29.0006 7.99928C28.8691 7.99928 28.739 8.02518 28.6176 8.07551C28.4962 8.12583 28.3859 8.19959 28.2931 8.29257L24.0006 12.5863L19.4143 8.00007L23.7081 3.70757C23.8957 3.51993 24.0011 3.26543 24.0011 3.00007C24.0011 2.7347 23.8957 2.48021 23.7081 2.29257C23.5204 2.10493 23.2659 1.99951 23.0006 1.99951C22.7352 1.99951 22.4807 2.10493 22.2931 2.29257L18.0006 6.58632L14.7081 3.29257C14.5204 3.10493 14.2659 2.99951 14.0006 2.99951C13.7352 2.99951 13.4807 3.10493 13.2931 3.29257C13.1054 3.48021 13 3.7347 13 4.00007C13 4.26543 13.1054 4.51993 13.2931 4.70757L14.0868 5.50007L7.46181 12.1251C6.99749 12.5894 6.62917 13.1406 6.37788 13.7472C6.12659 14.3539 5.99725 15.0041 5.99725 15.6607C5.99725 16.3173 6.12659 16.9675 6.37788 17.5742C6.62917 18.1808 6.99749 18.732 7.46181 19.1963L9.42556 21.1601L3.29306 27.2926C3.20015 27.3855 3.12645 27.4958 3.07616 27.6172C3.02588 27.7386 3 27.8687 3 28.0001C3 28.1315 3.02588 28.2616 3.07616 28.383C3.12645 28.5044 3.20015 28.6147 3.29306 28.7076C3.4807 28.8952 3.73519 29.0006 4.00056 29.0006C4.13195 29.0006 4.26206 28.9747 4.38345 28.9245C4.50485 28.8742 4.61515 28.8005 4.70806 28.7076L10.8443 22.5713L12.8081 24.5351C13.2724 24.9994 13.8236 25.3677 14.4302 25.619C15.0368 25.8703 15.687 25.9996 16.3437 25.9996C17.0003 25.9996 17.6505 25.8703 18.2572 25.619C18.8638 25.3677 19.415 24.9994 19.8793 24.5351L26.5043 17.9101L27.2968 18.7038C27.3897 18.7967 27.5 18.8704 27.6214 18.9207C27.7428 18.971 27.8729 18.9969 28.0043 18.9969C28.1357 18.9969 28.2658 18.971 28.3872 18.9207C28.5086 18.8704 28.6189 18.7967 28.7118 18.7038C28.8047 18.6109 28.8784 18.5006 28.9287 18.3792C28.979 18.2578 29.0049 18.1277 29.0049 17.9963C29.0049 17.8649 28.979 17.7348 28.9287 17.6134C28.8784 17.492 28.8047 17.3817 28.7118 17.2888L25.4143 14.0001L29.7081 9.70757C29.801 9.6147 29.8748 9.50441 29.9251 9.38301C29.9754 9.26161 30.0013 9.13148 30.0013 9.00007C30.0013 8.86865 29.9754 8.73853 29.9251 8.61713C29.8748 8.49573 29.801 8.38544 29.7081 8.29257ZM18.4656 23.1251C18.187 23.4038 17.8562 23.6249 17.4921 23.7758C17.128 23.9267 16.7378 24.0043 16.3437 24.0043C15.9496 24.0043 15.5593 23.9267 15.1953 23.7758C14.8312 23.6249 14.5004 23.4038 14.2218 23.1251L8.87556 17.7788C8.59681 17.5002 8.3757 17.1694 8.22483 16.8054C8.07397 16.4413 7.99632 16.051 7.99632 15.6569C7.99632 15.2628 8.07397 14.8726 8.22483 14.5085C8.3757 14.1445 8.59681 13.8137 8.87556 13.5351L15.5006 6.91007L25.0868 16.5001L18.4656 23.1251Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] !text-current">
|
||||
{t('integrations', 'Integrations')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
export const thirdPartyList: {identifier: string, Component: FC}[] = [];
|
||||
|
||||
export const thirdPartyWrapper = (identifier: string, Component: any): null => {
|
||||
if (thirdPartyList.map(p => p.identifier).includes(identifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
thirdPartyList.push({
|
||||
identifier,
|
||||
Component
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
import {
|
||||
ThirdParty,
|
||||
ThirdPartyAbstract,
|
||||
} from '@gitroom/nestjs-libraries/3rdparties/thirdparty.interface';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
@ThirdParty({
|
||||
identifier: 'heygen',
|
||||
title: 'HeyGen',
|
||||
description: 'HeyGen is a platform for creating AI-generated avatars videos.',
|
||||
position: 'media',
|
||||
fields: [],
|
||||
})
|
||||
export class HeygenProvider extends ThirdPartyAbstract<{
|
||||
voice: string;
|
||||
avatar: string;
|
||||
aspect_ratio: string;
|
||||
captions: string;
|
||||
}> {
|
||||
// @ts-ignore
|
||||
constructor(private _openaiService: OpenaiService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async checkConnection(
|
||||
apiKey: string
|
||||
): Promise<false | { name: string; username: string; id: string }> {
|
||||
const list = await fetch('https://api.heygen.com/v1/user/me', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (!list.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { data } = await list.json();
|
||||
|
||||
return {
|
||||
name: data.first_name + ' ' + data.last_name,
|
||||
username: data.username,
|
||||
id: data.username,
|
||||
};
|
||||
}
|
||||
|
||||
async generateVoice(apiKey: string, data: { text: string }) {
|
||||
return {
|
||||
voice: await this._openaiService.generateVoiceFromText(data.text),
|
||||
};
|
||||
}
|
||||
|
||||
async voices(apiKey: string) {
|
||||
const {
|
||||
data: { voices },
|
||||
} = await (
|
||||
await fetch('https://api.heygen.com/v2/voices', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return voices.slice(0, 20);
|
||||
}
|
||||
|
||||
async avatars(apiKey: string) {
|
||||
const {
|
||||
data: { avatar_group_list },
|
||||
} = await (
|
||||
await fetch(
|
||||
'https://api.heygen.com/v2/avatar_group.list?include_public=false',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const loadedAvatars = [];
|
||||
for (const avatar of avatar_group_list) {
|
||||
const {
|
||||
data: { avatar_list },
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://api.heygen.com/v2/avatar_group/${avatar.id}/avatars`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
loadedAvatars.push(...avatar_list);
|
||||
}
|
||||
|
||||
return loadedAvatars;
|
||||
}
|
||||
|
||||
async sendData(
|
||||
apiKey: string,
|
||||
data: {
|
||||
voice: string;
|
||||
avatar: string;
|
||||
aspect_ratio: string;
|
||||
captions: string;
|
||||
selectedVoice: string;
|
||||
}
|
||||
): Promise<string> {
|
||||
const {data: {video_id}} = await (
|
||||
await fetch(`https://api.heygen.com/v2/video/generate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
caption: data.captions === 'yes',
|
||||
video_inputs: [
|
||||
{
|
||||
character: {
|
||||
type: 'avatar',
|
||||
avatar_id: data.avatar,
|
||||
},
|
||||
voice: {
|
||||
type: 'text',
|
||||
input_text: data.voice,
|
||||
voice_id: data.selectedVoice,
|
||||
},
|
||||
},
|
||||
],
|
||||
dimension:
|
||||
data.aspect_ratio === 'story'
|
||||
? {
|
||||
width: 720,
|
||||
height: 1280,
|
||||
}
|
||||
: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
while (true) {
|
||||
const {data: {status, video_url}} = await (await fetch(`https://api.heygen.com/v1/video_status.get?video_id=${video_id}`, {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
})).json();
|
||||
|
||||
if (status === 'completed') {
|
||||
return video_url;
|
||||
} else if (status === 'failed') {
|
||||
throw new Error('Video generation failed');
|
||||
}
|
||||
|
||||
await timer(3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
export abstract class ThirdPartyAbstract<T = any> {
|
||||
abstract checkConnection(
|
||||
apiKey: string
|
||||
): Promise<false | { name: string; username: string; id: string }>;
|
||||
abstract sendData(apiKey: string, data: T): Promise<string>;
|
||||
[key: string]: ((apiKey: string, data?: any) => Promise<any>) | undefined;
|
||||
}
|
||||
|
||||
export interface ThirdPartyParams {
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string;
|
||||
position: 'media' | 'webhook';
|
||||
fields: {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
validation?: RegExp;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function ThirdParty(params: ThirdPartyParams) {
|
||||
return function (target: any) {
|
||||
// Apply @Injectable decorator to the target class
|
||||
Injectable()(target);
|
||||
|
||||
// Retrieve existing metadata or initialize an empty array
|
||||
const existingMetadata =
|
||||
Reflect.getMetadata('third:party', ThirdPartyAbstract) || [];
|
||||
|
||||
// Add the metadata information for this method
|
||||
existingMetadata.push({ target, ...params });
|
||||
|
||||
// Define metadata on the class prototype (so it can be retrieved from the class)
|
||||
Reflect.defineMetadata('third:party', existingMetadata, ThirdPartyAbstract);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ThirdPartyAbstract,
|
||||
ThirdPartyParams,
|
||||
} from '@gitroom/nestjs-libraries/3rdparties/thirdparty.interface';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { ThirdPartyService } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.service';
|
||||
|
||||
@Injectable()
|
||||
export class ThirdPartyManager {
|
||||
constructor(
|
||||
private _moduleRef: ModuleRef,
|
||||
private _thirdPartyService: ThirdPartyService
|
||||
) {}
|
||||
|
||||
getAllThirdParties(): any[] {
|
||||
return (Reflect.getMetadata('third:party', ThirdPartyAbstract) || []).map(
|
||||
(p: any) => ({
|
||||
identifier: p.identifier,
|
||||
title: p.title,
|
||||
description: p.description,
|
||||
fields: p.fields || [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getThirdPartyByName(
|
||||
identifier: string
|
||||
): (ThirdPartyParams & { instance: ThirdPartyAbstract }) | undefined {
|
||||
const thirdParty = (
|
||||
Reflect.getMetadata('third:party', ThirdPartyAbstract) || []
|
||||
).find((p: any) => p.identifier === identifier);
|
||||
|
||||
return { ...thirdParty, instance: this._moduleRef.get(thirdParty.target) };
|
||||
}
|
||||
|
||||
deleteIntegration(org: string, id: string) {
|
||||
return this._thirdPartyService.deleteIntegration(org, id);
|
||||
}
|
||||
|
||||
getIntegrationById(org: string, id: string) {
|
||||
return this._thirdPartyService.getIntegrationById(org, id);
|
||||
}
|
||||
|
||||
getAllThirdPartiesByOrganization(org: string) {
|
||||
return this._thirdPartyService.getAllThirdPartiesByOrganization(org);
|
||||
}
|
||||
|
||||
saveIntegration(
|
||||
org: string,
|
||||
identifier: string,
|
||||
apiKey: string,
|
||||
data: { name: string; username: string; id: string }
|
||||
) {
|
||||
return this._thirdPartyService.saveIntegration(
|
||||
org,
|
||||
identifier,
|
||||
apiKey,
|
||||
data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { HeygenProvider } from '@gitroom/nestjs-libraries/3rdparties/heygen/heygen.provider';
|
||||
import { ThirdPartyManager } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.manager';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [HeygenProvider, ThirdPartyManager],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
},
|
||||
})
|
||||
export class ThirdPartyModule {}
|
||||
|
|
@ -37,6 +37,8 @@ import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/au
|
|||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
import { SetsService } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.service';
|
||||
import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.repository';
|
||||
import { ThirdPartyRepository } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.repository';
|
||||
import { ThirdPartyService } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -82,6 +84,8 @@ import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/s
|
|||
ShortLinkService,
|
||||
SetsService,
|
||||
SetsRepository,
|
||||
ThirdPartyRepository,
|
||||
ThirdPartyService,
|
||||
],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ model Organization {
|
|||
signatures Signatures[]
|
||||
autoPost AutoPost[]
|
||||
sets Sets[]
|
||||
thirdParty ThirdParty[]
|
||||
}
|
||||
|
||||
model Tags {
|
||||
|
|
@ -612,6 +613,23 @@ model Sets {
|
|||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model ThirdParty {
|
||||
id String @id @default(uuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
identifier String
|
||||
name String
|
||||
internalId String
|
||||
apiKey String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@index([organizationId])
|
||||
@@index([deletedAt])
|
||||
@@unique([organizationId, internalId])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
|
|
|
|||
64
libraries/nestjs-libraries/src/database/prisma/third-party/third-party.repository.ts
vendored
Normal file
64
libraries/nestjs-libraries/src/database/prisma/third-party/third-party.repository.ts
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class ThirdPartyRepository {
|
||||
constructor(private _thirdParty: PrismaRepository<'thirdParty'>) {}
|
||||
|
||||
getAllThirdPartiesByOrganization(org: string) {
|
||||
return this._thirdParty.model.thirdParty.findMany({
|
||||
where: { organizationId: org, deletedAt: null },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
identifier: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteIntegration(org: string, id: string) {
|
||||
return this._thirdParty.model.thirdParty.update({
|
||||
where: { id, organizationId: org },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
getIntegrationById(org: string, id: string) {
|
||||
return this._thirdParty.model.thirdParty.findFirst({
|
||||
where: { id, organizationId: org, deletedAt: null },
|
||||
});
|
||||
}
|
||||
|
||||
saveIntegration(
|
||||
org: string,
|
||||
identifier: string,
|
||||
apiKey: string,
|
||||
data: { name: string; username: string; id: string }
|
||||
) {
|
||||
return this._thirdParty.model.thirdParty.upsert({
|
||||
where: {
|
||||
organizationId_internalId: {
|
||||
internalId: data.id,
|
||||
organizationId: org,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
organizationId: org,
|
||||
name: data.name,
|
||||
internalId: data.id,
|
||||
identifier,
|
||||
apiKey: AuthService.fixedEncryption(apiKey),
|
||||
deletedAt: null,
|
||||
},
|
||||
update: {
|
||||
organizationId: org,
|
||||
name: data.name,
|
||||
internalId: data.id,
|
||||
identifier,
|
||||
apiKey: AuthService.fixedEncryption(apiKey),
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
28
libraries/nestjs-libraries/src/database/prisma/third-party/third-party.service.ts
vendored
Normal file
28
libraries/nestjs-libraries/src/database/prisma/third-party/third-party.service.ts
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ThirdPartyRepository } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.repository';
|
||||
|
||||
@Injectable()
|
||||
export class ThirdPartyService {
|
||||
constructor(private _thirdPartyRepository: ThirdPartyRepository) {}
|
||||
|
||||
getAllThirdPartiesByOrganization(org: string) {
|
||||
return this._thirdPartyRepository.getAllThirdPartiesByOrganization(org);
|
||||
}
|
||||
|
||||
deleteIntegration(org: string, id: string) {
|
||||
return this._thirdPartyRepository.deleteIntegration(org, id);
|
||||
}
|
||||
|
||||
getIntegrationById(org: string, id: string) {
|
||||
return this._thirdPartyRepository.getIntegrationById(org, id);
|
||||
}
|
||||
|
||||
saveIntegration(
|
||||
org: string,
|
||||
identifier: string,
|
||||
apiKey: string,
|
||||
data: { name: string; username: string; id: string }
|
||||
) {
|
||||
return this._thirdPartyRepository.saveIntegration(org, identifier, apiKey, data);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ export class FarcasterProvider
|
|||
name = 'Warpcast';
|
||||
isBetweenSteps = false;
|
||||
isWeb3 = true;
|
||||
scopes = [];
|
||||
scopes = [] as string[];
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ const PicturePrompt = z.object({
|
|||
prompt: z.string(),
|
||||
});
|
||||
|
||||
const VoicePrompt = z.object({
|
||||
voice: z.string(),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class OpenaiService {
|
||||
async generateImage(prompt: string, isUrl: boolean) {
|
||||
|
|
@ -47,6 +51,27 @@ export class OpenaiService {
|
|||
);
|
||||
}
|
||||
|
||||
async generateVoiceFromText(prompt: string) {
|
||||
return (
|
||||
(
|
||||
await openai.beta.chat.completions.parse({
|
||||
model: 'gpt-4.1',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an assistant that takes a social media post and convert it to a normal human voice, to be later added to a character, when a person talk they don\'t use "-", and sometimes they add pause with "..." to make it sounds more natural, make sure you use a lot of pauses and make it sound like a real person`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `prompt: ${prompt}`,
|
||||
},
|
||||
],
|
||||
response_format: zodResponseFormat(VoicePrompt, 'voice'),
|
||||
})
|
||||
).choices[0].message.parsed?.voice || ''
|
||||
);
|
||||
}
|
||||
|
||||
async generatePosts(content: string) {
|
||||
const posts = (
|
||||
await Promise.all([
|
||||
|
|
@ -142,7 +167,9 @@ export class OpenaiService {
|
|||
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`,
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -186,6 +186,8 @@
|
|||
"please_add_the_following_command_in_your_chat": "Please add the following command in your chat:",
|
||||
"copy": "Copy",
|
||||
"settings": "Settings",
|
||||
"integrations": "Integrations",
|
||||
"add_integration": "Add Integration",
|
||||
"you_are_now_editing_only": "You are now editing only",
|
||||
"tag_a_company": "Tag a company",
|
||||
"video_length_is_invalid_must_be_up_to": "Video length is invalid, must be up to",
|
||||
|
|
@ -486,5 +488,6 @@
|
|||
"post_as_images_carousel": "Post as images carousel",
|
||||
"save_set": "Save Set",
|
||||
"separate_post": "Separate post to multiple posts",
|
||||
"label_who_can_reply_to_this_post": "Who can reply to this post?"
|
||||
"label_who_can_reply_to_this_post": "Who can reply to this post?",
|
||||
"delete_integration": "Delete Integration"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue