Skip to content
Merged
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
7,055 changes: 4,155 additions & 2,900 deletions frontend/package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
],
"scripts": {
"build": "make build",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"test": "fedx-scripts jest --coverage --passWithNoTests"
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
"test": "fedx-scripts jest --coverage --passWithNoTests",
"types": "tsc --noEmit"
},
"author": "Open edX Community",
"license": "Apache-2.0",
Expand All @@ -30,13 +31,15 @@
"devDependencies": {
"@edx/browserslist-config": "1.1.1",
"@openedx/frontend-build": "14.6.0",
"@edx/typescript-config": "^1.0.1",
"@types/react": "^18",
"@types/react-dom": "^18",
"@openedx/paragon": "23.14.0",
"jest": "29.7.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"dependencies": {
"prop-types": "^15.8.1",
"react-markdown": "^8.0.0 || ^9.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {
useState, useEffect, useRef, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { logError } from '@edx/frontend-platform/logging';
import { Spinner, Alert } from '@openedx/paragon';

// Import services
Expand All @@ -22,6 +22,8 @@ import {
AIEducatorLibraryAssistComponent,
AIEducatorLibraryResponseComponent,
} from './components';
import { PluginConfiguration } from './types';
import { WORKFLOW_ACTIONS } from './constants';

/**
* Component Registry
Expand All @@ -42,16 +44,23 @@ const COMPONENT_REGISTRY = {
* AIRequestComponent and AIResponseComponent with configuration.
* Manages state and orchestrates the AI interaction flow.
*/

interface ConfigurableAIAssistanceProps {
fallbackConfig?: PluginConfiguration | null;
onConfigLoad?: (config: PluginConfiguration) => void;
onConfigError?: (error) => void;
}

const ConfigurableAIAssistance = ({
fallbackConfig,
fallbackConfig = null,
onConfigLoad,
onConfigError,
...additionalProps
}) => {
}: ConfigurableAIAssistanceProps) => {
// Configuration state
const [isLoadingConfig, setIsLoadingConfig] = useState(true);
const [configError, setConfigError] = useState(null);
const [config, setConfig] = useState(null);
const [configError, setConfigError] = useState<null | string>(null);
const [config, setConfig] = useState<PluginConfiguration | null>(null);

// AI interaction state
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -86,22 +95,25 @@ const ConfigurableAIAssistance = ({
if (currentRequestId === requestIdRef.current) {
setConfig(fetchedConfig);

if (onConfigLoad) {
if (onConfigLoad && fetchedConfig) {
onConfigLoad(fetchedConfig);
}
}
} catch (err) {
// Type guard for error
const configErr = err instanceof Error ? err : new Error(String(err));

// Ignore aborted requests
if (err.name === 'AbortError' || err.message?.includes('aborted')) {
if (configErr.name === 'AbortError' || configErr.message?.includes('aborted')) {
return;
}

// Only update state if this is still the latest request
if (currentRequestId === requestIdRef.current) {
// eslint-disable-next-line no-console
console.error('[ConfigurableAIAssistance] Configuration error:', err);
console.error('[ConfigurableAIAssistance] Configuration error:', configErr);

setConfigError(err.message);
setConfigError(configErr.message);

if (fallbackConfig) {
setConfig(fallbackConfig);
Expand Down Expand Up @@ -141,17 +153,17 @@ const ConfigurableAIAssistance = ({
...additionalProps,
});

const requestMessage = config?.config?.customMessage
|| config?.config?.requestMessage
const requestMessage = config?.request.config?.customMessage
|| config?.request.config?.requestMessage
|| null;

let buffer = '';
// Make API call
const data = await callWorkflowService({
context: contextData,
action: 'run',
userInput: requestMessage,
payload: {
action: WORKFLOW_ACTIONS.RUN,
requestId: `ai-request-${Date.now()}`,
},
onStreamChunk: (chunk) => {
Expand Down Expand Up @@ -183,8 +195,7 @@ const ConfigurableAIAssistance = ({

setHasAsked(true);
} catch (err) {
// eslint-disable-next-line no-console
console.error('[ConfigurableAIAssistance] AI Assistant Error:', err);
logError('[ConfigurableAIAssistance] AI Assistant Error:', err);
const userFriendlyError = formatErrorMessage(err);
setError(userFriendlyError);
} finally {
Expand Down Expand Up @@ -331,19 +342,4 @@ const ConfigurableAIAssistance = ({
return null;
};

ConfigurableAIAssistance.propTypes = {
fallbackConfig: PropTypes.shape({
component: PropTypes.string.isRequired,
config: PropTypes.shape({}),
}),
onConfigLoad: PropTypes.func,
onConfigError: PropTypes.func,
};

ConfigurableAIAssistance.defaultProps = {
fallbackConfig: null,
onConfigLoad: null,
onConfigError: null,
};

export default ConfigurableAIAssistance;
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';

// Import service modules
import {
Expand All @@ -13,22 +12,28 @@ import {
AIRequestComponent,
AIResponseComponent,
} from './components';
import { WORKFLOW_ACTIONS } from './constants';

interface GetAIAssistanceButtonProps {
requestMessage?: string;
buttonText?: string;
}

/**
* Main AI Assistant Plugin Component
* Orchestrates the AI assistance flow using modular components
*/
const GetAIAssistanceButton = ({
requestMessage,
buttonText,
requestMessage = 'Need help understanding this content?',
buttonText = 'Get AI Assistance',
...props
}) => {
}: GetAIAssistanceButtonProps) => {
// Core state management
const [isLoading, setIsLoading] = useState(false);
const [response, setResponse] = useState('');
const [error, setError] = useState('');
const [hasAsked, setHasAsked] = useState(false);
const [requestId, setRequestId] = useState(null);
const [requestId, setRequestId] = useState<string | null>(null);

/**
* Handle AI assistant request
Expand All @@ -51,9 +56,9 @@ const GetAIAssistanceButton = ({
// Make API call with flexible parameters
const data = await callWorkflowService({
context: contextData,
action: 'simple_button_assistance',
userInput: requestMessage || 'Provide learning assistance for this content',
payload: {
action: WORKFLOW_ACTIONS.SIMPLE_BUTTON_ASSISTANCE,
requestId: `ai-request-${Date.now()}`,
},
onStreamChunk: (chunk) => {
Expand Down Expand Up @@ -141,7 +146,6 @@ const GetAIAssistanceButton = ({
response={response}
error={error}
isLoading={isLoading}
onAskAgain={handleAskAI}
onClear={handleReset}
onError={handleClearError}
/>
Expand All @@ -161,14 +165,4 @@ const GetAIAssistanceButton = ({
);
};

GetAIAssistanceButton.propTypes = {
requestMessage: PropTypes.string,
buttonText: PropTypes.string,
};

GetAIAssistanceButton.defaultProps = {
requestMessage: 'Need help understanding this content?',
buttonText: 'Get AI Assistance',
};

export default GetAIAssistanceButton;
Loading