-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5171309
commit 0b5e6c3
Showing
10 changed files
with
318 additions
and
231 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,163 +1,90 @@ | ||
import { AI_DISCLAIMER, AI_SEARCH_UNSUBMITTED } from "@/lib/constants/common"; | ||
import React, { useEffect, useState } from "react"; | ||
import { | ||
StyledResponseActions, | ||
StyledResponseDisclaimer, | ||
StyledUnsubmitted, | ||
} from "@/components/Chat/Response/Response.styled"; | ||
import { defaultState, useSearchState } from "@/context/search-context"; | ||
|
||
import Announcement from "@/components/Shared/Announcement"; | ||
import { Button } from "@nulib/design-system"; | ||
import ChatConversation from "./Conversation"; | ||
import ChatFeedback from "@/components/Chat/Feedback/Feedback"; | ||
import ChatResponse from "@/components/Chat/Response/Response"; | ||
import Container from "@/components/Shared/Container"; | ||
import { prepareQuestion } from "@/lib/chat-helpers"; | ||
import useChatSocket from "@/hooks/useChatSocket"; | ||
import useQueryParams from "@/hooks/useQueryParams"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
|
||
const Chat = ({ | ||
viewResultsCallback, | ||
}: { | ||
viewResultsCallback?: () => void; | ||
}) => { | ||
const { searchTerm = "" } = useQueryParams(); | ||
const { authToken, isConnected, message, sendMessage } = useChatSocket(); | ||
const [conversationRef, setConversationRef] = useState<string>(); | ||
|
||
const [streamingError, setStreamingError] = useState(""); | ||
interface Conversation { | ||
question: string; | ||
answer: string; | ||
} | ||
|
||
/** | ||
* get the`chat` state and dispatch function from the search context | ||
* for persisting the chat state when search screen tabs are switched | ||
*/ | ||
const { | ||
searchState: { chat }, | ||
searchDispatch, | ||
} = useSearchState(); | ||
const { question, answer } = chat; | ||
const Chat = () => { | ||
const { searchTerm } = useQueryParams(); | ||
|
||
const [isStreamingComplete, setIsStreamingComplete] = useState(false); | ||
const initialConversation = { | ||
question: searchTerm, | ||
answer: "", | ||
}; | ||
|
||
useEffect(() => { | ||
if ( | ||
!isStreamingComplete && | ||
isConnected && | ||
authToken && | ||
searchTerm && | ||
conversationRef | ||
) { | ||
resetChat(); | ||
const preparedQuestion = prepareQuestion( | ||
searchTerm, | ||
authToken, | ||
conversationRef, | ||
); | ||
sendMessage(preparedQuestion); | ||
} | ||
}, [ | ||
authToken, | ||
isStreamingComplete, | ||
isConnected, | ||
searchTerm, | ||
conversationRef, | ||
sendMessage, | ||
const [conversationRef, setConversationRef] = useState<string>(); | ||
const [conversation, setConversation] = useState<Conversation[]>([ | ||
initialConversation, | ||
]); | ||
const [isStreaming, setIsStreaming] = useState(false); | ||
|
||
useEffect(() => { | ||
setIsStreamingComplete(false); | ||
setConversationRef(uuidv4()); | ||
const conversationRef = uuidv4(); | ||
setIsStreaming(true); | ||
setConversationRef(conversationRef); | ||
setConversation([initialConversation]); | ||
}, [searchTerm]); | ||
|
||
useEffect(() => { | ||
if (!message || !conversationRef) return; | ||
}, [message]); | ||
|
||
function handleNewQuestion() { | ||
const input = document.getElementById("dc-search") as HTMLInputElement; | ||
if (input) { | ||
input.focus(); | ||
input.value = ""; | ||
} | ||
} | ||
|
||
function resetChat() { | ||
searchDispatch({ | ||
chat: defaultState.chat, | ||
type: "updateChat", | ||
}); | ||
} | ||
|
||
if (!searchTerm) | ||
return ( | ||
<Container> | ||
<StyledUnsubmitted>{AI_SEARCH_UNSUBMITTED}</StyledUnsubmitted> | ||
</Container> | ||
); | ||
const handleConversationCallback = (value: string) => { | ||
setConversation([ | ||
...conversation, | ||
{ | ||
question: value, | ||
answer: "", | ||
}, | ||
]); | ||
}; | ||
|
||
const handleResponseCallback = (content: any) => { | ||
if (!conversationRef) return; | ||
|
||
setIsStreamingComplete(true); | ||
searchDispatch({ | ||
chat: { | ||
// content here is now a react element | ||
// once continued conversations ar e in place | ||
// see note below for question refactor | ||
answer: content, | ||
|
||
// documents should be eventually removed as | ||
// they are now integrated into content | ||
// doing so will require some careful refactoring | ||
// as the documents are used in feedback form | ||
documents: [], | ||
|
||
// question should become an entry[] with | ||
// entry[n].question and entry[n].content | ||
question: searchTerm || "", | ||
|
||
ref: conversationRef, | ||
}, | ||
type: "updateChat", | ||
}); | ||
setIsStreaming(false); | ||
}; | ||
|
||
return ( | ||
<> | ||
<ChatResponse | ||
conversationRef={conversationRef} | ||
isStreamingComplete={isStreamingComplete} | ||
key={conversationRef} | ||
message={message} | ||
question={searchTerm} | ||
responseCallback={handleResponseCallback} | ||
/> | ||
{streamingError && ( | ||
<Container> | ||
<Announcement css={{ marginTop: "1rem" }}> | ||
{streamingError} | ||
</Announcement> | ||
</Container> | ||
)} | ||
{isStreamingComplete && ( | ||
<> | ||
<Container> | ||
<StyledResponseActions> | ||
<Button isPrimary isLowercase onClick={viewResultsCallback}> | ||
View More Results | ||
</Button> | ||
<Button isLowercase onClick={handleNewQuestion}> | ||
Ask Another Question | ||
</Button> | ||
</StyledResponseActions> | ||
<StyledResponseDisclaimer>{AI_DISCLAIMER}</StyledResponseDisclaimer> | ||
</Container> | ||
<ChatFeedback /> | ||
</> | ||
)} | ||
</> | ||
<Container> | ||
<section | ||
data-conversation-initial={searchTerm} | ||
data-conversation-length={conversation.length} | ||
data-conversation-ref={conversationRef} | ||
> | ||
{conversation.map((entry, index) => { | ||
return ( | ||
<ChatResponse | ||
conversationRef={conversationRef} | ||
key={index} | ||
question={entry.question} | ||
responseCallback={handleResponseCallback} | ||
/> | ||
); | ||
})} | ||
<ChatConversation | ||
conversationCallback={handleConversationCallback} | ||
isStreaming={isStreaming} | ||
/> | ||
{isStreaming && ( | ||
<> | ||
{/* <StyledResponseActions> | ||
<Button isPrimary isLowercase onClick={viewResultsCallback}> | ||
View More Results | ||
</Button> | ||
<Button isLowercase onClick={handleNewQuestion}> | ||
Ask Another Question | ||
</Button> | ||
</StyledResponseActions> | ||
<StyledResponseDisclaimer>{AI_DISCLAIMER}</StyledResponseDisclaimer> */} | ||
{/* <ChatFeedback /> */} | ||
</> | ||
)} | ||
</section> | ||
</Container> | ||
); | ||
}; | ||
|
||
export default React.memo(Chat); | ||
export default Chat; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import { styled } from "@/stitches.config"; | ||
import { useRef } from "react"; | ||
|
||
const textareaPlaceholder = "Ask a followup question..."; | ||
|
||
interface ChatConversationProps { | ||
conversationCallback: (message: string) => void; | ||
isStreaming?: boolean; | ||
} | ||
|
||
const ChatConversation: React.FC<ChatConversationProps> = ({ | ||
conversationCallback, | ||
isStreaming, | ||
}) => { | ||
const textareaRef = useRef<HTMLTextAreaElement>(null); | ||
const formRef = useRef<HTMLFormElement>(null); | ||
|
||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { | ||
e.preventDefault(); | ||
submitConversationCallback(); | ||
}; | ||
|
||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
if (isStreaming) return; | ||
|
||
if (e.key === "Enter" && !e.shiftKey) { | ||
e.preventDefault(); | ||
submitConversationCallback(); | ||
} | ||
}; | ||
|
||
const submitConversationCallback = () => { | ||
const value = textareaRef.current?.value; | ||
if (value) conversationCallback(value); | ||
|
||
/* Clear the textarea and unfocus it */ | ||
textareaRef.current!.value = ""; | ||
textareaRef.current!.blur(); | ||
}; | ||
|
||
const handleFocus = () => { | ||
const isFocused = String(textareaRef.current === document.activeElement); | ||
formRef.current!.dataset.isFocused = isFocused; | ||
}; | ||
|
||
return ( | ||
<StyledChatConversation> | ||
<form onSubmit={handleSubmit} ref={formRef} data-is-focused="false"> | ||
<textarea | ||
ref={textareaRef} | ||
onKeyDown={handleKeyDown} | ||
placeholder={textareaPlaceholder} | ||
onFocus={handleFocus} | ||
onBlur={handleFocus} | ||
></textarea> | ||
<button type="submit" disabled={isStreaming}> | ||
submit | ||
</button> | ||
</form> | ||
</StyledChatConversation> | ||
); | ||
}; | ||
|
||
const StyledChatConversation = styled("div", { | ||
position: "relative", | ||
zIndex: 0, | ||
|
||
form: { | ||
transition: "$dcAll", | ||
borderRadius: "3px", | ||
flexWrap: "wrap", | ||
overflow: "hidden", | ||
|
||
["&[data-is-focused=true]"]: { | ||
backgroundColor: "$white !important", | ||
boxShadow: "3px 3px 11px #0001", | ||
outline: "2px solid $purple60", | ||
}, | ||
|
||
["&[data-is-focused=false]"]: { | ||
backgroundColor: "#f0f0f0", | ||
boxShadow: "none", | ||
outline: "2px solid transparent", | ||
|
||
textarea: { | ||
color: "$black50", | ||
whiteSpace: "nowrap", | ||
overflow: "hidden", | ||
textOverflow: "ellipsis", | ||
}, | ||
}, | ||
}, | ||
|
||
textarea: { | ||
width: "100%", | ||
height: "100%", | ||
padding: "$gr3", | ||
border: "none", | ||
resize: "none", | ||
backgroundColor: "$gray6", | ||
fontSize: "$gr3", | ||
lineHeight: "147%", | ||
zIndex: "1", | ||
fontFamily: "$northwesternSansRegular", | ||
overflow: "hidden", | ||
outline: "none", | ||
transition: "$dcAll", | ||
boxSizing: "border-box", | ||
|
||
"&::placeholder": { | ||
overflow: "hidden", | ||
color: "$black50", | ||
textOverflow: "ellipsis", | ||
}, | ||
}, | ||
|
||
button: { | ||
position: "absolute", | ||
bottom: "$gr2", | ||
right: "$gr2", | ||
}, | ||
}); | ||
|
||
export default ChatConversation; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.