mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-1559] chore: make AI assistant endpoint workspace level (#4770)
* chore: make ai assistant endpoint workspace level * chore: create workspace level ai assistant endpoint
This commit is contained in:
parent
f4ceaaf01c
commit
aa92ace57f
@ -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/<str:slug>/ai-assistant/",
|
||||
WorkspaceGPTIntegrationEndpoint.as_view(),
|
||||
name="importer",
|
||||
),
|
||||
]
|
||||
|
@ -186,6 +186,7 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||
from .external.base import (
|
||||
GPTIntegrationEndpoint,
|
||||
UnsplashEndpoint,
|
||||
WorkspaceGPTIntegrationEndpoint,
|
||||
)
|
||||
from .estimate.base import (
|
||||
ProjectEstimatePointEndpoint,
|
||||
|
60
apiserver/plane/app/views/external/base.py
vendored
60
apiserver/plane/app/views/external/base.py
vendored
@ -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", "<br/>")
|
||||
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(
|
||||
|
@ -97,20 +97,17 @@ const PageDetailsPage = observer(() => {
|
||||
<PageHead title={name} />
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="h-full w-full flex-shrink-0 flex flex-col overflow-hidden">
|
||||
{projectId && (
|
||||
<PageEditorHeaderRoot
|
||||
editorRef={editorRef}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
editorReady={editorReady}
|
||||
readOnlyEditorReady={readOnlyEditorReady}
|
||||
handleDuplicatePage={handleDuplicatePage}
|
||||
markings={markings}
|
||||
page={page}
|
||||
projectId={projectId.toString()}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
setSidePeekVisible={(state) => setSidePeekVisible(state)}
|
||||
/>
|
||||
)}
|
||||
<PageEditorHeaderRoot
|
||||
editorRef={editorRef}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
editorReady={editorReady}
|
||||
readOnlyEditorReady={readOnlyEditorReady}
|
||||
handleDuplicatePage={handleDuplicatePage}
|
||||
markings={markings}
|
||||
page={page}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
setSidePeekVisible={(state) => setSidePeekVisible(state)}
|
||||
/>
|
||||
<PageEditorBody
|
||||
editorRef={editorRef}
|
||||
handleEditorReady={(val) => setEditorReady(val)}
|
||||
|
@ -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> = (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> = (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> = (props) => {
|
||||
};
|
||||
|
||||
const handleAIResponse = async (formData: FormData) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (formData.task === "") {
|
||||
handleInvalidTask();
|
||||
|
@ -239,7 +239,7 @@ export const IssueFormRoot: FC<IssueFormProps> = 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<IssueFormProps> = observer((props) => {
|
||||
{config?.has_openai_configured && projectId && (
|
||||
<GptAssistantPopover
|
||||
isOpen={gptAssistantModal}
|
||||
projectId={projectId}
|
||||
handleClose={() => {
|
||||
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<IssueFormProps> = 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<IssueFormProps> = observer((props) => {
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<ModuleDropdown
|
||||
projectId={projectId?? undefined}
|
||||
projectId={projectId ?? undefined}
|
||||
value={value ?? []}
|
||||
onChange={(moduleIds) => {
|
||||
onChange(moduleIds);
|
||||
@ -748,7 +747,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId?? undefined}
|
||||
projectId={projectId ?? undefined}
|
||||
issueId={isDraft ? undefined : data?.id}
|
||||
/>
|
||||
)}
|
||||
|
@ -21,12 +21,11 @@ type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
handleDuplicatePage: () => void;
|
||||
page: IPageStore;
|
||||
projectId: string;
|
||||
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
|
||||
};
|
||||
|
||||
export const PageExtraOptions: React.FC<Props> = 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<Props> = observer((props) => {
|
||||
{isContentEditable && config?.has_openai_configured && (
|
||||
<GptAssistantPopover
|
||||
isOpen={gptModalOpen}
|
||||
projectId={projectId}
|
||||
handleClose={() => {
|
||||
setGptModal((prevData) => !prevData);
|
||||
// this is done so that the title do not reset after gpt popover closed
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
readOnlyEditorReady,
|
||||
handleDuplicatePage,
|
||||
page,
|
||||
projectId,
|
||||
sidePeekVisible,
|
||||
setSidePeekVisible,
|
||||
} = props;
|
||||
@ -56,7 +54,6 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
editorRef={editorRef}
|
||||
handleDuplicatePage={handleDuplicatePage}
|
||||
page={page}
|
||||
projectId={projectId}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
readOnlyEditorReady,
|
||||
handleDuplicatePage,
|
||||
page,
|
||||
projectId,
|
||||
sidePeekVisible,
|
||||
setSidePeekVisible,
|
||||
} = props;
|
||||
@ -66,7 +64,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
editorRef={editorRef}
|
||||
handleDuplicatePage={handleDuplicatePage}
|
||||
page={page}
|
||||
projectId={projectId}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
/>
|
||||
</div>
|
||||
@ -79,7 +76,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
markings={markings}
|
||||
handleDuplicatePage={handleDuplicatePage}
|
||||
page={page}
|
||||
projectId={projectId}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
setSidePeekVisible={setSidePeekVisible}
|
||||
/>
|
||||
|
@ -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<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
|
||||
async createGptTask(workspaceSlug: string, data: { prompt: string; task: string }): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/ai-assistant/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
|
Loading…
Reference in New Issue
Block a user