Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@
- **Community Interest Targeting**: Community involvement question now only shown to users who provide contact information
- **Anonymous Feedback Improvements**: Community interest field hidden from summary for anonymous submissions
- **Focus Management**: Fixed keyboard focus jumping to previous questions; now properly targets current question only
- **Q&A Response System**: Replaced unreliable `streamMessage` with direct API calls using `injectMessage` for consistent response display

### Fixed
- **Critical Q&A Display Issue**: Fixed problem where Q&A responses stopped displaying after 3-4 questions due to react-chatbotify's `streamMessage` accumulating state
- **Query ID Generation**: Implemented UUID-based query ID generation directly in QA flow to ensure proper feedback tracking
- **Response Reliability**: Q&A responses now display consistently regardless of conversation length
- Resolved form state persistence issues where previous session data would interfere with new submissions
- Fixed multiple checkbox selection display in feedback summary (was showing only one selection)
- Corrected timing issues with form context updates using chatState.userInput pattern
- Fixed anonymous feedback showing previous contact information in summary
- Improved keyboard accessibility for checkboxes with proper focus management
- Removed broken fallback loop flow that used deprecated `streamMessage` approach

## [2.0.0-rc.1] - 2025-05-28

Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ The bot supports several conversation flows:
- Built-in feedback system with thumbs up/down options after each response
- Automatic feedback tracking and analytics
- Users can provide feedback or continue asking questions immediately
- Negative feedback offers direct path to support ticket creation
- Requires user to be logged in

### 🎫 Support Ticket Flows
Expand Down Expand Up @@ -238,7 +237,7 @@ REACT_APP_API_KEY=your-api-key-here

The bot integrates with the ACCESS Q&A API and JSM (Jira Service Management) for ticket creation. Configure your backend endpoints to handle:

- Q&A queries with streaming responses
- Q&A queries with JSON responses
- Support ticket creation with ProForma field mapping
- File upload processing
- User authentication and session management
Expand Down Expand Up @@ -406,4 +405,4 @@ npm install @snf/access-qa-bot@beta
#### 📚 Documentation
- Updated README with comprehensive feature list and integration examples
- Added detailed API documentation and configuration options
- Included changelog for version tracking
- Included changelog for version tracking
11 changes: 2 additions & 9 deletions src/components/QABot.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { v4 as uuidv4 } from 'uuid';
import BotController from './BotController';
import useThemeColors from '../hooks/useThemeColors';
import useChatBotSettings from '../hooks/useChatBotSettings';
import useHandleAIQuery from '../hooks/useHandleAIQuery';
import useUpdateHeader from '../hooks/useUpdateHeader';
import useRingEffect from '../hooks/useRingEffect';
import useFocusableSendButton from '../hooks/useFocusableSendButton';
Expand Down Expand Up @@ -55,12 +54,11 @@ const QABotInternal = React.forwardRef((props, botRef) => {
accessId
} = props;

const finalApiKey = apiKey || ((typeof process !== 'undefined' && process.env) ? process.env.REACT_APP_API_KEY : null);
const finalApiKey = apiKey || process.env.REACT_APP_API_KEY || null;

const [isBotLoggedIn, setIsBotLoggedIn] = useState(isLoggedIn !== undefined ? isLoggedIn : false);
const sessionIdRef = useRef(getOrCreateSessionId());
const sessionId = sessionIdRef.current;
const [currentQueryId, setCurrentQueryId] = useState(null);

// Use Form Context instead of local state
const { ticketForm, feedbackForm, updateTicketForm, updateFeedbackForm, resetTicketForm, resetFeedbackForm } = useFormContext();
Expand All @@ -72,8 +70,6 @@ const QABotInternal = React.forwardRef((props, botRef) => {
}
}, [isLoggedIn]);

// Initialize currentQueryId as null - will be set by useHandleAIQuery
// when actual queries are processed

// Listen for chat window toggle events from react-chatbotify
useEffect(() => {
Expand Down Expand Up @@ -105,7 +101,6 @@ const QABotInternal = React.forwardRef((props, botRef) => {
loginUrl
});

const handleQuery = useHandleAIQuery(finalApiKey, sessionId, setCurrentQueryId);

const formContext = useMemo(() => ({
ticketForm: ticketForm || {},
Expand All @@ -120,9 +115,7 @@ const QABotInternal = React.forwardRef((props, botRef) => {
welcomeMessage,
isBotLoggedIn,
loginUrl,
handleQuery,
sessionId,
currentQueryId,
ticketForm,
setTicketForm: updateTicketForm,
feedbackForm,
Expand All @@ -134,7 +127,7 @@ const QABotInternal = React.forwardRef((props, botRef) => {
name: userName || null,
accessId: accessId || null
}
}), [welcomeMessage, isBotLoggedIn, loginUrl, handleQuery, sessionId, currentQueryId, ticketForm, feedbackForm, updateTicketForm, updateFeedbackForm, formContext, finalApiKey, userEmail, userName, accessId]);
}), [welcomeMessage, isBotLoggedIn, loginUrl, sessionId, ticketForm, feedbackForm, updateTicketForm, updateFeedbackForm, formContext, finalApiKey, userEmail, userName, accessId]);

useUpdateHeader(isBotLoggedIn, containerRef);
useRingEffect(ringEffect, containerRef);
Expand Down
20 changes: 17 additions & 3 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export const DEFAULT_CONFIG = {
WELCOME_MESSAGE_LOGGED_OUT: 'To ask questions, please log in.',
WELCOME_MESSAGE_LOGIN_TRANSITION: 'Welcome! You are now logged in. What can I help you with?',
WELCOME_MESSAGE_LOGOUT_TRANSITION: 'You have been logged out.',
API_ENDPOINT: 'https://access-ai.ccs.uky.edu/api/query',
//API_ENDPOINT: 'https://access-ai.ccs.uky.edu:543/api/query',
API_ENDPOINT: 'https://access-ai-grace1-external.ccs.uky.edu/access/chat/api/',
RATING_ENDPOINT: 'https://access-ai-grace1-external.ccs.uky.edu/access/chat/rating/',

// Netlify function URL - this should point to the Netlify functions endpoint for ticket submission
// NOT the Q&A API endpoint
Expand All @@ -32,6 +32,19 @@ export const DEFAULT_CONFIG = {
}
};

// Runtime functions to get endpoints (handles env vars that may not be available at build time)
export const getApiEndpoint = () => {
return (typeof process !== 'undefined' && process.env?.REACT_APP_API_ENDPOINT)
? process.env.REACT_APP_API_ENDPOINT
: DEFAULT_CONFIG.API_ENDPOINT;
};

export const getRatingEndpoint = () => {
return (typeof process !== 'undefined' && process.env?.REACT_APP_RATING_ENDPOINT)
? process.env.REACT_APP_RATING_ENDPOINT
: DEFAULT_CONFIG.RATING_ENDPOINT;
};

// Helper functions from strings.js
export const buildWelcomeMessage = (isLoggedIn, welcomeMessage) => {
if (isLoggedIn) {
Expand All @@ -44,5 +57,6 @@ export const buildWelcomeMessage = (isLoggedIn, welcomeMessage) => {

export const getApiKey = (providedApiKey) => {
// Return provided API key if available, otherwise fall back to environment variable
return providedApiKey || ((typeof process !== 'undefined' && process.env) ? process.env.REACT_APP_API_KEY : null);
// Simplified for better Netlify compatibility
return providedApiKey || process.env.REACT_APP_API_KEY || null;
};
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function ExampleApp() {
open={chatOpen}
onOpenChange={setChatOpen}
loginUrl="/login"
apiKey={(typeof process !== 'undefined' && process.env) ? process.env.REACT_APP_API_KEY : null}
apiKey={process.env.REACT_APP_API_KEY || null}
welcome="What can I help you with?"
userEmail={email || undefined}
userName={name || undefined}
Expand Down
17 changes: 1 addition & 16 deletions src/utils/create-bot-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ function createBotFlow({
welcomeMessage,
isBotLoggedIn,
loginUrl,
handleQuery,
hasQueryError,
sessionId,
currentQueryId,
ticketForm = {},
setTicketForm = () => {},
// feedbackForm = {},
Expand All @@ -38,9 +35,7 @@ function createBotFlow({
// Create Q&A flow (requires login)
const qaFlow = isBotLoggedIn
? createQAFlow({
fetchAndStreamResponse: handleQuery,
sessionId,
currentQueryId,
apiKey
})
: {
Expand Down Expand Up @@ -84,17 +79,7 @@ function createBotFlow({
...(qaFlow || {}),
...(ticketFlow || {}),
//...(feedbackFlow || {}), // TODO: add feedback flow back in
...(securityFlow || {}),
// Add fallback loop for errors (only if logged in)
...(isBotLoggedIn && {
loop: {
message: async (params) => {
await handleQuery(params);
},
renderMarkdown: ["BOT"],
path: () => hasQueryError ? 'start' : 'loop'
}
})
...(securityFlow || {})
};

return flow;
Expand Down
108 changes: 61 additions & 47 deletions src/utils/flows/qa-flow.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,99 @@
import { handleBotError } from '../error-handler';
import { DEFAULT_CONFIG } from '../../config/constants';
import { getApiEndpoint, getRatingEndpoint } from '../../config/constants';
import { v4 as uuidv4 } from 'uuid';

/**
* Creates the Q&A conversation flow
*
* @param {Object} params Configuration
* @param {Function} params.fetchAndStreamResponse Function to fetch and stream responses
* @param {string} params.sessionId Current session ID
* @param {string} params.currentQueryId Current query ID
* @param {string} params.apiKey API key for requests
* @returns {Object} Q&A flow configuration
*/
export const createQAFlow = ({ fetchAndStreamResponse, sessionId, currentQueryId, apiKey }) => {
export const createQAFlow = ({ sessionId, apiKey }) => {
// Track the query ID for the most recent response that can receive feedback
let feedbackQueryId = null;

return {
go_ahead_and_ask: {
message: "Please type your question.",
path: "qa_loop"
},
qa_loop: {
message: async (chatState) => {
try {
await fetchAndStreamResponse(chatState);
return "Was this helpful?";
} catch (error) {
console.error('Error in bot flow:', error);
return handleBotError(error);
}
},
renderMarkdown: ["BOT"],
options: ["👍 Yes", "👎 No"],
chatDisabled: false,
function: async (chatState) => {
if (chatState.userInput === "👍 Yes" || chatState.userInput === "👎 No") {
if (apiKey && sessionId) {
const isPositive = chatState.userInput === "👍 Yes";
const { userInput } = chatState;

// Handle feedback first if it's feedback
if (userInput === "👍 Helpful" || userInput === "👎 Not helpful") {

// Send feedback using the captured query ID
if (apiKey && sessionId && feedbackQueryId) {
const isPositive = userInput === "👍 Helpful";
const headers = {
'Content-Type': 'application/json',
'X-Origin': 'access',
'X-API-KEY': apiKey,
'X-Session-ID': sessionId,
'X-Query-ID': currentQueryId,
'X-Query-ID': feedbackQueryId,
'X-Feedback': isPositive ? 1 : 0
};

const endpoint = getRatingEndpoint();

try {
await fetch(`${DEFAULT_CONFIG.API_ENDPOINT}/rating`, {
await fetch(endpoint, {
method: 'POST',
headers
});
} catch (error) {
console.error('Error sending feedback:', error);
}
}
return "Thanks for the feedback! Feel free to ask another question.";
} else {
// Process as a question - fetch response directly
try {
// Generate our own query ID since we're bypassing useHandleAIQuery
const queryId = uuidv4();
feedbackQueryId = queryId;

const headers = {
'Content-Type': 'application/json',
'X-Origin': 'access',
'X-API-KEY': apiKey,
'X-Session-ID': sessionId,
'X-Query-ID': queryId
};

const response = await fetch(getApiEndpoint(), {
method: 'POST',
headers,
body: JSON.stringify({
query: userInput
})
});

const body = await response.json();
const text = body.response;

// Inject the response
await chatState.injectMessage(text);
return null;
} catch (error) {
console.error('Error in bot flow:', error);
return handleBotError(error);
}
}
},
path: (chatState) => {
if (chatState.userInput === "👍 Yes") {
return "qa_positive_feedback";
} else if (chatState.userInput === "👎 No") {
return "qa_negative_feedback";
}
return "qa_loop";
}
},
qa_positive_feedback: {
message: "Thank you for your feedback! It helps us improve this tool.",
transition: { duration: 1000 },
path: "go_ahead_and_ask"
},
qa_negative_feedback: {
message: "Sorry that wasn't useful. Would you like to open a help ticket for assistance?",
options: ["Open a help ticket", "Ask another question"],
chatDisabled: true,
path: (chatState) => {
if (chatState.userInput === "Open a help ticket") {
return "help_ticket";
renderMarkdown: ["BOT"],
options: (chatState) => {
// Only show feedback options if the input isn't already feedback
if (chatState.userInput === "👍 Helpful" || chatState.userInput === "👎 Not helpful") {
return []; // No options after feedback is given
}
return "qa_continue";
}
},
qa_continue: {
message: "Ask another question, but remember that each question must stand alone.",
return ["👍 Helpful", "👎 Not helpful"];
},
chatDisabled: false,
path: "qa_loop"
}
};
Expand Down