Skip to content

Commit

Permalink
feat(chatDraft): rewrite chat draft with AI (#49)
Browse files Browse the repository at this point in the history
* feat[ChatDraft]: adding chat draft UI

* feat(ChatDraft): change to make chat ui better

* feat(ChatDraft): refactor unnecessary code

* feat(ChatDraft): rename quill modules and formats

* fix: prettier issue

* feat(ChatDraft): position add to draft button

* feat(chatDraft): rewrite with AI in chat draft

* fix: update successful message text

* refactor: improve error handling and message

* refactor: display throw error on toast message

* refactor: display throw error on toast message
  • Loading branch information
ArslanSaleem authored Oct 30, 2024
1 parent e3739bd commit ae8dd96
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 22 deletions.
46 changes: 44 additions & 2 deletions backend/app/api/v1/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@

from app.config import settings
from app.database import get_db
from app.exceptions import CreditLimitExceededException
from app.logger import Logger
from app.models.asset_content import AssetProcessingStatus
from app.repositories import (
conversation_repository,
project_repository,
user_repository,
)
from app.requests import chat_query
from app.requests import chat_query, request_draft_with_ai
from app.utils import clean_text, find_following_sentence_ending, find_sentence_endings
from app.vectorstore.chroma import ChromaDB
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session

chat_router = APIRouter()
Expand All @@ -24,6 +25,10 @@ class ChatRequest(BaseModel):
conversation_id: Optional[str] = None
query: str

class DraftRequest(BaseModel):
content: str = Field(..., min_length=1, description="Content cannot be empty")
prompt: str = Field(..., min_length=1, description="Prompt cannot be empty")


logger = Logger()

Expand Down Expand Up @@ -237,3 +242,40 @@ def chat_status(project_id: int, db: Session = Depends(get_db)):
status_code=400,
detail="Unable to process the chat query. Please try again.",
)

@chat_router.post("/draft", status_code=200)
def draft_with_ai(draft_request: DraftRequest, db: Session = Depends(get_db)):
try:

users = user_repository.get_users(db, n=1)

if not users:
raise HTTPException(status_code=404, detail="No User Exists!")

api_key = user_repository.get_user_api_key(db, users[0].id)

if not api_key:
raise HTTPException(status_code=404, detail="API Key not found!")

response = request_draft_with_ai(api_key.key, draft_request.model_dump_json())

return {
"status": "success",
"message": "Draft successfully generated!",
"data": {"response": response["response"]},
}

except HTTPException:
raise

except CreditLimitExceededException:
raise HTTPException(
status_code=402, detail="Credit limit Reached, Wait next month or upgrade your Plan!"
)

except Exception:
logger.error(traceback.format_exc())
raise HTTPException(
status_code=400,
detail="Unable to generate draft. Please try again.",
)
30 changes: 30 additions & 0 deletions backend/app/requests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,33 @@ def get_user_usage_data(api_token: str):
except requests.exceptions.JSONDecodeError:
logger.error(f"Invalid JSON response from API server: {response.text}")
raise Exception("Invalid JSON response")


def request_draft_with_ai(api_token: str, draft_request: dict) -> dict:
# Prepare the headers with the Bearer token
headers = {"x-authorization": f"Bearer {api_token}"}
# Send the request
response = requests.post(
f"{settings.pandaetl_server_url}/v1/draft",
data=draft_request,
headers=headers,
timeout=360,
)

try:
if response.status_code not in [200, 201]:

if response.status_code == 402:
raise CreditLimitExceededException(
response.json().get("detail", "Credit limit exceeded!")
)

logger.error(
f"Failed to draft with AI. It returned {response.status_code} code: {response.text}"
)
raise Exception(response.text)

return response.json()
except requests.exceptions.JSONDecodeError:
logger.error(f"Invalid JSON response from API server: {response.text}")
raise Exception("Invalid JSON response")
150 changes: 130 additions & 20 deletions frontend/src/components/ChatDraftDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"use client";
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import Drawer from "./ui/Drawer";
import { Button } from "./ui/Button";
import ReactQuill from "react-quill";
import { BookTextIcon } from "lucide-react";
import { BookTextIcon, Check, Loader2, X } from "lucide-react";
import { Textarea } from "./ui/Textarea";
import { draft_with_ai } from "@/services/chat";
import toast from "react-hot-toast";

interface IProps {
draft: string;
Expand Down Expand Up @@ -48,6 +51,10 @@ const ChatDraftDrawer = ({
onCancel,
}: IProps) => {
const quillRef = useRef<ReactQuill | null>(null);
const [step, setStep] = useState<number>(0);
const [userInput, setUserInput] = useState<string>("");
const [aiDraft, setAIDraft] = useState<string>("");
const [loadingAIDraft, setLoadingAIDraft] = useState<boolean>(false);

useEffect(() => {
if (quillRef.current) {
Expand All @@ -59,28 +66,131 @@ const ChatDraftDrawer = ({
}
}, [draft]);

const handleUserInputChange = (
event: React.ChangeEvent<HTMLTextAreaElement>
) => {
setUserInput(event.target.value);
};

const handleUserInputKeyPress = async (
event: React.KeyboardEvent<HTMLTextAreaElement>
) => {
if (event.key === "Enter" && userInput.trim() !== "") {
event.preventDefault();
try {
if (userInput.length === 0) {
toast.error("Please provide the prompt and try again!");
return;
}
setLoadingAIDraft(true);
const data = await draft_with_ai({ content: draft, prompt: userInput });
setAIDraft(data.response);
setUserInput("");
setStep(2);
setLoadingAIDraft(false);
} catch (error) {
console.error(error);
toast.error(error instanceof Error ? error.message : String(error));
setLoadingAIDraft(false);
}
}
};

return (
<Drawer isOpen={isOpen} onClose={onCancel} title="Draft Chat">
<div className="flex flex-col h-full">
<ReactQuill
ref={quillRef}
theme="snow"
value={draft}
onChange={onSubmit}
modules={quill_modules}
formats={quill_formats}
/>
<div className="sticky bottom-0 bg-white pb-4">
<div className="flex gap-2">
<Button
// onClick={onSubmit}
className="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark"
>
<BookTextIcon className="inline-block mr-2" size={16} />
Rewrite with AI
</Button>
{(step === 0 || step === 1) && (
<>
<ReactQuill
ref={quillRef}
theme="snow"
value={draft}
onChange={onSubmit}
modules={quill_modules}
formats={quill_formats}
/>

<div className="sticky bottom-0 bg-white pb-4 pt-4">
<Button
onClick={() => {
setStep(1);
}}
disabled={draft.length == 0}
className="px-4 bg-primary text-white rounded hover:bg-primary-dark"
>
<BookTextIcon className="inline-block mr-2" size={16} />
Rewrite with AI
</Button>
</div>
</>
)}

{step === 2 && (
<>
<ReactQuill
ref={quillRef}
theme="snow"
value={aiDraft}
readOnly={true}
modules={{ toolbar: false }}
/>

<div className="sticky bottom-0 bg-white pb-4">
<div className="flex gap-2">
<Button
onClick={() => setStep(0)}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-800"
>
<X className="inline-block mr-2" size={16} />
Cancel
</Button>
<Button
onClick={() => {
onSubmit(aiDraft);
setStep(0);
}}
className="mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-800"
>
<Check className="inline-block mr-2" size={16} />
Accept
</Button>
<Button
onClick={() => setStep(1)}
className="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark"
>
<BookTextIcon className="inline-block mr-2" size={16} />
Rewrite
</Button>
</div>
</div>
</>
)}
{/* Centered overlay input for step 1 */}
{step === 1 && (
<div className="absolute inset-0 flex items-center justify-center z-50 bg-opacity-75 bg-gray-800">
<div className="bg-white p-6 rounded-lg shadow-lg max-w-lg w-full text-center">
{loadingAIDraft ? (
<Loader2 className="mx-auto my-4 h-8 w-8 animate-spin text-gray-500" />
) : (
<>
<Textarea
className="w-full p-2 border border-gray-300 rounded mb-4"
placeholder="Write prompt to edit content and press enter..."
value={userInput}
onChange={handleUserInputChange}
onKeyDown={handleUserInputKeyPress}
/>
<Button
onClick={() => setStep(0)}
className="text-sm text-gray-500 hover:text-gray-700"
>
Close
</Button>
</>
)}
</div>
</div>
</div>
)}
</div>
</Drawer>
);
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/interfaces/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ export interface ChatReferences {
start: number;
end: number;
}

export interface ChatDraftRequest {
content: string;
prompt: string;
}
23 changes: 23 additions & 0 deletions frontend/src/services/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from "axios";
import { GetRequest, PostRequest } from "@/lib/requests";
import {
ChatDraftRequest,
ChatRequest,
ChatResponse,
ChatStatusResponse,
Expand Down Expand Up @@ -51,3 +52,25 @@ export const chatStatus = async (projectId: string) => {
}
}
};

export const draft_with_ai = async (data: ChatDraftRequest) => {
try {
const response = await PostRequest<ChatResponse>(
`${chatApiUrl}/draft`,
{ ...data },
{},
300000
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.data) {
throw new Error(error.response.data.detail);
} else {
throw new Error("Failed to generate draft with AI. Please try again.");
}
} else {
throw new Error("Failed to generate draft with AI. Please try again.");
}
}
};

0 comments on commit ae8dd96

Please sign in to comment.