Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6dfc38c
Fix imports and version pins
JeffersonBledsoe Aug 1, 2025
edf6b7f
fix intermitent error
djay Jul 31, 2025
de9bc4d
Sidebar UI prototype
JeffersonBledsoe Aug 1, 2025
3c6fd76
Add 'Show in sidebar' toggle
JeffersonBledsoe Aug 1, 2025
2cf33b0
Move sidebar rendering to it's own component
JeffersonBledsoe Aug 1, 2025
2502e6e
Remove floating button config
JeffersonBledsoe Aug 1, 2025
f16efc8
Rename 'showInSidebar' to 'globalMode'
JeffersonBledsoe Aug 1, 2025
8428f92
Render block as a button if we're in global mode
JeffersonBledsoe Aug 1, 2025
4dada5b
Fields tidy up
JeffersonBledsoe Aug 5, 2025
fed8c80
Load chat that corresponds to the clicked block
JeffersonBledsoe Aug 5, 2025
c87c208
Ensure sidebar is always rendered
JeffersonBledsoe Aug 6, 2025
1977ee2
Rename field
JeffersonBledsoe Aug 6, 2025
6104d2a
Add server wake on clicking into form for the first time
JeffersonBledsoe Aug 7, 2025
af2b66a
Send wake on user input after a period of time
JeffersonBledsoe Aug 7, 2025
c201fd2
Remove logging
JeffersonBledsoe Aug 7, 2025
6049057
Some improved wake handling
JeffersonBledsoe Aug 27, 2025
fce8662
Refactor wake and state handling so it can be more tightly integrated
JeffersonBledsoe Sep 2, 2025
de235b2
Cleanup signal handling
JeffersonBledsoe Sep 2, 2025
563a893
Fix not using the correct agent
JeffersonBledsoe Sep 4, 2025
39aab16
Fix being unable to do follow-up messages
JeffersonBledsoe Sep 4, 2025
7ee9787
Throw stream error (for debugging)
JeffersonBledsoe Sep 4, 2025
d97e344
Fix loader not showing up sometimes when asleep
JeffersonBledsoe Sep 5, 2025
eb2afd5
Ensure the chatId is updated when switching between multiple chats
JeffersonBledsoe Sep 10, 2025
fc48ee3
Improve chat clearing (maybe fix it?)
JeffersonBledsoe Sep 10, 2025
bbd4671
Don't show loading indicator when trying to wake
JeffersonBledsoe Sep 10, 2025
83a4e53
Rework completeMessageDetail to avoid stale chat state
JeffersonBledsoe Sep 11, 2025
32343db
docs
JeffersonBledsoe Sep 11, 2025
8aa0d8e
Better error message
JeffersonBledsoe Sep 11, 2025
dc7ce00
Initial version of waiting before healthy before starting submission
JeffersonBledsoe Sep 11, 2025
3a693c7
Initial version of timeout while waiting for next packet
JeffersonBledsoe Sep 12, 2025
d24e49b
Fix initial waking state
JeffersonBledsoe Sep 12, 2025
e72dd41
Fix stale state in wake function
JeffersonBledsoe Sep 15, 2025
feddc74
Try to avoid getting in a state where we're submitting but the chat s…
JeffersonBledsoe Sep 23, 2025
e4df389
Remove sidebar code that is part of https://github.com/eea/volto-chat…
JeffersonBledsoe Sep 24, 2025
ac88a97
Fix stale wake chat state
JeffersonBledsoe Sep 24, 2025
d1f61b3
Reset error state when resetting chat
JeffersonBledsoe Sep 24, 2025
e40dd24
Try to improve error handling
JeffersonBledsoe Sep 24, 2025
4922414
Fix chat being cleared to ready on startup
JeffersonBledsoe Sep 24, 2025
f00bd33
Try to catch some edge cases of waiting for wake/ waking that cause a…
JeffersonBledsoe Sep 25, 2025
d59a489
Fix warning timeout causing an exception and bump timeout time
JeffersonBledsoe Sep 26, 2025
c9c06a6
Fix submit being available while streaming
JeffersonBledsoe Oct 3, 2025
d0de535
Simplify wake call
JeffersonBledsoe Oct 3, 2025
a0eae04
Basic error handling for create chat session
JeffersonBledsoe Oct 3, 2025
ef32d34
Fix sleep timeout not triggering on follow-up questions
JeffersonBledsoe Oct 3, 2025
c003abb
Don't send second wake if we don't need to
JeffersonBledsoe Oct 3, 2025
104a4f9
Change warning/ error timeouts to 60 seconds and 120 seconds respecti…
JeffersonBledsoe Oct 10, 2025
b3dfe88
Disable warning
JeffersonBledsoe Oct 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"dependencies": {
"@eeacms/volto-matomo": "6.0.0",
"@microsoft/fetch-event-source": "2.0.1",
"dequal": "2.0.3",
"fast-json-patch": "3.1.1",
"highlight.js": "11.10.0",
"luxon": "3.5.0",
Expand All @@ -83,6 +84,10 @@
"react-textarea-autosize": "^8.5.3",
"rehype-prism-plus": "1.6.0",
"remark-gfm": "3.0.1",
"unist-util-visit": "5.0.0",
"uuid": "10.0.0"
},
"peerDependencies": {
"@plone/volto": ">18.0.0 < 19.0.0"
}
}
23 changes: 19 additions & 4 deletions src/ChatBlock/AutoResizeTextarea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@ import { SVGIcon } from './utils';
import SendIcon from './../icons/send.svg';

export default React.forwardRef(function AutoResizeTextarea(props, ref) {
const { onSubmit, isStreaming, enableMatomoTracking, persona, ...rest } =
props;
const {
onSubmit,
onFocus,
onChange = () => {},
disableSubmit,
enableMatomoTracking,
persona,
...rest
} = props;
const [input, setInput] = React.useState('');

const handleSubmit = (e) => {
e.preventDefault();
if (disableSubmit) {
return
}
const trimmedInput = input.trim();
if (trimmedInput) {
if (enableMatomoTracking) {
Expand All @@ -30,8 +40,13 @@ export default React.forwardRef(function AutoResizeTextarea(props, ref) {
return (
<>
<TextareaAutosize
aria-describedby="chat-wake-error-message"
value={input}
onChange={(e) => setInput(e.target.value)}
onFocus={onFocus}
onChange={(e) => {
onChange(e);
setInput(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
handleSubmit(e);
Expand All @@ -46,7 +61,7 @@ export default React.forwardRef(function AutoResizeTextarea(props, ref) {

<Button
className="submit-btn"
disabled={isStreaming}
disabled={disableSubmit}
type="submit"
aria-label="Send"
onKeyDown={(e) => {
Expand Down
2 changes: 1 addition & 1 deletion src/ChatBlock/ChatMessageBubble.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import visit from 'unist-util-visit';
import { visit } from 'unist-util-visit';
import loadable from '@loadable/component';
import { Button, Message, MessageContent } from 'semantic-ui-react';
import { trackEvent } from '@eeacms/volto-matomo/utils';
Expand Down
40 changes: 31 additions & 9 deletions src/ChatBlock/ChatWindow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { trackEvent } from '@eeacms/volto-matomo/utils';
import AutoResizeTextarea from './AutoResizeTextarea';
import { ChatMessageBubble } from './ChatMessageBubble';
import EmptyState from './EmptyState';
import { useScrollonStream } from './lib';
import { useBackendChat } from './useBackendChat';
import { useScrollonStream } from "./lib";
import { useBackendChat, ChatState } from './useBackendChat';
import { SVGIcon } from './utils';
import PenIcon from './../icons/square-pen.svg';

Expand Down Expand Up @@ -41,19 +41,23 @@ function ChatWindow({
enableMatomoTracking,
} = data;
const [qualityCheckEnabled, setQualityCheckEnabled] = React.useState(true);
const libs = { rehypePrism, remarkGfm }; // rehypePrism, remarkGfm
const libs = {rehypePrism, remarkGfm}; // rehypePrism, remarkGfm
const abortController = React.useRef(new AbortController());
const {
onSubmit,
messages,
isStreaming,
isFetchingRelatedQuestions,
chatState,
clearChat,
error,
wake,
} = useBackendChat({
persona,
qgenAsistantId,
enableQgen,
signal: abortController.current.signal,
});
const [showLandingPage, setShowLandingPage] = React.useState(false);
const isStreaming = chatState === ChatState.STREAMING;

const textareaRef = React.useRef(null);
const conversationRef = React.useRef(null);
Expand Down Expand Up @@ -151,7 +155,7 @@ function ChatWindow({
enableShowTotalFailMessage={enableShowTotalFailMessage}
totalFailMessage={totalFailMessage}
showToolCalls={showToolCalls}
isFetchingRelatedQuestions={isFetchingRelatedQuestions}
isFetchingRelatedQuestions={chatState === ChatState.FETCHING_RELATED}
enableMatomoTracking={enableMatomoTracking}
persona={persona}
/>
Expand All @@ -160,25 +164,43 @@ function ChatWindow({
</div>
</>
)}
{isStreaming && !isFetchingRelatedQuestions && (
{/* TODO: Only show this if it's taking a while to prevent flashing. Could cause WCAG SC 2.3.1 failure. */}
{[ChatState.STREAMING, ChatState.SUBMITTING].includes(chatState) && (
<div className="loader" />
)}
</div>

<div className="chat-form">
<Form>
{error ? (
<p
id="chat-wake-error-message"
aria-live="polite"
className="ui red basic label form-error-label"
>
{error}
</p>
) : null}
<div className="textarea-wrapper">
<AutoResizeTextarea
maxRows={8}
minRows={1}
ref={textareaRef}
placeholder={
messages.length > 0 ? 'Ask follow-up...' : placeholderPrompt
messages.length > 0 ? "Ask follow-up..." : placeholderPrompt
}
isStreaming={isStreaming}
disableSubmit={[ChatState.STREAMING].includes(
chatState,
)}
enableMatomoTracking={enableMatomoTracking}
persona={persona}
onSubmit={onSubmit}
onFocus={() => {
wake(chatState);
}}
onChange={() => {
wake(chatState);
}}
/>
</div>
</Form>
Expand Down
85 changes: 74 additions & 11 deletions src/ChatBlock/lib.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,68 @@
import { useRef, useEffect } from 'react';

import config from "@plone/registry";

export const delay = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};

// GET Request to DANSWER_URL/api/health to wake it up before starting.
// Will retry 3 times over 90 seconds and throw if no response received or unhealthy status is returned.
export async function wakeApi() {
// let timeout = 15000;
//
// function fetchWithRetry(url, retries, abortController) {
// return fetch(url, {
// signal: abortController,
// headers: {
// "Content-Type": "application/json",
// },
// }).catch((error) => {
// if ("connection" in navigator && navigator.connection.saveData === true) {
// throw error;
// }
// if (retries > 0 && error.message !== "Request timed out") {
// // Add 5 seconds to the timeout each retry
// timeout += 5000;
// return fetchWithRetry(url, retries - 1, AbortSignal.timeout(timeout));
// } else {
// throw error;
// }
// });
// }
let healthResponse = undefined;
const healthCheckUrl = config.settings["volto-chatbot"].rewakeUrl;
try {
healthResponse = await fetch(healthCheckUrl, {
signal: AbortSignal.timeout(60000),
headers: {
"Content-Type": "application/json",
},
});
// healthResponse = await fetchWithRetry(
// healthCheckUrl,
// 3,
// AbortSignal.timeout(timeout),
// );
} catch (err) {
// Timeout reached
if (err.name === "TimeoutError") {
throw Error("Failed to start a chat session at this time. Please try again later");
}
// Fetch request cancelled
else if (err.name === "AbortError") {
throw Error(`Chat session startup cancelled. Reason: ${err.message}`)
} else {
throw Error(`Unknown Error: type: ${err.name}, message: ${err.message}`)
}
}
if (!healthResponse?.ok) {
return false;
}

return true;
}

export async function createChatSession(personaId, description) {
const createChatSessionResponse = await fetch(
'/_da/chat/create-chat-session',
Expand Down Expand Up @@ -145,6 +204,11 @@ export function buildLatestMessageChain(messageMap) {
return finalMessageList; // .concat(additionalMessagesOnMainline);
}

/**
*
* @param {Object} options
* @param {AbortSignal} signal
*/
export async function* sendMessage({
message,
fileDescriptors,
Expand All @@ -161,6 +225,7 @@ export async function* sendMessage({
systemPromptOverride,
useExistingUserMessage,
alternateAssistantId,
signal,
}) {
const documentsAreSelected =
selectedDocumentIds && selectedDocumentIds.length > 0;
Expand All @@ -170,6 +235,7 @@ export async function* sendMessage({
headers: {
'Content-Type': 'application/json',
},
signal,
body: JSON.stringify({
alternate_assistant_id: alternateAssistantId,
chat_session_id: chatSessionId,
Expand Down Expand Up @@ -310,7 +376,7 @@ export class CurrentMessageFIFO {
}
}

export async function fetchRelatedQuestions(message, qgenAsistantId) {
export async function fetchRelatedQuestions(message, { qgenAsistantId, signal }) {
const { query, answer } = message;
const chatSessionId = await createChatSession(qgenAsistantId, `Q: ${query}`);

Expand All @@ -323,6 +389,7 @@ export async function fetchRelatedQuestions(message, qgenAsistantId) {
promptId: 0,
filters: {},
selectedDocumentIds: [],
signal: signal,
};
const promise = updateCurrentMessageFIFO(params, {}, () => {});

Expand Down Expand Up @@ -358,23 +425,19 @@ export async function fetchRelatedQuestions(message, qgenAsistantId) {
return result;
}

export async function* updateCurrentMessageFIFO(
params,
isCancelledRef,
setIsCancelled,
) {
/**
*
* @param {Object} params
* @param {AbortSignal} params.signal
*/
export async function* updateCurrentMessageFIFO(params) {
const promise = sendMessage(params);

try {
for await (const packetBunch of promise) {
for (const packet of packetBunch) {
yield { packet };
}

if (isCancelledRef.current) {
setIsCancelled(false);
break;
}
}
} catch (error) {
yield { error: String(error) };
Expand Down
Loading