Skip to content
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ EXTENSION_ID=""

# Misc Settings
OPENAI_API_KEY=""
# Set to "1", "true", or "yes" to disable MCP server (no AI/MCP features)
# DISABLE_MCP=""
NEXT_PUBLIC_DISCORD_SUPPORT=""
NEXT_PUBLIC_POLOTNO=""
# NOT_SECURED=false
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service';
import { CopilotController } from '@gitroom/backend/api/routes/copilot.controller';
import { CopilotFeaturesController } from '@gitroom/backend/api/routes/copilot-features.controller';
import { PublicController } from '@gitroom/backend/api/routes/public.controller';
import { RootController } from '@gitroom/backend/api/routes/root.controller';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
Expand Down Expand Up @@ -57,6 +58,7 @@ const authenticatedController = [
StripeController,
AuthController,
PublicController,
CopilotFeaturesController,
MonitorController,
EnterpriseController,
NoAuthIntegrationsController,
Expand Down
23 changes: 23 additions & 0 deletions apps/backend/src/api/routes/copilot-features.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';

/**
* Public controller for GET /copilot/features only.
* Used by the frontend during SSR (layout) without auth, so it must not require authentication.
*/
@Controller('/copilot')
export class CopilotFeaturesController {
@Get('/features')
getFeatures(@Res() res: Response) {
const chatEnabled =
!!process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.trim() !== '';
const mcpDisabled =
process.env.DISABLE_MCP === '1' ||
process.env.DISABLE_MCP === 'true' ||
process.env.DISABLE_MCP === 'yes';
return res.json({
chatEnabled,
mcpEnabled: !mcpDisabled,
});
}
}
51 changes: 39 additions & 12 deletions apps/backend/src/api/routes/copilot.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Logger,
Controller,
Get,
Post,
Expand Down Expand Up @@ -40,10 +39,13 @@ export class CopilotController {
chatAgent(@Req() req: Request, @Res() res: Response) {
if (
process.env.OPENAI_API_KEY === undefined ||
process.env.OPENAI_API_KEY === ''
process.env.OPENAI_API_KEY === '' ||
process.env.OPENAI_API_KEY?.trim() === ''
) {
Logger.warn('OpenAI API key not set, chat functionality will not work');
return;
return res.status(503).json({
error: 'Chat is not configured',
code: 'CHAT_DISABLED',
});
}

const copilotRuntimeHandler = copilotRuntimeNodeHttpEndpoint({
Expand All @@ -66,10 +68,13 @@ export class CopilotController {
) {
if (
process.env.OPENAI_API_KEY === undefined ||
process.env.OPENAI_API_KEY === ''
process.env.OPENAI_API_KEY === '' ||
process.env.OPENAI_API_KEY?.trim() === ''
) {
Logger.warn('OpenAI API key not set, chat functionality will not work');
return;
return res.status(503).json({
error: 'Chat is not configured',
code: 'CHAT_DISABLED',
});
}
const mastra = await this._mastraService.mastra();
const runtimeContext = new RuntimeContext<ChannelsContext>();
Expand Down Expand Up @@ -115,27 +120,49 @@ export class CopilotController {
);
}

private _isChatEnabled(): boolean {
const key = process.env.OPENAI_API_KEY;
return !!key && key.trim() !== '';
}

@Get('/:thread/list')
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
async getMessagesList(
@Res() res: Response,
@GetOrgFromRequest() organization: Organization,
@Param('thread') threadId: string
): Promise<any> {
if (!this._isChatEnabled()) {
return res.status(503).json({
error: 'Chat is not configured',
code: 'CHAT_DISABLED',
});
}
const mastra = await this._mastraService.mastra();
const memory = await mastra.getAgent('postiz').getMemory();
try {
return await memory.query({
const data = await memory.query({
resourceId: organization.id,
threadId,
});
return res.json(data);
} catch (err) {
return { messages: [] };
return res.json({ messages: [] });
}
}

@Get('/list')
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
async getList(@GetOrgFromRequest() organization: Organization) {
async getList(
@Res() res: Response,
@GetOrgFromRequest() organization: Organization
) {
if (!this._isChatEnabled()) {
return res.status(503).json({
error: 'Chat is not configured',
code: 'CHAT_DISABLED',
});
}
const mastra = await this._mastraService.mastra();
// @ts-ignore
const memory = await mastra.getAgent('postiz').getMemory();
Expand All @@ -147,11 +174,11 @@ export class CopilotController {
sortDirection: 'DESC',
});

return {
return res.json({
threads: list.threads.map((p) => ({
id: p.id,
title: p.title,
})),
};
});
}
}
8 changes: 7 additions & 1 deletion apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ async function start() {
},
});

await startMcp(app);
const mcpDisabled =
process.env.DISABLE_MCP === '1' ||
process.env.DISABLE_MCP === 'true' ||
process.env.DISABLE_MCP === 'yes';
if (!mcpDisabled) {
await startMcp(app);
}

app.useGlobalPipes(
new ValidationPipe({
Expand Down
24 changes: 24 additions & 0 deletions apps/frontend/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,30 @@ const jakartaSans = Plus_Jakarta_Sans({
subsets: ['latin'],
});

async function getAiFeatures(): Promise<{
chatEnabled: boolean;
mcpEnabled: boolean;
}> {
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
if (!backendUrl) return { chatEnabled: false, mcpEnabled: false };
try {
const res = await fetch(`${backendUrl}/copilot/features`, {
cache: 'no-store',
});
if (!res.ok) return { chatEnabled: false, mcpEnabled: false };
const data = await res.json();
return {
chatEnabled: !!data.chatEnabled,
mcpEnabled: !!data.mcpEnabled,
};
} catch {
return { chatEnabled: false, mcpEnabled: false };
}
}

export default async function AppLayout({ children }: { children: ReactNode }) {
const allHeaders = headers();
const aiFeatures = await getAiFeatures();
const Plausible = !!process.env.STRIPE_PUBLISHABLE_KEY
? PlausibleProvider
: Fragment;
Expand Down Expand Up @@ -79,6 +101,8 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS}
sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!}
extensionId={process.env.EXTENSION_ID || ''}
chatEnabled={aiFeatures.chatEnabled}
mcpEnabled={aiFeatures.mcpEnabled}
language={allHeaders.get(headerName)}
transloadit={
process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/app/(extension)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS}
sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!}
extensionId={process.env.EXTENSION_ID || ''}
chatEnabled={false}
mcpEnabled={false}
transloadit={
process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE
? [
Expand Down
21 changes: 21 additions & 0 deletions apps/frontend/src/components/agents/agent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Integration } from '@prisma/client';
import Link from 'next/link';
import { useParams, usePathname, useRouter } from 'next/navigation';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useVariables } from '@gitroom/react/helpers/variable.context';

export const MediaPortal: FC<{
media: { path: string; id: string }[];
Expand Down Expand Up @@ -199,6 +200,26 @@ export const AgentList: FC<{ onChange: (arr: any[]) => void }> = ({
export const PropertiesContext = createContext({ properties: [] });
export const Agent: FC<{ children: ReactNode }> = ({ children }) => {
const [properties, setProperties] = useState([]);
const { chatEnabled } = useVariables();
const t = useT();

if (!chatEnabled) {
return (
<div className="bg-newBgColorInner flex flex-1 items-center justify-center p-[24px]">
<div className="text-center text-newTextColor max-w-[400px]">
<p className="font-[600] text-[18px] mb-[8px]">
{t('ai_features_disabled', 'AI features are disabled')}
</p>
<p className="text-[14px] opacity-80">
{t(
'ai_features_disabled_description',
'Chat and AI features are not configured on this installation. Set OPENAI_API_KEY to enable.'
)}
</p>
</div>
</div>
);
}

return (
<PropertiesContext.Provider value={{ properties }}>
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/components/launches/launches.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ export const MenuComponent: FC<
export const LaunchesComponent = () => {
const fetch = useFetch();
const user = useUser();
const { billingEnabled } = useVariables();
const { billingEnabled, chatEnabled } = useVariables();
const router = useRouter();
const search = useSearchParams();
const toast = useToaster();
Expand Down Expand Up @@ -537,7 +537,8 @@ export const LaunchesComponent = () => {
{sortedIntegrations?.length > 0 && <NewPost />}
{sortedIntegrations?.length > 0 &&
user?.tier?.ai &&
billingEnabled && <GeneratorComponent />}
billingEnabled &&
chatEnabled && <GeneratorComponent />}
</div>
</div>
<div className="gap-[32px] flex flex-col select-none flex-1">
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/components/layout/top.menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface MenuItemInterface {
}

export const useMenuItem = () => {
const { isGeneral } = useVariables();
const { isGeneral, chatEnabled } = useVariables();
const t = useT();

const firstMenu = [
Expand Down Expand Up @@ -58,6 +58,7 @@ export const useMenuItem = () => {
</svg>
),
path: '/agents',
hide: !chatEnabled,
},
{
name: t('analytics', 'Analytics'),
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/components/new-launch/manage.modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { useHasScroll } from '@gitroom/frontend/components/ui/is.scroll.hook';
import { useShortlinkPreference } from '@gitroom/frontend/components/settings/shortlink-preference.component';
import dayjs from 'dayjs';
import { Button } from '@gitroom/react/form/button';
import { useVariables } from '@gitroom/react/helpers/variable.context';

function countCharacters(text: string, type: string): number {
if (type !== 'x') {
Expand All @@ -62,6 +63,7 @@ export const ManageModal: FC<AddEditModalProps> = (props) => {
const modal = useModals();
const [showSettings, setShowSettings] = useState(false);
const { data: shortlinkPreferenceData } = useShortlinkPreference();
const { chatEnabled } = useVariables();

const { addEditSets, mutate, customClose, dummy } = props;

Expand Down Expand Up @@ -663,6 +665,7 @@ export const ManageModal: FC<AddEditModalProps> = (props) => {
</div>
</div>
</div>
{chatEnabled && (
<CopilotPopup
hitEscapeToClose={false}
clickOutsideToClose={true}
Expand All @@ -685,6 +688,7 @@ After using the addPostFor{num} it will create a new addPostContentFor{num+ 1} f
),
}}
/>
)}
</div>
);
};
Expand Down
Loading