Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
“Shriya
2024-08-21 06:00:19 +05:30
17 changed files with 1513 additions and 1789 deletions

View File

@@ -74,7 +74,7 @@ def run_async_chain(chain, question, chat_history):
def get_data_from_api_key(api_key):
data = api_key_collection.find_one({"key": api_key})
# # Raise custom exception if the API key is not found
if data is None:
raise Exception("Invalid API Key, please generate new key", 401)
@@ -129,10 +129,10 @@ def save_conversation(conversation_id, question, response, source_log_docs, llm)
"content": "Summarise following conversation in no more than 3 "
"words, respond ONLY with the summary, use the same "
"language as the system \n\nUser: "
+question
+"\n\n"
+"AI: "
+response,
+ question
+ "\n\n"
+ "AI: "
+ response,
},
{
"role": "user",
@@ -172,7 +172,9 @@ def get_prompt(prompt_id):
return prompt
def complete_stream(question, retriever, conversation_id, user_api_key):
def complete_stream(
question, retriever, conversation_id, user_api_key, isNoneDoc=False
):
try:
response_full = ""
@@ -186,126 +188,136 @@ def complete_stream(question, retriever, conversation_id, user_api_key):
elif "source" in line:
source_log_docs.append(line["source"])
if isNoneDoc:
for doc in source_log_docs:
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
)
if(user_api_key is None):
)
if user_api_key is None:
conversation_id = save_conversation(
conversation_id, question, response_full, source_log_docs, llm
)
# send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)})
yield f"data: {data}\n\n"
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
except Exception as e:
print("\033[91merr", str(e), file=sys.stderr)
data = json.dumps({"type": "error","error":"Please try again later. We apologize for any inconvenience.",
"error_exception": str(e)})
data = json.dumps(
{
"type": "error",
"error": "Please try again later. We apologize for any inconvenience.",
"error_exception": str(e),
}
)
yield f"data: {data}\n\n"
return
return
@answer.route("/stream", methods=["POST"])
def stream():
try:
data = request.get_json()
# get parameter from url question
question = data["question"]
if "history" not in data:
history = []
else:
history = data["history"]
history = json.loads(history)
if "conversation_id" not in data:
conversation_id = None
else:
conversation_id = data["conversation_id"]
if "prompt_id" in data:
prompt_id = data["prompt_id"]
else:
prompt_id = "default"
if "selectedDocs" in data and data["selectedDocs"] is None:
chunks = 0
elif "chunks" in data:
chunks = int(data["chunks"])
else:
chunks = 2
if "token_limit" in data:
token_limit = data["token_limit"]
else:
token_limit = settings.DEFAULT_MAX_HISTORY
try:
data = request.get_json()
question = data["question"]
if "history" not in data:
history = []
else:
history = data["history"]
history = json.loads(history)
if "conversation_id" not in data:
conversation_id = None
else:
conversation_id = data["conversation_id"]
if "prompt_id" in data:
prompt_id = data["prompt_id"]
else:
prompt_id = "default"
if "selectedDocs" in data and data["selectedDocs"] is None:
chunks = 0
elif "chunks" in data:
chunks = int(data["chunks"])
else:
chunks = 2
if "token_limit" in data:
token_limit = data["token_limit"]
else:
token_limit = settings.DEFAULT_MAX_HISTORY
# check if active_docs or api_key is set
# check if active_docs or api_key is set
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key["chunks"])
prompt_id = data_key["prompt_id"]
source = {"active_docs": data_key["source"]}
user_api_key = data["api_key"]
elif "active_docs" in data:
source = {"active_docs": data["active_docs"]}
user_api_key = None
else:
source = {}
user_api_key = None
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key["chunks"])
prompt_id = data_key["prompt_id"]
source = {"active_docs": data_key["source"]}
user_api_key = data["api_key"]
elif "active_docs" in data:
source = {"active_docs": data["active_docs"]}
user_api_key = None
else:
source = {}
user_api_key = None
if (
source["active_docs"].split("/")[0] == "default"
or source["active_docs"].split("/")[0] == "local"
):
retriever_name = "classic"
else:
retriever_name = source["active_docs"]
if source["active_docs"].split("/")[0] in ["default", "local"]:
retriever_name = "classic"
else:
retriever_name = source["active_docs"]
prompt = get_prompt(prompt_id)
prompt = get_prompt(prompt_id)
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,
source=source,
chat_history=history,
prompt=prompt,
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
)
return Response(
complete_stream(
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,
retriever=retriever,
conversation_id=conversation_id,
source=source,
chat_history=history,
prompt=prompt,
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
),
mimetype="text/event-stream",
)
except ValueError:
message = "Malformed request body"
print("\033[91merr", str(message), file=sys.stderr)
return Response(
error_stream_generate(message),
status=400,
mimetype="text/event-stream",
)
except Exception as e:
)
return Response(
complete_stream(
question=question,
retriever=retriever,
conversation_id=conversation_id,
user_api_key=user_api_key,
isNoneDoc=data.get("isNoneDoc"),
),
mimetype="text/event-stream",
)
except ValueError:
message = "Malformed request body"
print("\033[91merr", str(message), file=sys.stderr)
return Response(
error_stream_generate(message),
status=400,
mimetype="text/event-stream",
)
except Exception as e:
print("\033[91merr", str(e), file=sys.stderr)
message = e.args[0]
status_code = 400
# # Custom exceptions with two arguments, index 1 as status code
if(len(e.args) >= 2):
if len(e.args) >= 2:
status_code = e.args[1]
return Response(
error_stream_generate(message),
status=status_code,
mimetype="text/event-stream",
)
error_stream_generate(message),
status=status_code,
mimetype="text/event-stream",
)
def error_stream_generate(err_response):
data = json.dumps({"type": "error", "error":err_response})
yield f"data: {data}\n\n"
data = json.dumps({"type": "error", "error": err_response})
yield f"data: {data}\n\n"
@answer.route("/api/answer", methods=["POST"])
def api_answer():
@@ -346,10 +358,7 @@ def api_answer():
source = data
user_api_key = None
if (
source["active_docs"].split("/")[0] == "default"
or source["active_docs"].split("/")[0] == "local"
):
if source["active_docs"].split("/")[0] in ["default", "local"]:
retriever_name = "classic"
else:
retriever_name = source["active_docs"]
@@ -375,6 +384,10 @@ def api_answer():
elif "answer" in line:
response_full += line["answer"]
if data.get("isNoneDoc"):
for doc in source_log_docs:
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
)
@@ -395,7 +408,6 @@ def api_answer():
@answer.route("/api/search", methods=["POST"])
def api_search():
data = request.get_json()
# get parameter from url question
question = data["question"]
if "chunks" in data:
chunks = int(data["chunks"])
@@ -413,10 +425,7 @@ def api_search():
source = {}
user_api_key = None
if (
source["active_docs"].split("/")[0] == "default"
or source["active_docs"].split("/")[0] == "local"
):
if source["active_docs"].split("/")[0] in ["default", "local"]:
retriever_name = "classic"
else:
retriever_name = source["active_docs"]
@@ -437,4 +446,9 @@ def api_search():
user_api_key=user_api_key,
)
docs = retriever.search()
if data.get("isNoneDoc"):
for doc in docs:
doc["source"] = "None"
return docs

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "docsgpt",
"version": "0.3.9",
"version": "0.4.0",
"private": false,
"description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.",
"source": "./src/index.html",

View File

@@ -5,7 +5,7 @@ import snarkdown from '@bpmn-io/snarkdown';
import styled, { keyframes, createGlobalStyle } from 'styled-components';
import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons';
import MessageIcon from '../assets/message.svg';
import { MESSAGE_TYPE, Query, Status } from '../types/index';
import { MESSAGE_TYPE, Query, Status, WidgetProps } from '../types/index';
import { fetchAnswerStreaming } from '../requests/streamingApi';
const GlobalStyles = createGlobalStyle`
@@ -46,13 +46,12 @@ const WidgetContainer = styled.div`
text-align: left;
`;
const StyledContainer = styled.div`
display: block;
display: flex;
position: relative;
flex-direction: column;
justify-content: center;
bottom: 0;
left: 0;
width: 352px;
height: 407px;
max-height: 407px;
border-radius: 0.75rem;
background-color: #222327;
font-family: sans-serif;
@@ -130,14 +129,23 @@ const Description = styled.p`
color: #A1A1AA;
margin-top: 0;
`;
const Conversation = styled.div`
height: 16rem;
const Conversation = styled.div<{ size: string }>`
min-height: 300px;
height: ${props => props.size === 'medium' ? '70vh' : '320px'};
width: ${props => props.size === 'medium' ? '28vw' : '400px'};
padding-inline: 0.5rem;
border-radius: 0.375rem;
text-align: left;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #4a4a4a transparent; /* thumb color track color */
@media only screen and (max-width: 768px) {
width: 90vw !important;
}
@media only screen and (min-width:768px ) and (max-width: 1024px) {
width:${props => props.size === 'medium' ? '60vw' : '400px'} !important;
}
`;
const MessageBubble = styled.div<{ type: MESSAGE_TYPE }>`
@@ -152,7 +160,7 @@ const Message = styled.p<{ type: MESSAGE_TYPE }>`
'#38383b'};
color: #ffff;
border: none;
max-width: 80%;
max-width: ${props => props.type === 'ANSWER' ? '100%' : '80'};
overflow: auto;
margin: 4px;
display: block;
@@ -192,16 +200,13 @@ const Delay = styled(DotAnimation) <{ delay: number }>`
`;
const PromptContainer = styled.form`
background-color: transparent;
height: 36px;
position: absolute;
bottom: 25px;
left: 24px;
right: 24px;
height: 40px;
margin: 16px;
display: flex;
justify-content: space-evenly;
`;
const StyledInput = styled.input`
width: 260px;
width: 100%;
height: 36px;
border: 1px solid #686877;
padding-left: 12px;
@@ -290,9 +295,9 @@ export const DocsGPTWidget = ({
title = 'Get AI assistance',
description = 'DocsGPT\'s AI Chatbot is here to help',
heroTitle = 'Welcome to DocsGPT !',
heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.'
}) => {
heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.',
size = 'small'
}: WidgetProps) => {
const [prompt, setPrompt] = React.useState('');
const [status, setStatus] = React.useState<Status>('idle');
const [queries, setQueries] = React.useState<Query[]>([])
@@ -393,7 +398,7 @@ export const DocsGPTWidget = ({
</ContentWrapper>
</Header>
</div>
<Conversation onWheel={handleUserInterrupt} onTouchMove={handleUserInterrupt}>
<Conversation size={size} onWheel={handleUserInterrupt} onTouchMove={handleUserInterrupt}>
{
queries.length > 0 ? queries?.map((query, index) => {
return (

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { createRoot } from 'react-dom/client';
import { DocsGPTWidget } from './components/DocsGPTWidget';
const renderWidget = (elementId: string, props = {}) => {
const root = createRoot(document.getElementById(elementId) as HTMLElement);
root.render(<DocsGPTWidget {...props} />);

View File

@@ -1,6 +1,10 @@
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER' | 'ERROR';
export type Status = 'idle' | 'loading' | 'failed';
export type FEEDBACK = 'LIKE' | 'DISLIKE';
export type DIMENSION = {
width: string,
height: string
}
export interface Query {
prompt: string;
@@ -10,4 +14,15 @@ export interface Query {
sources?: { title: string; text: string }[];
conversationId?: string | null;
title?: string | null;
}
export interface WidgetProps {
apiHost?: string;
selectDocs?: string;
apiKey?: string;
avatar?: string;
title?: string;
description?: string;
heroTitle?: string;
heroDescription?: string;
size?: 'small' | 'medium';
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,40 +21,40 @@
"dependencies": {
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
"i18next": "^23.11.5",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.3",
"react-i18next": "^14.1.2",
"react-markdown": "^8.0.7",
"react-i18next": "^15.0.1",
"react-markdown": "^9.0.1",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.1",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.0"
"remark-gfm": "^4.0.0"
},
"devDependencies": {
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.6",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-react": "^4.2.1",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.13",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-config-standard-with-typescript": "^34.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.6.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^8.0.0",
"lint-staged": "^15.2.8",
"postcss": "^8.4.31",
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"tailwindcss": "^3.2.4",

View File

@@ -0,0 +1,3 @@
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9294 5.375V12.4583C12.9294 12.8341 12.7801 13.1944 12.5145 13.4601C12.2488 13.7257 11.8885 13.875 11.5127 13.875H3.01274C2.63701 13.875 2.27668 13.7257 2.011 13.4601C1.74532 13.1944 1.59607 12.8341 1.59607 12.4583V2.54167C1.59607 2.16594 1.74532 1.80561 2.011 1.53993C2.27668 1.27426 2.63701 1.125 3.01274 1.125H8.6794M12.9294 5.375V5.25317C12.9293 4.87747 12.78 4.5172 12.5143 4.25158L9.80282 1.54008C9.53721 1.27439 9.17693 1.12508 8.80124 1.125H8.6794M12.9294 5.375H10.0961C9.72035 5.375 9.36001 5.22574 9.09434 4.96007C8.82866 4.69439 8.6794 4.33406 8.6794 3.95833V1.125" stroke="#949494" stroke-width="1.41667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

View File

@@ -0,0 +1,25 @@
<svg width="24" height="27" viewBox="0 0 24 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.73517 17.2121L0.538918 17.9125C0.3753 18.0084 0.239616 18.1455 0.145332 18.31C0.0510491 18.4746 0.00144577 18.661 0.00144577 18.8506C0.00144577 19.0403 0.0510491 19.2267 0.145332 19.3912C0.239616 19.5558 0.3753 19.6929 0.538918 19.7888L11.0514 25.9513C11.2181 26.0489 11.4078 26.1004 11.601 26.1004C11.7941 26.1004 11.9838 26.0489 12.1505 25.9513L22.663 19.7888C22.8266 19.6929 22.9623 19.5558 23.0566 19.3912C23.1509 19.2267 23.2005 19.0403 23.2005 18.8506C23.2005 18.661 23.1509 18.4746 23.0566 18.31C22.9623 18.1455 22.8266 18.0084 22.663 17.9125L21.4668 17.2107L13.2511 22.0276C12.7506 22.321 12.1811 22.4757 11.601 22.4757C11.0209 22.4757 10.4513 22.321 9.95087 22.0276L1.73517 17.2121Z" fill="url(#paint0_linear_4013_8178)"/>
<path d="M1.73517 11.4121L0.538918 12.1124C0.3753 12.2084 0.239616 12.3454 0.145332 12.51C0.0510491 12.6746 0.00144577 12.8609 0.00144577 13.0506C0.00144577 13.2403 0.0510491 13.4266 0.145332 13.5912C0.239616 13.7558 0.3753 13.8928 0.538918 13.9887L11.0514 20.1512C11.2181 20.2489 11.4078 20.3003 11.601 20.3003C11.7941 20.3003 11.9838 20.2489 12.1505 20.1512L22.663 13.9887C22.8266 13.8928 22.9623 13.7558 23.0566 13.5912C23.1509 13.4266 23.2005 13.2403 23.2005 13.0506C23.2005 12.8609 23.1509 12.6746 23.0566 12.51C22.9623 12.3454 22.8266 12.2084 22.663 12.1124L21.4668 11.4106L13.2511 16.2275C12.7506 16.5209 12.1811 16.6756 11.601 16.6756C11.0209 16.6756 10.4513 16.5209 9.95087 16.2275L1.73517 11.4121Z" fill="url(#paint1_linear_4013_8178)"/>
<path d="M12.152 0.149921C11.9849 0.0517579 11.7947 0 11.601 0C11.4072 0 11.217 0.0517579 11.05 0.149921L0.537472 6.31242C0.373854 6.40835 0.23817 6.5454 0.143887 6.70997C0.0496035 6.87454 0 7.06091 0 7.25057C0 7.44024 0.0496035 7.6266 0.143887 7.79117C0.23817 7.95574 0.373854 8.09279 0.537472 8.18872L11.05 14.3512C11.217 14.4494 11.4072 14.5011 11.601 14.5011C11.7947 14.5011 11.9849 14.4494 12.152 14.3512L22.6645 8.18872C22.8281 8.09279 22.9638 7.95574 23.0581 7.79117C23.1523 7.6266 23.2019 7.44024 23.2019 7.25057C23.2019 7.06091 23.1523 6.87454 23.0581 6.70997C22.9638 6.5454 22.8281 6.40835 22.6645 6.31242L12.152 0.149921Z" fill="url(#paint2_linear_4013_8178)"/>
<defs>
<linearGradient id="paint0_linear_4013_8178" x1="0.00144577" y1="21.6555" x2="23.2005" y2="21.6555" gradientUnits="userSpaceOnUse">
<stop stop-color="#70FDF7"/>
<stop offset="0.325" stop-color="#747696"/>
<stop offset="0.68" stop-color="#BD5372"/>
<stop offset="1" stop-color="#F5A06C"/>
</linearGradient>
<linearGradient id="paint1_linear_4013_8178" x1="0.00144577" y1="15.8555" x2="23.2005" y2="15.8555" gradientUnits="userSpaceOnUse">
<stop stop-color="#70FDF7"/>
<stop offset="0.325" stop-color="#747696"/>
<stop offset="0.68" stop-color="#BD5372"/>
<stop offset="1" stop-color="#F5A06C"/>
</linearGradient>
<linearGradient id="paint2_linear_4013_8178" x1="0" y1="7.25057" x2="23.2019" y2="7.25057" gradientUnits="userSpaceOnUse">
<stop stop-color="#70FDF7"/>
<stop offset="0.325" stop-color="#747696"/>
<stop offset="0.68" stop-color="#BD5372"/>
<stop offset="1" stop-color="#F5A06C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,54 @@
import React from 'react';
import Exit from '../assets/exit.svg';
type SidebarProps = {
isOpen: boolean;
toggleState: (arg0: boolean) => void;
children: React.ReactNode;
};
export default function Sidebar({
isOpen,
toggleState,
children,
}: SidebarProps) {
const sidebarRef = React.useRef<HTMLDivElement>(null);
const handleClickOutside = (event: MouseEvent) => {
if (
sidebarRef.current &&
!sidebarRef.current.contains(event.target as Node)
) {
toggleState(false);
}
};
React.useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div ref={sidebarRef} className="h-vh relative">
<div
className={`fixed right-0 top-0 z-50 h-full w-72 transform bg-white shadow-xl transition-all duration-300 dark:bg-chinese-black sm:w-96 ${
isOpen ? 'translate-x-[10px]' : 'translate-x-full'
} border-l border-[#9ca3af]/10`}
>
<div className="flex w-full flex-row items-end justify-end px-4 pt-3">
<button
className="w-7 rounded-full p-2 hover:bg-gray-1000 hover:dark:bg-gun-metal"
onClick={() => toggleState(!isOpen)}
>
<img className="filter dark:invert" src={Exit} />
</button>
</div>
<div className="flex h-full flex-col items-center gap-2 py-4 px-6 text-center">
{children}
</div>
</div>
</div>
);
}

View File

@@ -1,17 +1,27 @@
import { forwardRef, useState } from 'react';
import Avatar from '../components/Avatar';
import CopyButton from '../components/CopyButton';
import remarkGfm from 'remark-gfm';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import classes from './ConversationBubble.module.css';
import Alert from './../assets/alert.svg';
import Like from './../assets/like.svg?react';
import Dislike from './../assets/dislike.svg?react';
import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import remarkGfm from 'remark-gfm';
import Alert from '../assets/alert.svg';
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
import Dislike from '../assets/dislike.svg?react';
import Document from '../assets/document.svg';
import Like from '../assets/like.svg?react';
import Link from '../assets/link.svg';
import Sources from '../assets/sources.svg';
import Avatar from '../components/Avatar';
import CopyButton from '../components/CopyButton';
import Sidebar from '../components/Sidebar';
import {
selectChunks,
selectSelectedDocs,
} from '../preferences/preferenceSlice';
import classes from './ConversationBubble.module.css';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
const ConversationBubble = forwardRef<
@@ -29,15 +39,16 @@ const ConversationBubble = forwardRef<
{ message, type, className, feedback, handleFeedback, sources, retryBtn },
ref,
) {
const [openSource, setOpenSource] = useState<number | null>(null);
const chunks = useSelector(selectChunks);
const selectedDocs = useSelector(selectSelectedDocs);
const [isLikeHovered, setIsLikeHovered] = useState(false);
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
const [isLikeClicked, setIsLikeClicked] = useState(false);
const [isDislikeClicked, setIsDislikeClicked] = useState(false);
const [activeTooltip, setActiveTooltip] = useState<number | null>(null);
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
let bubble;
if (type === 'QUESTION') {
bubble = (
<div ref={ref} className={`flex flex-row-reverse self-end ${className}`}>
@@ -55,18 +66,147 @@ const ConversationBubble = forwardRef<
ref={ref}
className={`flex flex-wrap self-start ${className} group flex-col dark:text-bright-gray`}
>
<div className="flex flex-wrap self-start lg:flex-nowrap">
<Avatar
className="mt-2 h-12 w-12 text-2xl"
avatar={
<img
src={DocsGPT3}
alt="DocsGPT"
className="h-full w-full object-cover"
{DisableSourceFE ||
type === 'ERROR' ||
sources?.length === 0 ||
sources?.some((source) => source.source === 'None') ? null : !sources &&
chunks !== '0' &&
selectedDocs ? (
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
<div className="my-2 flex flex-row items-center justify-center gap-3">
<Avatar
className="h-[26px] w-[30px] text-xl"
avatar={
<img
src={Sources}
alt="Sources"
className="h-full w-full object-fill"
/>
}
/>
}
/>
<p className="text-base font-semibold">Sources</p>
</div>
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="flex h-28 cursor-pointer flex-col items-start gap-1 rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
>
<span className="h-px w-10 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
<span className="h-px w-24 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
<span className="h-px w-16 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
<span className="h-px w-32 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
<span className="h-px w-24 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
<span className="h-px w-20 animate-pulse cursor-pointer rounded-[20px] bg-[#B2B2B2] p-1"></span>
</div>
))}
</div>
</div>
) : (
sources && (
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
<div className="my-2 flex flex-row items-center justify-center gap-3">
<Avatar
className="h-[26px] w-[30px] text-xl"
avatar={
<img
src={Sources}
alt="Sources"
className="h-full w-full object-fill"
/>
}
/>
<p className="text-base font-semibold">Sources</p>
</div>
<div className="ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{sources?.slice(0, 3)?.map((source, index) => (
<div key={index} className="relative">
<div
className="h-28 cursor-pointer rounded-[20px] bg-gray-1000 p-4 hover:bg-[#F1F1F1] dark:bg-gun-metal dark:hover:bg-[#2C2E3C]"
onMouseOver={() => setActiveTooltip(index)}
onMouseOut={() => setActiveTooltip(null)}
>
<p className="ellipsis-text h-12 break-words text-xs">
{source.text}
</p>
<div
className={`mt-[14px] flex flex-row items-center gap-[6px] underline-offset-2 ${
source.source && source.source !== 'local'
? 'hover:text-[#007DFF] hover:underline dark:hover:text-[#48A0FF]'
: ''
}`}
onClick={() =>
source.source && source.source !== 'local'
? window.open(
source.source,
'_blank',
'noopener, noreferrer',
)
: null
}
>
<img
src={Document}
alt="Document"
className="h-[17px] w-[17px] object-fill"
/>
<p
className="mt-[2px] truncate text-xs"
title={
source.source && source.source !== 'local'
? source.source
: source.title
}
>
{source.source && source.source !== 'local'
? source.source
: source.title}
</p>
</div>
</div>
{activeTooltip === index && (
<div
className={`absolute left-1/2 z-30 max-h-48 w-40 translate-x-[-50%] translate-y-[3px] rounded-xl bg-[#FBFBFB] p-4 text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver sm:w-56`}
onMouseOver={() => setActiveTooltip(index)}
onMouseOut={() => setActiveTooltip(null)}
>
<p className="max-h-[164px] overflow-y-auto break-words rounded-md text-sm">
{source.text}
</p>
</div>
)}
</div>
))}
{(sources?.length ?? 0) > 3 && (
<div
className="flex h-24 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
onClick={() => setIsSidebarOpen(true)}
>
<p className="ellipsis-text h-22 text-xs">{`View ${
sources?.length ? sources.length - 3 : 0
} more`}</p>
</div>
)}
</div>
</div>
</div>
)
)}
<div className="flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
<div className="my-2 flex flex-row items-center justify-center gap-3">
<Avatar
className="h-[34px] w-[34px] text-2xl"
avatar={
<img
src={DocsGPT3}
alt="DocsGPT"
className="h-full w-full object-cover"
/>
}
/>
<p className="text-base font-semibold">Answer</p>
</div>
<div
className={`ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
type === 'ERROR'
@@ -86,19 +226,19 @@ const ConversationBubble = forwardRef<
className="whitespace-pre-wrap break-normal leading-normal"
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
code(props) {
const { children, className, node, ref, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
return match ? (
<div className="group relative">
<SyntaxHighlighter
{...rest}
PreTag="div"
children={String(children).replace(/\n$/, '')}
language={match[1]}
{...props}
style={vscDarkPlus}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
/>
<div
className={`absolute right-3 top-3 lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''} `}
@@ -165,51 +305,9 @@ const ConversationBubble = forwardRef<
>
{message}
</ReactMarkdown>
{DisableSourceFE ||
type === 'ERROR' ||
!sources ||
sources.length === 0 ? null : (
<>
<span className="mt-3 h-px w-full bg-[#DEDEDE]"></span>
<div className="mt-3 flex w-full flex-row flex-wrap items-center justify-start gap-2">
<div className="py-1 text-base font-semibold">Sources:</div>
<div className="flex flex-row flex-wrap items-center justify-start gap-2">
{sources?.map((source, index) => (
<div
key={index}
className={`max-w-xs cursor-pointer rounded-[28px] px-4 py-1 sm:max-w-sm md:max-w-md ${
openSource === index
? 'bg-[#007DFF]'
: 'bg-[#D7EBFD] hover:bg-[#BFE1FF]'
}`}
onClick={() =>
source.source !== 'local'
? window.open(
source.source,
'_blank',
'noopener, noreferrer',
)
: setOpenSource(openSource === index ? null : index)
}
>
<p
className={`truncate text-center text-base font-medium ${
openSource === index
? 'text-white'
: 'text-[#007DFF]'
}`}
>
{index + 1}. {source.title.substring(0, 45)}
</p>
</div>
))}
</div>
</div>
</>
)}
</div>
</div>
<div className="my-2 flex justify-start lg:ml-12">
<div className="my-2 ml-2 flex justify-start">
<div
className={`relative mr-5 block items-center justify-center lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
@@ -292,19 +390,14 @@ const ConversationBubble = forwardRef<
</>
)}
</div>
{sources && openSource !== null && sources[openSource] && (
<div className="ml-10 mt-12 max-w-[300px] break-words rounded-xl bg-blue-200 p-2 dark:bg-gun-metal sm:max-w-[800px] lg:mt-2">
<p className="m-1 w-3/4 truncate text-xs text-gray-500 dark:text-bright-gray">
Source: {sources[openSource].title}
</p>
<div className="m-2 rounded-xl border-2 border-gray-200 bg-white p-2 dark:border-chinese-silver dark:bg-dark-charcoal">
<p className="text-break text-black dark:text-bright-gray">
{sources[openSource].text}
</p>
</div>
</div>
{sources && (
<Sidebar
isOpen={isSidebarOpen}
toggleState={(state: boolean) => {
setIsSidebarOpen(state);
}}
children={<AllSources sources={sources} />}
/>
)}
</div>
);
@@ -312,4 +405,49 @@ const ConversationBubble = forwardRef<
return bubble;
});
type AllSourcesProps = {
sources: { title: string; text: string; source: string }[];
};
function AllSources(sources: AllSourcesProps) {
return (
<div className="h-full w-full">
<div className="w-full">
<p className="text-left text-xl">{`${sources.sources.length} Sources`}</p>
<div className="mx-1 mt-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%] "></div>
</div>
<div className="mt-6 flex h-[90%] w-60 flex-col items-center gap-4 overflow-y-auto sm:w-80">
{sources.sources.map((source, index) => (
<div
key={index}
className="min-h-32 w-full rounded-[20px] bg-gray-1000 p-4 dark:bg-[#28292E]"
>
<span className="flex flex-row">
<p
title={source.title}
className="ellipsis-text break-words text-left text-sm font-semibold"
>
{`${index + 1}. ${source.title}`}
</p>
{source.source && source.source !== 'local' ? (
<img
src={Link}
alt="Link"
className="h-3 w-3 cursor-pointer object-fill"
onClick={() =>
window.open(source.source, '_blank', 'noopener, noreferrer')
}
></img>
) : null}
</span>
<p className="mt-3 max-h-24 overflow-y-auto break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
{source.text}
</p>
</div>
))}
</div>
</div>
);
}
export default ConversationBubble;

View File

@@ -116,6 +116,7 @@ export function handleFetchAnswerSteaming(
prompt_id: promptId,
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
},
signal,
)
@@ -184,6 +185,7 @@ export function handleSearch(
history,
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
})
.then((response) => response.json())
.then((data) => {

View File

@@ -62,7 +62,7 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
dispatch(
updateStreamingSource({
index: state.conversation.queries.length - 1,
query: { sources },
query: { sources: sources ?? [] },
}),
);
});

View File

@@ -433,3 +433,11 @@ template {
.bottom-safe {
bottom: env(safe-area-inset-bottom, 0);
}
.ellipsis-text {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}

View File

@@ -76,17 +76,17 @@ export function setLocalPrompt(prompt: string): void {
localStorage.setItem('DocsGPTPrompt', prompt);
}
export function setLocalRecentDocs(doc: Doc): void {
export function setLocalRecentDocs(doc: Doc | null): void {
localStorage.setItem('DocsGPTRecentDocs', JSON.stringify(doc));
let namePath = doc.name;
if (doc.language === namePath) {
let namePath = doc?.name;
if (doc?.language === namePath) {
namePath = '.project';
}
let docPath = 'default';
if (doc.location === 'local') {
if (doc?.location === 'local') {
docPath = 'local' + '/' + doc.name + '/';
} else if (doc.location === 'remote') {
} else if (doc?.location === 'remote') {
docPath =
doc.language + '/' + namePath + '/' + doc.version + '/' + doc.model + '/';
}

View File

@@ -95,8 +95,7 @@ prefListenerMiddleware.startListening({
matcher: isAnyOf(setSelectedDocs),
effect: (action, listenerApi) => {
setLocalRecentDocs(
(listenerApi.getState() as RootState).preference.selectedDocs ??
([] as unknown as Doc),
(listenerApi.getState() as RootState).preference.selectedDocs ?? null,
);
},
});