diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 73023a89..91e343fc 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -84,14 +84,10 @@ def api_feedback(): question = data["question"] answer = data["answer"] feedback = data["feedback"] - - feedback_collection.insert_one( - { - "question": question, - "answer": answer, - "feedback": feedback, - } - ) + new_doc = {"question": question, "answer": answer, "feedback": feedback} + if "api_key" in data: + new_doc["api_key"] = data["api_key"] + feedback_collection.insert_one(new_doc) return {"status": "ok"} diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index 813478e2..d449d0a3 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -1,5 +1,5 @@ { - "name": "docsgpt-react", + "name": "docsgpt", "version": "0.4.2", "private": false, "description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.", @@ -11,6 +11,18 @@ "dist", "package.json" ], + "targets": { + "modern": { + "engines": { + "browsers": "Chrome 80" + } + }, + "legacy": { + "engines": { + "browsers": "> 0.5%, last 2 versions, not dead" + } + } + }, "@parcel/resolver-default": { "packageExports": true }, diff --git a/extensions/react-widget/src/assets/dislike.svg b/extensions/react-widget/src/assets/dislike.svg new file mode 100644 index 00000000..ec1d24c2 --- /dev/null +++ b/extensions/react-widget/src/assets/dislike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/react-widget/src/assets/like.svg b/extensions/react-widget/src/assets/like.svg new file mode 100644 index 00000000..c49604ed --- /dev/null +++ b/extensions/react-widget/src/assets/like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index bc6adb6e..83defbcf 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,11 +1,13 @@ "use client"; -import React from 'react' +import React, { useRef } from 'react' import DOMPurify from 'dompurify'; import styled, { keyframes, createGlobalStyle } from 'styled-components'; import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons'; -import { MESSAGE_TYPE, Query, Status, WidgetProps } from '../types/index'; -import { fetchAnswerStreaming } from '../requests/streamingApi'; +import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetProps } from '../types/index'; +import { fetchAnswerStreaming, sendFeedback } from '../requests/streamingApi'; import { ThemeProvider } from 'styled-components'; +import Like from "../assets/like.svg" +import Dislike from "../assets/dislike.svg" import MarkdownIt from 'markdown-it'; const themes = { dark: { @@ -63,6 +65,10 @@ const GlobalStyles = createGlobalStyle` background-color: #646464; color: #fff !important; } +.response code { + white-space: pre-wrap !important; + line-break: loose !important; +} `; const Overlay = styled.div` position: fixed; @@ -195,12 +201,24 @@ const Conversation = styled.div<{ size: string }>` width:${props => props.size === 'large' ? '90vw' : props.size === 'medium' ? '60vw' : '400px'} !important; } `; - +const Feedback = styled.div` + background-color: transparent; + font-weight: normal; + gap: 12px; + display: flex; + padding: 6px; + clear: both; +`; const MessageBubble = styled.div<{ type: MESSAGE_TYPE }>` - display: flex; + display: block; font-size: 16px; - justify-content: ${props => props.type === 'QUESTION' ? 'flex-end' : 'flex-start'}; - margin: 0.5rem; + position: relative; + width: 100%;; + float: right; + margin: 0rem; + &:hover ${Feedback} * { + visibility: visible !important; + } `; const Message = styled.div<{ type: MESSAGE_TYPE }>` background: ${props => props.type === 'QUESTION' ? @@ -208,6 +226,7 @@ const Message = styled.div<{ type: MESSAGE_TYPE }>` props.theme.secondary.bg}; color: ${props => props.type === 'ANSWER' ? props.theme.primary.text : '#fff'}; border: none; + float: ${props => props.type === 'QUESTION' ? 'right' : 'left'}; max-width: ${props => props.type === 'ANSWER' ? '100%' : '80'}; overflow: auto; margin: 4px; @@ -315,6 +334,7 @@ const HeroDescription = styled.p` font-size: 14px; line-height: 1.5; `; + const Hero = ({ title, description, theme }: { title: string, description: string, theme: string }) => { return ( <> @@ -345,7 +365,8 @@ export const DocsGPTWidget = ({ size = 'small', theme = 'dark', buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/message.svg', - buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)' + buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', + collectFeedback = true }: WidgetProps) => { const [prompt, setPrompt] = React.useState(''); const [status, setStatus] = React.useState('idle'); @@ -353,6 +374,7 @@ export const DocsGPTWidget = ({ const [conversationId, setConversationId] = React.useState(null) const [open, setOpen] = React.useState(false) const [eventInterrupt, setEventInterrupt] = React.useState(false); //click or scroll by user while autoScrolling + const isBubbleHovered = useRef(false) const endMessageRef = React.useRef(null); const md = new MarkdownIt(); @@ -376,6 +398,36 @@ export const DocsGPTWidget = ({ !eventInterrupt && scrollToBottom(endMessageRef.current); }, [queries.length, queries[queries.length - 1]?.response]); + async function handleFeedback(feedback: FEEDBACK, index: number) { + let query = queries[index] + if (!query.response) + return; + if (query.feedback != feedback) { + sendFeedback({ + question: query.prompt, + answer: query.response, + feedback: feedback, + apikey: apiKey + }, apiHost) + .then(res => { + if (res.status == 200) { + query.feedback = feedback; + setQueries((prev: Query[]) => { + return prev.map((q, i) => (i === index ? query : q)); + }); + } + }) + .catch(err => console.log("Connection failed",err)) + } + else { + delete query.feedback; + setQueries((prev: Query[]) => { + return prev.map((q, i) => (i === index ? query : q)); + }); + + } + } + async function stream(question: string) { setStatus('loading') try { @@ -473,7 +525,7 @@ export const DocsGPTWidget = ({ } { - query.response ? + query.response ? { isBubbleHovered.current = true }} type='ANSWER'> + + {collectFeedback && + + handleFeedback("LIKE", index)} /> + handleFeedback("DISLIKE", index)} /> + } :
{ @@ -518,7 +588,7 @@ export const DocsGPTWidget = ({ type='text' placeholder="What do you want to do?" /> + disabled={prompt.trim().length == 0 || status !== 'idle'}> diff --git a/extensions/react-widget/src/requests/streamingApi.ts b/extensions/react-widget/src/requests/streamingApi.ts index b594915f..9cb9fddc 100644 --- a/extensions/react-widget/src/requests/streamingApi.ts +++ b/extensions/react-widget/src/requests/streamingApi.ts @@ -1,3 +1,4 @@ +import { FEEDBACK } from "@/types"; interface HistoryItem { prompt: string; response?: string; @@ -11,6 +12,12 @@ interface FetchAnswerStreamingProps { apiHost?: string; onEvent?: (event: MessageEvent) => void; } +interface FeedbackPayload { + question: string; + answer: string; + apikey: string; + feedback: FEEDBACK; +} export function fetchAnswerStreaming({ question = '', apiKey = '', @@ -20,12 +27,12 @@ export function fetchAnswerStreaming({ onEvent = () => { console.log("Event triggered, but no handler provided."); } }: FetchAnswerStreamingProps): Promise { return new Promise((resolve, reject) => { - const body= { + const body = { question: question, history: JSON.stringify(history), conversation_id: conversationId, model: 'default', - api_key:apiKey + api_key: apiKey }; fetch(apiHost + '/stream', { method: 'POST', @@ -80,4 +87,20 @@ export function fetchAnswerStreaming({ reject(error); }); }); -} \ No newline at end of file +} + + +export const sendFeedback = (payload: FeedbackPayload,apiHost:string): Promise => { + return fetch(`${apiHost}/api/feedback`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + question: payload.question, + answer: payload.answer, + feedback: payload.feedback, + api_key:payload.apikey + }), + }); +}; \ No newline at end of file diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index cb46f06b..a55b6342 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -23,4 +23,5 @@ export interface WidgetProps { theme?:THEME, buttonIcon?:string; buttonBg?:string; + collectFeedback?:boolean } \ No newline at end of file