Merge pull request #876 from ManishMadan2882/main

Pause Auto-scroll on user interrupt
pull/880/head
Alex 3 months ago committed by GitHub
commit 725033659a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -7,7 +7,7 @@
"license": "MIT",
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt": "^0.3.6",
"docsgpt": "^0.3.7",
"next": "^14.0.4",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "docsgpt",
"version": "0.3.6",
"version": "0.3.7",
"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",
@ -14,7 +14,7 @@
"@parcel/resolver-default": {
"packageExports": true
},
"resolution":{
"resolution": {
"styled-components": "^5"
},
"scripts": {
@ -27,6 +27,7 @@
},
"dependencies": {
"@babel/plugin-transform-flow-strip-types": "^7.23.3",
"@bpmn-io/snarkdown": "^2.2.0",
"@parcel/resolver-glob": "^2.12.0",
"@parcel/transformer-svg-react": "^2.12.0",
"@parcel/transformer-typescript-tsc": "^2.12.0",
@ -36,6 +37,7 @@
"@types/react-dom": "^18.2.19",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"dompurify": "^3.0.9",
"flow-bin": "^0.229.2",
"i": "^0.3.7",
"install": "^0.13.0",
@ -51,6 +53,7 @@
"@babel/preset-react": "^7.23.3",
"@parcel/packager-ts": "^2.12.0",
"@parcel/transformer-typescript-types": "^2.12.0",
"@types/dompurify": "^3.0.5",
"babel-loader": "^8.0.4",
"process": "^0.11.10",
"typescript": "^5.3.3"

@ -5,7 +5,35 @@ import { MESSAGE_TYPE } from '../models/types';
import { Query, Status } from '../models/types';
import MessageIcon from '../assets/message.svg'
import { fetchAnswerStreaming } from '../requests/streamingApi';
import styled, { keyframes } from 'styled-components';
import styled, { keyframes, createGlobalStyle } from 'styled-components';
import snarkdown from '@bpmn-io/snarkdown';
import { sanitize } from 'dompurify';
const GlobalStyles = createGlobalStyle`
.response pre {
padding: 8px;
width: 90%;
font-size: 12px;
border-radius: 6px;
overflow-x: auto;
background-color: #1B1C1F;
}
.response h1{
font-size: 20px;
}
.response h2{
font-size: 18px;
}
.response h3{
font-size: 16px;
}
.response code:not(pre code){
border-radius: 6px;
padding: 1px 3px 1px 3px;
font-size: 12px;
display: inline-block;
background-color: #646464;
}
`;
const WidgetContainer = styled.div`
display: block;
position: fixed;
@ -125,9 +153,10 @@ const Message = styled.p<{ type: MESSAGE_TYPE }>`
color: #ffff;
border: none;
max-width: 80%;
overflow: auto;
margin: 4px;
display: block;
line-height: 1.5;
padding: 0.75rem;
border-radius: 0.375rem;
`;
@ -136,6 +165,7 @@ const ErrorAlert = styled.div`
border:0.1px solid #b91c1c;
display: flex;
padding:4px;
margin:0.7rem;
opacity: 90%;
max-width: 70%;
font-weight: 400;
@ -268,8 +298,11 @@ export const DocsGPTWidget = ({
const [queries, setQueries] = useState<Query[]>([])
const [conversationId, setConversationId] = useState<string | null>(null)
const [open, setOpen] = useState<boolean>(false)
const scrollRef = useRef<HTMLDivElement | null>(null);
const [eventInterrupt, setEventInterrupt] = useState<boolean>(false); //click or scroll by user while autoScrolling
const endMessageRef = useRef<HTMLDivElement | null>(null);
const handleUserInterrupt = () => {
(status === 'loading') && setEventInterrupt(true);
}
const scrollToBottom = (element: Element | null) => {
//recursive function to scroll to the last child of the last child ...
// to get to the bottom most element
@ -285,11 +318,11 @@ export const DocsGPTWidget = ({
};
useEffect(() => {
scrollToBottom(scrollRef.current);
!eventInterrupt && scrollToBottom(endMessageRef.current);
}, [queries.length, queries[queries.length - 1]?.response]);
async function stream(question: string) {
setStatus('loading');
setStatus('loading')
try {
await fetchAnswerStreaming(
{
@ -319,18 +352,18 @@ export const DocsGPTWidget = ({
}
);
} catch (error) {
console.log(error);
const updatedQueries = [...queries];
updatedQueries[updatedQueries.length - 1].error = 'error'
setQueries(updatedQueries);
setStatus('idle')
//setEventInterrupt(false)
}
}
// submit handler
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setEventInterrupt(false);
queries.push({ prompt })
setPrompt('')
await stream(prompt)
@ -341,13 +374,14 @@ export const DocsGPTWidget = ({
return (
<>
<WidgetContainer>
<GlobalStyles />
{!open && <FloatingButton onClick={() => setOpen(true)} hidden={open}>
<MessageIcon style={{ marginTop: '8px' }} />
</FloatingButton>}
{open && <StyledContainer>
<div>
<CancelButton onClick={() => setOpen(false)}>
<Cross2Icon width={24} height={24} color='white'/>
<Cross2Icon width={24} height={24} color='white' />
</CancelButton>
<Header>
<IconWrapper>
@ -359,66 +393,66 @@ export const DocsGPTWidget = ({
</ContentWrapper>
</Header>
</div>
<Conversation>
{
queries.length > 0 ? queries?.map((query, index) => {
return (
<Fragment key={index}>
{
query.prompt && <MessageBubble type='QUESTION'>
<Message
type='QUESTION'
ref={(!(query.response || query.error) && index === queries.length - 1) ? scrollRef : null}>
{query.prompt}
</Message>
</MessageBubble>
}
{
query.response ? <MessageBubble type='ANSWER'>
<Message
type='ANSWER'
ref={(index === queries.length - 1) ? scrollRef : null}
>
{query.response}
</Message>
</MessageBubble>
: <div>
{
query.error ? <ErrorAlert>
<IconWrapper>
<ExclamationTriangleIcon style={{ marginTop: '4px' }} width={22} height={22} color='#b91c1c' />
</IconWrapper>
<div>
<h5 style={{ margin: 2 }}>Network Error</h5>
<span style={{ margin: 2, fontSize: '13px' }}>Something went wrong !</span>
</div>
</ErrorAlert>
: <MessageBubble type='ANSWER'>
<Message type='ANSWER' style={{ fontWeight: 600 }}>
<DotAnimation>.</DotAnimation>
<Delay delay={200}>.</Delay>
<Delay delay={400}>.</Delay>
</Message>
</MessageBubble>
}
</div>
}
</Fragment>)
})
: <Hero title={heroTitle} description={heroDescription} />
}
</Conversation>
<Conversation onWheel={handleUserInterrupt} onTouchMove={handleUserInterrupt}>
{
queries.length > 0 ? queries?.map((query, index) => {
return (
<Fragment key={index}>
{
query.prompt && <MessageBubble type='QUESTION'>
<Message
type='QUESTION'
ref={(!(query.response || query.error) && index === queries.length - 1) ? endMessageRef : null}>
{query.prompt}
</Message>
</MessageBubble>
}
{
query.response ? <MessageBubble type='ANSWER'>
<Message
type='ANSWER'
ref={(index === queries.length - 1) ? endMessageRef : null}
>
<div className="response" dangerouslySetInnerHTML={{ __html: sanitize(snarkdown(query.response)) }} />
</Message>
</MessageBubble>
: <div>
{
query.error ? <ErrorAlert>
<IconWrapper>
<ExclamationTriangleIcon style={{ marginTop: '4px' }} width={22} height={22} color='#b91c1c' />
</IconWrapper>
<div>
<h5 style={{ margin: 2 }}>Network Error</h5>
<span style={{ margin: 2, fontSize: '13px' }}>Something went wrong !</span>
</div>
</ErrorAlert>
: <MessageBubble type='ANSWER'>
<Message type='ANSWER' style={{ fontWeight: 600 }}>
<DotAnimation>.</DotAnimation>
<Delay delay={200}>.</Delay>
<Delay delay={400}>.</Delay>
</Message>
</MessageBubble>
}
</div>
}
</Fragment>)
})
: <Hero title={heroTitle} description={heroDescription} />
}
</Conversation>
<PromptContainer
onSubmit={handleSubmit}>
<StyledInput
value={prompt} onChange={(event) => setPrompt(event.target.value)}
type='text' placeholder="What do you want to do?" />
<StyledButton
disabled={prompt.length == 0 || status !== 'idle'}>
<PaperPlaneIcon width={15} height={15} color='white' />
</StyledButton>
</PromptContainer>
onSubmit={handleSubmit}>
<StyledInput
value={prompt} onChange={(event) => setPrompt(event.target.value)}
type='text' placeholder="What do you want to do?" />
<StyledButton
disabled={prompt.length == 0 || status !== 'idle'}>
<PaperPlaneIcon width={15} height={15} color='white' />
</StyledButton>
</PromptContainer>
</StyledContainer>}
</WidgetContainer>
</>

@ -54,7 +54,6 @@ export function fetchAnswerStreaming({
value,
}: ReadableStreamReadResult<Uint8Array>) => {
if (done) {
console.log(counterrr);
resolve();
return;
}

@ -5,7 +5,7 @@ import About from './About';
import PageNotFound from './PageNotFound';
import { inject } from '@vercel/analytics';
import { useMediaQuery } from './hooks';
import { useState,useEffect } from 'react';
import { useState} from 'react';
import Setting from './Setting';
inject();
@ -32,4 +32,4 @@ export default function App() {
</div>
</div>
);
}
}

@ -6,14 +6,15 @@ export default function Hero({ className = '' }: { className?: string }) {
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme()
return (
<div className={`mt-14 ${isMobile ? 'mb-2' : 'mb-12'}flex flex-col text-black-1000 dark:text-bright-gray`}>
<div className={`mt-14 ${isMobile ? 'mb-2' : 'mb-12'} flex flex-col text-black-1000 dark:text-bright-gray`}>
<div className=" mb-2 flex items-center justify-center sm:mb-10">
<p className="mr-2 text-4xl font-semibold">DocsGPT</p>
<img className="mb-2 h-14" src={DocsGPT3} alt="DocsGPT" />
</div>
{isMobile ? (
<p className="mb-3 text-center leading-6">
Welcome to <span className="font-bold ">DocsGPT</span>, your technical
Welcome to <span className="font-bold">DocsGPT</span>, your technical
documentation assistant! Start by entering your query in the input
field below, and we&apos;ll provide you with the most relevant
answers.

@ -1,12 +1,12 @@
import { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { NavLink, useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import DocsGPT3 from './assets/cute_docsgpt3.svg';
import Documentation from './assets/documentation.svg';
import DocumentationDark from './assets/documentation-dark.svg';
import Discord from './assets/discord.svg';
import DiscordDark from './assets/discord-dark.svg';
import Expand from './assets/expand.svg';
import Github from './assets/github.svg';
import GithubDark from './assets/github-dark.svg';
@ -46,10 +46,10 @@ interface NavigationProps {
navOpen: boolean;
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const NavImage: React.FC<{ Light: string; Dark: string }> = ({
Light,
Dark,
}) => {
const NavImage: React.FC<{
Light: string | undefined;
Dark: string | undefined;
}> = ({ Light, Dark }) => {
return (
<>
<img src={Dark} alt="icon" className="ml-2 hidden w-5 dark:block " />
@ -57,6 +57,10 @@ const NavImage: React.FC<{ Light: string; Dark: string }> = ({
</>
);
};
NavImage.propTypes = {
Light: PropTypes.string,
Dark: PropTypes.string,
};
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const dispatch = useDispatch();
const docs = useSelector(selectSourceDocs);

@ -4,7 +4,7 @@ import { useDarkTheme } from '../hooks';
import Hero from '../Hero';
import { AppDispatch } from '../store';
import ConversationBubble from './ConversationBubble';
import conversationSlice, {
import {
addQuery,
fetchAnswer,
selectQueries,
@ -17,19 +17,23 @@ import Spinner from './../assets/spinner.svg';
import { FEEDBACK, Query } from './conversationModels';
import { sendFeedback } from './conversationApi';
import ArrowDown from './../assets/arrow-down.svg';
import { selectConversationId } from '../preferences/preferenceSlice';
export default function Conversation() {
const queries = useSelector(selectQueries);
const status = useSelector(selectStatus);
const conversationId = useSelector(selectConversationId)
const dispatch = useDispatch<AppDispatch>();
const endMessageRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null);
const [isDarkTheme] = useDarkTheme();
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
const fetchStream = useRef<any>(null)
const fetchStream = useRef<any>(null);
const [eventInterrupt, setEventInterrupt] = useState(false);
const handleUserInterruption = () => {
if (!eventInterrupt && status === "loading")
setEventInterrupt(true)
}
useEffect(() => {
scrollIntoView();
!eventInterrupt && scrollIntoView();
}, [queries.length, queries[queries.length - 1]]);
useEffect(() => {
@ -46,6 +50,7 @@ export default function Conversation() {
}
}
}, [status])
useEffect(() => {
const observerCallback: IntersectionObserverCallback = (entries) => {
entries.forEach((entry) => {
@ -71,14 +76,14 @@ export default function Conversation() {
behavior: 'smooth',
block: 'start',
});
};
}
const handleQuestion = (question: string) => {
question = question.trim();
if (question === '') return;
setEventInterrupt(false)
dispatch(addQuery({ prompt: question }));
fetchStream.current = dispatch(fetchAnswer({ question }));
};
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
const prevFeedback = query.feedback;
@ -126,7 +131,10 @@ export default function Conversation() {
};
return (
<div className="flex flex-col justify-center p-4 md:flex-row">
<div
onWheel={handleUserInterruption}
onTouchMove={handleUserInterruption}
className="flex flex-col justify-center w-full p-4 md:flex-row">
{queries.length > 0 && !hasScrolledToLast && (
<button
onClick={scrollIntoView}
@ -142,7 +150,7 @@ export default function Conversation() {
)}
{queries.length > 0 && (
<div className="mt-20 mb-9 flex flex-col transition-all md:w-3/4">
<div className="mt-20 mb-9 flex flex-col transition-all md:w-3/4">
{queries.map((query, index) => {
return (
<Fragment key={index}>
@ -160,7 +168,7 @@ export default function Conversation() {
</div>
)}
{queries.length === 0 && (
<Hero className="mt-24 h-[100vh] md:mt-52"></Hero>
<Hero className="mt-24 md:mt-52"></Hero>
)}
<div className="absolute bottom-0 flex w-11/12 md:w-[65%] flex-col items-end self-center bg-white dark:bg-raisin-black pt-4 md:fixed">
<div className="flex h-full w-full">

Loading…
Cancel
Save