Skip to content

Commit 87f6c81

Browse files
committed
feat(chatDraft): rewrite with AI in chat draft
1 parent 305a202 commit 87f6c81

File tree

5 files changed

+212
-22
lines changed

5 files changed

+212
-22
lines changed

backend/app/api/v1/chat.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
project_repository,
1111
user_repository,
1212
)
13-
from app.requests import chat_query
13+
from app.requests import chat_query, request_draft_with_ai
1414
from app.utils import clean_text, find_following_sentence_ending, find_sentence_endings
1515
from app.vectorstore.chroma import ChromaDB
1616
from fastapi import APIRouter, Depends, HTTPException
17-
from pydantic import BaseModel
17+
from pydantic import BaseModel, Field
1818
from sqlalchemy.orm import Session
1919

2020
chat_router = APIRouter()
@@ -24,6 +24,10 @@ class ChatRequest(BaseModel):
2424
conversation_id: Optional[str] = None
2525
query: str
2626

27+
class DraftRequest(BaseModel):
28+
content: str = Field(..., min_length=1, description="Content cannot be empty")
29+
prompt: str = Field(..., min_length=1, description="Prompt cannot be empty")
30+
2731

2832
logger = Logger()
2933

@@ -237,3 +241,35 @@ def chat_status(project_id: int, db: Session = Depends(get_db)):
237241
status_code=400,
238242
detail="Unable to process the chat query. Please try again.",
239243
)
244+
245+
@chat_router.post("/draft", status_code=200)
246+
def draft_with_ai(draft_request: DraftRequest, db: Session = Depends(get_db)):
247+
try:
248+
249+
users = user_repository.get_users(db, n=1)
250+
251+
if not users:
252+
raise HTTPException(status_code=404, detail="No User Exists!")
253+
254+
api_key = user_repository.get_user_api_key(db, users[0].id)
255+
256+
if not api_key:
257+
raise HTTPException(status_code=404, detail="API Key not found!")
258+
259+
response = request_draft_with_ai(api_key.key, draft_request.model_dump_json())
260+
261+
return {
262+
"status": "success",
263+
"message": "Chat message successfully generated.",
264+
"data": {"response": response["response"]},
265+
}
266+
267+
except HTTPException:
268+
raise
269+
270+
except Exception:
271+
logger.error(traceback.format_exc())
272+
raise HTTPException(
273+
status_code=400,
274+
detail="Unable to process the chat query. Please try again.",
275+
)

backend/app/requests/__init__.py

+24
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,27 @@ def get_user_usage_data(api_token: str):
220220
except requests.exceptions.JSONDecodeError:
221221
logger.error(f"Invalid JSON response from API server: {response.text}")
222222
raise Exception("Invalid JSON response")
223+
224+
225+
def request_draft_with_ai(api_token: str, draft_request: dict) -> dict:
226+
# Prepare the headers with the Bearer token
227+
headers = {"x-authorization": f"Bearer {api_token}"}
228+
# Send the request
229+
response = requests.post(
230+
f"{settings.pandaetl_server_url}/v1/draft",
231+
data=draft_request,
232+
headers=headers,
233+
timeout=360,
234+
)
235+
236+
try:
237+
if response.status_code not in [200, 201]:
238+
logger.error(
239+
f"Failed to draft with AI. It returned {response.status_code} code: {response.text}"
240+
)
241+
raise Exception(response.text)
242+
243+
return response.json()
244+
except requests.exceptions.JSONDecodeError:
245+
logger.error(f"Invalid JSON response from API server: {response.text}")
246+
raise Exception("Invalid JSON response")

frontend/src/components/ChatDraftDrawer.tsx

+130-20
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"use client";
2-
import React, { useEffect, useRef } from "react";
2+
import React, { useEffect, useRef, useState } from "react";
33
import Drawer from "./ui/Drawer";
44
import { Button } from "./ui/Button";
55
import ReactQuill from "react-quill";
6-
import { BookTextIcon } from "lucide-react";
6+
import { BookTextIcon, Check, Loader2, X } from "lucide-react";
7+
import { Textarea } from "./ui/Textarea";
8+
import { draft_with_ai } from "@/services/chat";
9+
import toast from "react-hot-toast";
710

811
interface IProps {
912
draft: string;
@@ -48,6 +51,10 @@ const ChatDraftDrawer = ({
4851
onCancel,
4952
}: IProps) => {
5053
const quillRef = useRef<ReactQuill | null>(null);
54+
const [step, setStep] = useState<number>(0);
55+
const [userInput, setUserInput] = useState<string>("");
56+
const [aiDraft, setAIDraft] = useState<string>("");
57+
const [loadingAIDraft, setLoadingAIDraft] = useState<boolean>(false);
5158

5259
useEffect(() => {
5360
if (quillRef.current) {
@@ -59,28 +66,131 @@ const ChatDraftDrawer = ({
5966
}
6067
}, [draft]);
6168

69+
const handleUserInputChange = (
70+
event: React.ChangeEvent<HTMLTextAreaElement>
71+
) => {
72+
setUserInput(event.target.value);
73+
};
74+
75+
const handleUserInputKeyPress = async (
76+
event: React.KeyboardEvent<HTMLTextAreaElement>
77+
) => {
78+
if (event.key === "Enter" && userInput.trim() !== "") {
79+
event.preventDefault();
80+
try {
81+
if (userInput.length === 0) {
82+
toast.error("Please provide the prompt and try again!");
83+
return;
84+
}
85+
setLoadingAIDraft(true);
86+
const data = await draft_with_ai({ content: draft, prompt: userInput });
87+
setAIDraft(data.response);
88+
setUserInput("");
89+
setStep(2);
90+
setLoadingAIDraft(false);
91+
} catch (error) {
92+
console.error(error);
93+
toast.error("Failed to draft with AI. Try again!");
94+
setLoadingAIDraft(false);
95+
}
96+
}
97+
};
98+
6299
return (
63100
<Drawer isOpen={isOpen} onClose={onCancel} title="Draft Chat">
64101
<div className="flex flex-col h-full">
65-
<ReactQuill
66-
ref={quillRef}
67-
theme="snow"
68-
value={draft}
69-
onChange={onSubmit}
70-
modules={quill_modules}
71-
formats={quill_formats}
72-
/>
73-
<div className="sticky bottom-0 bg-white pb-4">
74-
<div className="flex gap-2">
75-
<Button
76-
// onClick={onSubmit}
77-
className="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark"
78-
>
79-
<BookTextIcon className="inline-block mr-2" size={16} />
80-
Rewrite with AI
81-
</Button>
102+
{(step === 0 || step === 1) && (
103+
<>
104+
<ReactQuill
105+
ref={quillRef}
106+
theme="snow"
107+
value={draft}
108+
onChange={onSubmit}
109+
modules={quill_modules}
110+
formats={quill_formats}
111+
/>
112+
113+
<div className="sticky bottom-0 bg-white pb-4 pt-4">
114+
<Button
115+
onClick={() => {
116+
setStep(1);
117+
}}
118+
disabled={draft.length == 0}
119+
className="px-4 bg-primary text-white rounded hover:bg-primary-dark"
120+
>
121+
<BookTextIcon className="inline-block mr-2" size={16} />
122+
Rewrite with AI
123+
</Button>
124+
</div>
125+
</>
126+
)}
127+
128+
{step === 2 && (
129+
<>
130+
<ReactQuill
131+
ref={quillRef}
132+
theme="snow"
133+
value={aiDraft}
134+
readOnly={true}
135+
modules={{ toolbar: false }}
136+
/>
137+
138+
<div className="sticky bottom-0 bg-white pb-4">
139+
<div className="flex gap-2">
140+
<Button
141+
onClick={() => setStep(0)}
142+
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-800"
143+
>
144+
<X className="inline-block mr-2" size={16} />
145+
Cancel
146+
</Button>
147+
<Button
148+
onClick={() => {
149+
onSubmit(aiDraft);
150+
setStep(0);
151+
}}
152+
className="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-800"
153+
>
154+
<Check className="inline-block mr-2" size={16} />
155+
Accept
156+
</Button>
157+
<Button
158+
onClick={() => setStep(1)}
159+
className="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark"
160+
>
161+
<BookTextIcon className="inline-block mr-2" size={16} />
162+
Rewrite
163+
</Button>
164+
</div>
165+
</div>
166+
</>
167+
)}
168+
{/* Centered overlay input for step 1 */}
169+
{step === 1 && (
170+
<div className="absolute inset-0 flex items-center justify-center z-50 bg-opacity-75 bg-gray-800">
171+
<div className="bg-white p-6 rounded-lg shadow-lg max-w-lg w-full text-center">
172+
{loadingAIDraft ? (
173+
<Loader2 className="mx-auto my-4 h-8 w-8 animate-spin text-gray-500" />
174+
) : (
175+
<>
176+
<Textarea
177+
className="w-full p-2 border border-gray-300 rounded mb-4"
178+
placeholder="Write prompt to edit content and press enter..."
179+
value={userInput}
180+
onChange={handleUserInputChange}
181+
onKeyDown={handleUserInputKeyPress}
182+
/>
183+
<Button
184+
onClick={() => setStep(0)}
185+
className="text-sm text-gray-500 hover:text-gray-700"
186+
>
187+
Close
188+
</Button>
189+
</>
190+
)}
191+
</div>
82192
</div>
83-
</div>
193+
)}
84194
</div>
85195
</Drawer>
86196
);

frontend/src/interfaces/chat.ts

+5
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ export interface ChatReferences {
2727
start: number;
2828
end: number;
2929
}
30+
31+
export interface ChatDraftRequest {
32+
content: string;
33+
prompt: string;
34+
}

frontend/src/services/chat.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from "axios";
22
import { GetRequest, PostRequest } from "@/lib/requests";
33
import {
4+
ChatDraftRequest,
45
ChatRequest,
56
ChatResponse,
67
ChatStatusResponse,
@@ -51,3 +52,17 @@ export const chatStatus = async (projectId: string) => {
5152
}
5253
}
5354
};
55+
56+
export const draft_with_ai = async (data: ChatDraftRequest) => {
57+
try {
58+
const response = await PostRequest<ChatResponse>(
59+
`${chatApiUrl}/draft`,
60+
{ ...data },
61+
{},
62+
300000
63+
);
64+
return response.data.data;
65+
} catch (error) {
66+
throw error;
67+
}
68+
};

0 commit comments

Comments
 (0)