Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/self open ai key #491

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand All @@ -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"
Comment on lines +93 to +94
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review the default value and placement of X self-integration toggle.

A few concerns about the ENABLE_X_SELF configuration:

  1. Default value is set to "true" which might not be safe for production environments
  2. It's placed at the end of the file instead of being grouped with other X-related settings (lines 27-31)

Consider these changes:

-#Enable X Integration with Self Generated Tokens.
-ENABLE_X_SELF="true"

+# X Settings
 X_API_KEY=""
 X_API_SECRET=""
 X_CLIENT=""
 X_SECRET=""
+ENABLE_X_SELF="false" # Enable X Integration with Self Generated Tokens

Committable suggestion skipped: line range outside the PR's diff.


31 changes: 26 additions & 5 deletions apps/backend/src/api/routes/copilot.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +26 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle missing OpenAI API key by returning an appropriate HTTP response

When the OpenAI API key is missing or invalid, the method logs a warning and returns without sending a response to the client. This may cause the client request to hang or receive an unexpected result. It's important to inform the client of the issue.

Apply this diff to return a 400 Bad Request response with an error message:

 if (openAIAPIKey === undefined || openAIAPIKey === '') {
   Logger.warn('OpenAI API key not set, chat functionality will not work');
+  res.status(400).json({ error: 'OpenAI API key is not set or invalid.' });
+  return;
 }

Committable suggestion skipped: line range outside the PR's diff.

}

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 ===
Expand Down
11 changes: 8 additions & 3 deletions apps/backend/src/api/routes/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Down
Binary file added apps/frontend/public/icons/platforms/xself.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 2 additions & 5 deletions apps/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
return (
<html className={interClass}>
<head>
<link
rel="icon"
href="/favicon.ico"
sizes="any"
/>
<link rel="icon" href="/favicon.ico" sizes="any" />
</head>
<body className={clsx(chakra.className, 'text-primary dark')}>
<VariableContextComponent
Expand All @@ -42,6 +38,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
frontEndUrl={process.env.FRONTEND_URL!}
isGeneral={!!process.env.IS_GENERAL}
enabledOpenaiSelf={!!process.env.ENABLE_OPENAI_SELF}
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
tolt={process.env.NEXT_PUBLIC_TOLT!}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<ProviderComponent
key={integration.id}
{...integration}
value={value}
show={selectedProvider?.id === integration.id}
/>
);
})}
</>
);
};

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 <ProviderComponent key={integration.id} {...integration} value={value} show={selectedProvider?.id === integration.id} />;
})}
</>
)
}
Original file line number Diff line number Diff line change
@@ -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<boolean> => {
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.'));
};
});
};
7 changes: 5 additions & 2 deletions apps/frontend/src/components/layout/layout.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
>
<div className="min-w-[55px]">
<Image
src={isGeneral ? '/postiz.svg' : '/logo.svg'}
src={!isGeneral ? '/postiz.svg' : '/logo.svg'}
width={55}
height={53}
alt="Logo"
Expand Down Expand Up @@ -132,7 +132,10 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
) : (
<div />
)}
<div id = "systray-buttons" className="flex items-center justify-self-end gap-[8px] order-2 md:order-3">
<div
id="systray-buttons"
className="flex items-center justify-self-end gap-[8px] order-2 md:order-3"
>
<ModeComponent />
<SettingsComponent />
<NotificationComponent />
Expand Down
21 changes: 17 additions & 4 deletions apps/frontend/src/components/layout/settings.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useSearchParams } from 'next/navigation';
import { useVariables } from '@gitroom/react/helpers/variable.context';

export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
const {isGeneral} = useVariables();
const { isGeneral, enabledOpenaiSelf } = useVariables();
const { getRef } = props;
const fetch = useFetch();
const toast = useToaster();
Expand All @@ -38,11 +38,12 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (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);
}, []);
Expand Down Expand Up @@ -76,6 +77,8 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
loadProfile();
}, []);

console.log('zaaa', enabledOpenaiSelf);

return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(submit)}>
Expand Down Expand Up @@ -155,7 +158,10 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
/>
</svg>
</div>
<div className="text-[12px] text-white" onClick={openMedia}>
<div
className="text-[12px] text-white"
onClick={openMedia}
>
Upload image
</div>
</button>
Expand Down Expand Up @@ -188,10 +194,17 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
<div>
<Textarea label="Bio" name="bio" className="resize-none" />
</div>
{enabledOpenaiSelf && (
<div>
<Input label="OpenAI API Key" name="openAIAPIKey" />
</div>
)}
</div>
{!getRef && (
<div className="justify-end flex">
<Button type="submit" className='rounded-md'>Save</Button>
<Button type="submit" className="rounded-md">
Save
</Button>
</div>
)}
{!!user?.tier?.team_members && isGeneral && <TeamsComponent />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ export class IntegrationService {
timezone?: number,
customInstanceDetails?: string
) {
console.log('XXXX UPLOADING');
const uploadedPicture = picture
? await this.storage.uploadSimple(picture)
: undefined;
console.log('XXXX UPLOAD DONE');
Comment on lines +57 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove debug console.log statements.

Debug console.log statements should not be committed to production code. Consider replacing them with proper logging using a logging service if upload tracking is needed.

-    console.log('XXXX UPLOADING');
     const uploadedPicture = picture
       ? await this.storage.uploadSimple(picture)
       : undefined;
-    console.log('XXXX UPLOAD DONE');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log('XXXX UPLOADING');
const uploadedPicture = picture
? await this.storage.uploadSimple(picture)
: undefined;
console.log('XXXX UPLOAD DONE');
const uploadedPicture = picture
? await this.storage.uploadSimple(picture)
: undefined;

return this._integrationRepository.createOrUpdateIntegration(
org,
name,
Expand Down Expand Up @@ -408,7 +410,7 @@ export class IntegrationService {
}) {
const getPlugById = await this._integrationRepository.getPlug(data.plugId);
if (!getPlugById) {
return ;
return;
}

const integration = this._integrationManager.getSocialIntegration(
Expand All @@ -434,11 +436,11 @@ export class IntegrationService {
);

if (process) {
return ;
return;
}

if (data.totalRuns === data.currentRun) {
return ;
return;
}

this._workerServiceProducer.emit('plugs', {
Expand Down
Loading