diff --git a/apiserver/plane/app/urls/external.py b/apiserver/plane/app/urls/external.py index 8db87a249..744c646ca 100644 --- a/apiserver/plane/app/urls/external.py +++ b/apiserver/plane/app/urls/external.py @@ -2,7 +2,7 @@ from django.urls import path from plane.app.views import UnsplashEndpoint -from plane.app.views import GPTIntegrationEndpoint +from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpoint urlpatterns = [ @@ -16,4 +16,9 @@ urlpatterns = [ GPTIntegrationEndpoint.as_view(), name="importer", ), + path( + "workspaces//ai-assistant/", + WorkspaceGPTIntegrationEndpoint.as_view(), + name="importer", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 8934e832c..580901879 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -186,6 +186,7 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint from .external.base import ( GPTIntegrationEndpoint, UnsplashEndpoint, + WorkspaceGPTIntegrationEndpoint, ) from .estimate.base import ( ProjectEstimatePointEndpoint, diff --git a/apiserver/plane/app/views/external/base.py b/apiserver/plane/app/views/external/base.py index 2d5d2c7aa..d9a66b850 100644 --- a/apiserver/plane/app/views/external/base.py +++ b/apiserver/plane/app/views/external/base.py @@ -11,7 +11,7 @@ from rest_framework import status # Module imports from ..base import BaseAPIView -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission, WorkspaceEntityPermission from plane.db.models import Workspace, Project from plane.app.serializers import ( ProjectLiteSerializer, @@ -83,6 +83,64 @@ class GPTIntegrationEndpoint(BaseAPIView): ) +class WorkspaceGPTIntegrationEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def post(self, request, slug): + OPENAI_API_KEY, GPT_ENGINE = get_configuration_value( + [ + { + "key": "OPENAI_API_KEY", + "default": os.environ.get("OPENAI_API_KEY", None), + }, + { + "key": "GPT_ENGINE", + "default": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), + }, + ] + ) + + # Get the configuration value + # Check the keys + if not OPENAI_API_KEY or not GPT_ENGINE: + return Response( + {"error": "OpenAI API key and engine is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + prompt = request.data.get("prompt", False) + task = request.data.get("task", False) + + if not task: + return Response( + {"error": "Task is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + final_text = task + "\n" + prompt + + client = OpenAI( + api_key=OPENAI_API_KEY, + ) + + response = client.chat.completions.create( + model=GPT_ENGINE, + messages=[{"role": "user", "content": final_text}], + ) + + text = response.choices[0].message.content.strip() + text_html = text.replace("\n", "
") + return Response( + { + "response": text, + "response_html": text_html, + }, + status=status.HTTP_200_OK, + ) + + class UnsplashEndpoint(BaseAPIView): def get(self, request): (UNSPLASH_ACCESS_KEY,) = get_configuration_value( diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx index c61c291cb..2c8df4618 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx @@ -97,20 +97,17 @@ const PageDetailsPage = observer(() => {
- {projectId && ( - setSidePeekVisible(state)} - /> - )} + setSidePeekVisible(state)} + /> setEditorReady(val)} diff --git a/web/core/components/core/modals/gpt-assistant-popover.tsx b/web/core/components/core/modals/gpt-assistant-popover.tsx index 84b87d952..dcb464550 100644 --- a/web/core/components/core/modals/gpt-assistant-popover.tsx +++ b/web/core/components/core/modals/gpt-assistant-popover.tsx @@ -5,19 +5,17 @@ import { Placement } from "@popperjs/core"; import { useParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; // services import { usePopper } from "react-popper"; -// ui import { AlertCircle } from "lucide-react"; import { Popover, Transition } from "@headlessui/react"; +// ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor"; -// icons // components -// hooks +import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor"; +// services import { AIService } from "@/services/ai.service"; type Props = { isOpen: boolean; - projectId: string; handleClose: () => void; onResponse: (response: any) => void; onError?: (error: any) => void; @@ -35,7 +33,7 @@ type FormData = { const aiService = new AIService(); export const GptAssistantPopover: React.FC = (props) => { - const { isOpen, projectId, handleClose, onResponse, onError, placement, prompt, button, className = "" } = props; + const { isOpen, handleClose, onResponse, onError, placement, prompt, button, className = "" } = props; // states const [response, setResponse] = useState(""); const [invalidResponse, setInvalidResponse] = useState(false); @@ -88,7 +86,7 @@ export const GptAssistantPopover: React.FC = (props) => { const callAIService = async (formData: FormData) => { try { - const res = await aiService.createGptTask(workspaceSlug as string, projectId, { + const res = await aiService.createGptTask(workspaceSlug.toString(), { prompt: prompt || "", task: formData.task, }); @@ -111,7 +109,7 @@ export const GptAssistantPopover: React.FC = (props) => { }; const handleAIResponse = async (formData: FormData) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug) return; if (formData.task === "") { handleInvalidTask(); diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index 1efcc9fb4..c22f0831b 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -239,7 +239,7 @@ export const IssueFormRoot: FC = observer((props) => { setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug.toString(), projectId, { + .createGptTask(workspaceSlug.toString(), { prompt: issueName, task: "Generate a proper description for this issue.", }) @@ -489,7 +489,6 @@ export const IssueFormRoot: FC = observer((props) => { {config?.has_openai_configured && projectId && ( { setGptAssistantModal((prevData) => !prevData); // this is done so that the title do not reset after gpt popover closed @@ -528,7 +527,7 @@ export const IssueFormRoot: FC = observer((props) => { onChange(stateId); handleFormChange(); }} - projectId={projectId?? undefined} + projectId={projectId ?? undefined} buttonVariant="border-with-text" tabIndex={getTabIndex("state_id")} /> @@ -651,7 +650,7 @@ export const IssueFormRoot: FC = observer((props) => { render={({ field: { value, onChange } }) => (
{ onChange(moduleIds); @@ -748,7 +747,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); setSelectedParentIssue(issue); }} - projectId={projectId?? undefined} + projectId={projectId ?? undefined} issueId={isDraft ? undefined : data?.id} /> )} diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index 659aeb4bf..c4908b7a0 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -21,12 +21,11 @@ type Props = { editorRef: React.RefObject; handleDuplicatePage: () => void; page: IPageStore; - projectId: string; readOnlyEditorRef: React.RefObject; }; export const PageExtraOptions: React.FC = observer((props) => { - const { editorRef, handleDuplicatePage, page, projectId, readOnlyEditorRef } = props; + const { editorRef, handleDuplicatePage, page, readOnlyEditorRef } = props; // states const [gptModalOpen, setGptModal] = useState(false); // store hooks @@ -56,7 +55,6 @@ export const PageExtraOptions: React.FC = observer((props) => { {isContentEditable && config?.has_openai_configured && ( { setGptModal((prevData) => !prevData); // this is done so that the title do not reset after gpt popover closed diff --git a/web/core/components/pages/editor/header/mobile-root.tsx b/web/core/components/pages/editor/header/mobile-root.tsx index 44cd9d38b..1bcf5c317 100644 --- a/web/core/components/pages/editor/header/mobile-root.tsx +++ b/web/core/components/pages/editor/header/mobile-root.tsx @@ -13,7 +13,6 @@ type Props = { handleDuplicatePage: () => void; markings: IMarking[]; page: IPageStore; - projectId: string; sidePeekVisible: boolean; setSidePeekVisible: (sidePeekState: boolean) => void; editorReady: boolean; @@ -29,7 +28,6 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { readOnlyEditorReady, handleDuplicatePage, page, - projectId, sidePeekVisible, setSidePeekVisible, } = props; @@ -56,7 +54,6 @@ export const PageEditorMobileHeaderRoot: React.FC = observer((props) => { editorRef={editorRef} handleDuplicatePage={handleDuplicatePage} page={page} - projectId={projectId} readOnlyEditorRef={readOnlyEditorRef} />
diff --git a/web/core/components/pages/editor/header/root.tsx b/web/core/components/pages/editor/header/root.tsx index 7f17c43c3..fdccbddc3 100644 --- a/web/core/components/pages/editor/header/root.tsx +++ b/web/core/components/pages/editor/header/root.tsx @@ -15,7 +15,6 @@ type Props = { handleDuplicatePage: () => void; markings: IMarking[]; page: IPageStore; - projectId: string; sidePeekVisible: boolean; setSidePeekVisible: (sidePeekState: boolean) => void; editorReady: boolean; @@ -31,7 +30,6 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { readOnlyEditorReady, handleDuplicatePage, page, - projectId, sidePeekVisible, setSidePeekVisible, } = props; @@ -66,7 +64,6 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { editorRef={editorRef} handleDuplicatePage={handleDuplicatePage} page={page} - projectId={projectId} readOnlyEditorRef={readOnlyEditorRef} />
@@ -79,7 +76,6 @@ export const PageEditorHeaderRoot: React.FC = observer((props) => { markings={markings} handleDuplicatePage={handleDuplicatePage} page={page} - projectId={projectId} sidePeekVisible={sidePeekVisible} setSidePeekVisible={setSidePeekVisible} /> diff --git a/web/core/services/ai.service.ts b/web/core/services/ai.service.ts index b59465637..7ebe9ccf0 100644 --- a/web/core/services/ai.service.ts +++ b/web/core/services/ai.service.ts @@ -10,8 +10,8 @@ export class AIService extends APIService { super(API_BASE_URL); } - async createGptTask(workspaceSlug: string, projectId: string, data: { prompt: string; task: string }): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) + async createGptTask(workspaceSlug: string, data: { prompt: string; task: string }): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/ai-assistant/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response;