diff --git a/.env.example b/.env.example index 7ca10d13..32b1f4c5 100644 --- a/.env.example +++ b/.env.example @@ -74,6 +74,7 @@ MASTODON_CLIENT_ID="" MASTODON_CLIENT_SECRET="" # Misc Settings +ENABLE_OPENAI_SELF="false" OPENAI_API_KEY="" NEXT_PUBLIC_DISCORD_SUPPORT="" NEXT_PUBLIC_POLOTNO="" @@ -88,3 +89,7 @@ STRIPE_SIGNING_KEY_CONNECT="" # Developer Settings NX_ADD_PLUGINS=false IS_GENERAL="true" # required for now + +#Enable X Integration with Self Generated Tokens. +ENABLE_X_SELF="true" + diff --git a/apps/backend/src/api/routes/copilot.controller.ts b/apps/backend/src/api/routes/copilot.controller.ts index 65928b20..e304c4a1 100644 --- a/apps/backend/src/api/routes/copilot.controller.ts +++ b/apps/backend/src/api/routes/copilot.controller.ts @@ -5,23 +5,44 @@ import { copilotRuntimeNestEndpoint, } from '@copilotkit/runtime'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; -import { Organization } from '@prisma/client'; +import { Organization, User } from '@prisma/client'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; +import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; +import OpenAI from 'openai'; @Controller('/copilot') export class CopilotController { - constructor(private _subscriptionService: SubscriptionService) {} + constructor( + private _subscriptionService: SubscriptionService, + private _userService: UsersService + ) {} @Post('/chat') - chat(@Req() req: Request, @Res() res: Response) { - if (process.env.OPENAI_API_KEY === undefined || process.env.OPENAI_API_KEY === '') { + async chat( + @Req() req: Request, + @Res() res: Response, + @GetUserFromRequest() user: User + ) { + let openAIAPIKey = ''; + + //Check if OPEN AI is enabled for Self tokens. + if (process.env.ENABLE_OPENAI_SELF === 'true') { + const userPersonal = await this._userService.getPersonal(user.id); + openAIAPIKey = userPersonal.openAIAPIKey; + } else { + openAIAPIKey = process.env.OPENAI_API_KEY; + } + + if (openAIAPIKey === undefined || openAIAPIKey === '') { Logger.warn('OpenAI API key not set, chat functionality will not work'); - return + return; } const copilotRuntimeHandler = copilotRuntimeNestEndpoint({ endpoint: '/copilot/chat', runtime: new CopilotRuntime(), serviceAdapter: new OpenAIAdapter({ + openai: new OpenAI({ apiKey: openAIAPIKey }), model: // @ts-ignore req?.body?.variables?.data?.metadata?.requestType === diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 85311535..d334b6cd 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -42,7 +42,7 @@ export class UsersController { async getSelf( @GetUserFromRequest() user: User, @GetOrgFromRequest() organization: Organization, - @Req() req: Request, + @Req() req: Request ) { if (!organization) { throw new HttpForbiddenException(); @@ -52,9 +52,14 @@ export class UsersController { ...user, orgId: organization.id, // @ts-ignore - totalChannels: organization?.subscription?.totalChannels || pricing.FREE.channel, + totalChannels: + // @ts-ignore + organization?.subscription?.totalChannels || pricing.FREE.channel, // @ts-ignore - tier: organization?.subscription?.subscriptionTier || (!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE'), + tier: + // @ts-ignore + organization?.subscription?.subscriptionTier || + (!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE'), // @ts-ignore role: organization?.users[0]?.role, // @ts-ignore diff --git a/apps/frontend/public/icons/platforms/xself.png b/apps/frontend/public/icons/platforms/xself.png new file mode 100644 index 00000000..59799be4 Binary files /dev/null and b/apps/frontend/public/icons/platforms/xself.png differ diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index 9a99ad77..2fe81b96 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -25,11 +25,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { return ( - + diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index 3b26f62b..cf0665f2 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -1,11 +1,11 @@ -import {FC} from "react"; -import {Integrations} from "@gitroom/frontend/components/launches/calendar.context"; -import DevtoProvider from "@gitroom/frontend/components/launches/providers/devto/devto.provider"; -import XProvider from "@gitroom/frontend/components/launches/providers/x/x.provider"; -import LinkedinProvider from "@gitroom/frontend/components/launches/providers/linkedin/linkedin.provider"; -import RedditProvider from "@gitroom/frontend/components/launches/providers/reddit/reddit.provider"; -import MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider"; -import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider"; +import { FC } from 'react'; +import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; +import DevtoProvider from '@gitroom/frontend/components/launches/providers/devto/devto.provider'; +import XProvider from '@gitroom/frontend/components/launches/providers/x/x.provider'; +import LinkedinProvider from '@gitroom/frontend/components/launches/providers/linkedin/linkedin.provider'; +import RedditProvider from '@gitroom/frontend/components/launches/providers/reddit/reddit.provider'; +import MediumProvider from '@gitroom/frontend/components/launches/providers/medium/medium.provider'; +import HashnodeProvider from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider'; import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider'; import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.collaborators'; import YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider'; @@ -17,40 +17,58 @@ import DiscordProvider from '@gitroom/frontend/components/launches/providers/dis import SlackProvider from '@gitroom/frontend/components/launches/providers/slack/slack.provider'; import MastodonProvider from '@gitroom/frontend/components/launches/providers/mastodon/mastodon.provider'; import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider'; +import XSelfProvider from '@gitroom/frontend/components/launches/providers/xself/xself.provider'; export const Providers = [ - {identifier: 'devto', component: DevtoProvider}, - {identifier: 'x', component: XProvider}, - {identifier: 'linkedin', component: LinkedinProvider}, - {identifier: 'linkedin-page', component: LinkedinProvider}, - {identifier: 'reddit', component: RedditProvider}, - {identifier: 'medium', component: MediumProvider}, - {identifier: 'hashnode', component: HashnodeProvider}, - {identifier: 'facebook', component: FacebookProvider}, - {identifier: 'instagram', component: InstagramProvider}, - {identifier: 'youtube', component: YoutubeProvider}, - {identifier: 'tiktok', component: TiktokProvider}, - {identifier: 'pinterest', component: PinterestProvider}, - {identifier: 'dribbble', component: DribbbleProvider}, - {identifier: 'threads', component: ThreadsProvider}, - {identifier: 'discord', component: DiscordProvider}, - {identifier: 'slack', component: SlackProvider}, - {identifier: 'mastodon', component: MastodonProvider}, - {identifier: 'bluesky', component: BlueskyProvider}, + { identifier: 'devto', component: DevtoProvider }, + { identifier: 'x', component: XProvider }, + { identifier: 'linkedin', component: LinkedinProvider }, + { identifier: 'linkedin-page', component: LinkedinProvider }, + { identifier: 'reddit', component: RedditProvider }, + { identifier: 'medium', component: MediumProvider }, + { identifier: 'hashnode', component: HashnodeProvider }, + { identifier: 'facebook', component: FacebookProvider }, + { identifier: 'instagram', component: InstagramProvider }, + { identifier: 'youtube', component: YoutubeProvider }, + { identifier: 'tiktok', component: TiktokProvider }, + { identifier: 'pinterest', component: PinterestProvider }, + { identifier: 'dribbble', component: DribbbleProvider }, + { identifier: 'threads', component: ThreadsProvider }, + { identifier: 'discord', component: DiscordProvider }, + { identifier: 'slack', component: SlackProvider }, + { identifier: 'mastodon', component: MastodonProvider }, + { identifier: 'bluesky', component: BlueskyProvider }, + { identifier: 'xself', component: XSelfProvider }, ]; +export const ShowAllProviders: FC<{ + integrations: Integrations[]; + value: Array<{ content: string; id?: string }>; + selectedProvider?: Integrations; +}> = (props) => { + const { integrations, value, selectedProvider } = props; + return ( + <> + {integrations.map((integration) => { + const { component: ProviderComponent } = Providers.find( + (provider) => provider.identifier === integration.identifier + ) || { component: null }; + if ( + !ProviderComponent || + integrations.map((p) => p.id).indexOf(selectedProvider?.id!) === -1 + ) { + return null; + } + return ( + + ); + })} + + ); +}; -export const ShowAllProviders: FC<{integrations: Integrations[], value: Array<{content: string, id?: string}>, selectedProvider?: Integrations}> = (props) => { - const {integrations, value, selectedProvider} = props; - return ( - <> - {integrations.map((integration) => { - const {component: ProviderComponent} = Providers.find(provider => provider.identifier === integration.identifier) || {component: null}; - if (!ProviderComponent || integrations.map(p => p.id).indexOf(selectedProvider?.id!) === -1) { - return null; - } - return ; - })} - - ) -} \ No newline at end of file diff --git a/apps/frontend/src/components/launches/providers/xself/xself.provider.tsx b/apps/frontend/src/components/launches/providers/xself/xself.provider.tsx new file mode 100644 index 00000000..a9ffc5fe --- /dev/null +++ b/apps/frontend/src/components/launches/providers/xself/xself.provider.tsx @@ -0,0 +1,52 @@ +import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +export default withProvider( + null, + undefined, + undefined, + async (posts) => { + if (posts.some((p) => p.length > 4)) { + return 'There can be maximum 4 pictures in a post.'; + } + + if ( + posts.some( + (p) => p.some((m) => m.path.indexOf('mp4') > -1) && p.length > 1 + ) + ) { + return 'There can be maximum 1 video in a post.'; + } + + for (const load of posts.flatMap((p) => p.flatMap((a) => a.path))) { + if (load.indexOf('mp4') > -1) { + const isValid = await checkVideoDuration(load); + if (!isValid) { + return 'Video duration must be less than or equal to 140 seconds.'; + } + } + } + return true; + }, + 280 +); + +const checkVideoDuration = async (url: string): Promise => { + return new Promise((resolve, reject) => { + const video = document.createElement('video'); + video.src = url; + video.preload = 'metadata'; + + video.onloadedmetadata = () => { + // Check if the duration is less than or equal to 140 seconds + const duration = video.duration; + if (duration <= 140) { + resolve(true); // Video duration is acceptable + } else { + resolve(false); // Video duration exceeds 140 seconds + } + }; + + video.onerror = () => { + reject(new Error('Failed to load video metadata.')); + }; + }); +}; diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 38ce4d9d..79a049be 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -87,7 +87,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { >
Logo { ) : (
)} -
+
diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index f4d56eea..a6436911 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -20,7 +20,7 @@ import { useSearchParams } from 'next/navigation'; import { useVariables } from '@gitroom/react/helpers/variable.context'; export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { - const {isGeneral} = useVariables(); + const { isGeneral, enabledOpenaiSelf } = useVariables(); const { getRef } = props; const fetch = useFetch(); const toast = useToaster(); @@ -38,11 +38,12 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { }, []); const url = useSearchParams(); - const showLogout = !url.get('onboarding') || user?.tier?.current === "FREE"; + const showLogout = !url.get('onboarding') || user?.tier?.current === 'FREE'; const loadProfile = useCallback(async () => { const personal = await (await fetch('/user/personal')).json(); form.setValue('fullname', personal.name || ''); + form.setValue('openAIAPIKey', personal.openAIAPIKey || ''); form.setValue('bio', personal.bio || ''); form.setValue('picture', personal.picture); }, []); @@ -76,6 +77,8 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { loadProfile(); }, []); + console.log('zaaa', enabledOpenaiSelf); + return (
@@ -155,7 +158,10 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { />
-
+
Upload image
@@ -188,10 +194,17 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => {