diff --git a/config/constants/development.js b/config/constants/development.js index 67b69f47..aa3c8e95 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -22,6 +22,7 @@ module.exports = { TC_FINANCE_API_URL: process.env.TC_FINANCE_API_URL || `${API_V6}/finance`, TC_AI_API_BASE_URL: process.env.TC_AI_API_BASE_URL || `${API_V6}/ai`, TC_AI_SKILLS_EXTRACTION_WORKFLOW_ID: process.env.TC_AI_SKILLS_EXTRACTION_WORKFLOW_ID || 'skillExtractionWorkflow', + TC_AI_AUTOWRITE_WORKFLOW_ID: process.env.TC_AI_AUTOWRITE_WORKFLOW_ID || 'jdAutowriteWorkflow', CHALLENGE_DEFAULT_REVIEWERS_URL: `${DEV_API_HOSTNAME}/v6/challenge/default-reviewers`, CHALLENGE_API_VERSION: '1.1.0', CHALLENGE_TIMELINE_TEMPLATES_URL: `${DEV_API_HOSTNAME}/v6/timeline-templates`, diff --git a/config/constants/local.js b/config/constants/local.js index c551b163..3048bf97 100644 --- a/config/constants/local.js +++ b/config/constants/local.js @@ -36,6 +36,7 @@ module.exports = { ENGAGEMENTS_ROOT_API_URL: `${LOCAL_CHALLENGE_API}/engagements`, APPLICATIONS_API_URL: `${LOCAL_CHALLENGE_API}/engagements/applications`, TC_FINANCE_API_URL: process.env.TC_FINANCE_API_URL || 'http://localhost:3009/v6/finance', + TC_AI_AUTOWRITE_WORKFLOW_ID: process.env.TC_AI_AUTOWRITE_WORKFLOW_ID || 'jdAutowriteWorkflow', CHALLENGE_DEFAULT_REVIEWERS_URL: `${LOCAL_CHALLENGE_API.replace(/\/v6$/, '')}/v6/challenge/default-reviewers`, CHALLENGE_API_VERSION: '1.1.0', CHALLENGE_TIMELINE_TEMPLATES_URL: `${LOCAL_CHALLENGE_API}/timeline-templates`, diff --git a/config/constants/production.js b/config/constants/production.js index 94c3e95e..70c7d52a 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -21,6 +21,7 @@ module.exports = { TC_FINANCE_API_URL: process.env.TC_FINANCE_API_URL || `${API_V6}/finance`, TC_AI_API_BASE_URL: process.env.TC_AI_API_BASE_URL || `${API_V6}/ai`, TC_AI_SKILLS_EXTRACTION_WORKFLOW_ID: process.env.TC_AI_SKILLS_EXTRACTION_WORKFLOW_ID || 'skillExtractionWorkflow', + TC_AI_AUTOWRITE_WORKFLOW_ID: process.env.TC_AI_AUTOWRITE_WORKFLOW_ID || 'jdAutowriteWorkflow', CHALLENGE_DEFAULT_REVIEWERS_URL: `${PROD_API_HOSTNAME}/v6/challenge/default-reviewers`, CHALLENGE_API_VERSION: '1.1.0', CHALLENGE_TIMELINE_TEMPLATES_URL: `${PROD_API_HOSTNAME}/v6/timeline-templates`, @@ -37,9 +38,9 @@ module.exports = { RESOURCE_ROLES_API_URL: `${PROD_API_HOSTNAME}/v6/resource-roles`, SUBMISSIONS_API_URL: `${PROD_API_HOSTNAME}/v6/submissions`, REVIEW_TYPE_API_URL: `${PROD_API_HOSTNAME}/v6/reviewTypes`, - REVIEWS_API_URL: `${PROD_API_HOSTNAME}/v6/reviews`, + REVIEWS_API_URL: `${PROD_API_HOSTNAME}/v6/reviews`, SCORECARDS_API_URL: `${PROD_API_HOSTNAME}/v6/scorecards`, - WORKFLOWS_API_URL: `${PROD_API_HOSTNAME}/v6/workflows`, + WORKFLOWS_API_URL: `${PROD_API_HOSTNAME}/v6/workflows`, SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`, STUDIO_URL: `https://studio.${DOMAIN}`, CONNECT_APP_URL: `https://connect.${DOMAIN}`, diff --git a/src/components/EngagementEditor/DescriptionField/index.js b/src/components/EngagementEditor/DescriptionField/index.js index c23b9ab6..fddf6f6c 100644 --- a/src/components/EngagementEditor/DescriptionField/index.js +++ b/src/components/EngagementEditor/DescriptionField/index.js @@ -433,6 +433,31 @@ class DescriptionField extends Component { } } + componentDidUpdate (prevProps) { + const { engagement, isGeneratingDescription } = this.props + + if (this.easyMDE) { + const cm = this.easyMDE.codemirror + + // Disable / enable editor + if (prevProps.isGeneratingDescription !== isGeneratingDescription) { + cm.setOption('readOnly', isGeneratingDescription ? 'nocursor' : false) + } + + const prevDescription = prevProps.engagement.description + const newDescription = engagement.description + + if (prevDescription !== newDescription) { + const editorValue = this.easyMDE.value() + + if (editorValue !== newDescription) { + cm.setValue(newDescription || '') + this.currentValue = newDescription + } + } + } + } + /** * Convert the first char to Uppercase * @param str @@ -883,6 +908,7 @@ DescriptionField.propTypes = { engagement: PropTypes.shape().isRequired, onUpdateDescription: PropTypes.func.isRequired, isPrivate: PropTypes.bool, - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + isGeneratingDescription: PropTypes.bool } export default DescriptionField diff --git a/src/components/EngagementEditor/EngagementEditor.module.scss b/src/components/EngagementEditor/EngagementEditor.module.scss index c5dc670f..929a9e29 100644 --- a/src/components/EngagementEditor/EngagementEditor.module.scss +++ b/src/components/EngagementEditor/EngagementEditor.module.scss @@ -69,6 +69,8 @@ margin-bottom: 0; padding-top: 0; align-items: flex-start; + display: flex; + flex-direction: column; } &.col2 { @@ -112,9 +114,17 @@ display: flex; align-items: center; - span { + .required { color: $tc-red; } + + .aiButtonRow { + width: 100%; + } + + .aiRewriteButton { + margin-top: 10px; + } } &.col2 { diff --git a/src/components/EngagementEditor/index.js b/src/components/EngagementEditor/index.js index 44515c62..ab359cc1 100644 --- a/src/components/EngagementEditor/index.js +++ b/src/components/EngagementEditor/index.js @@ -17,6 +17,8 @@ import { suggestProfiles } from '../../services/user' import { getCountableAssignments } from '../../util/engagements' import { serializeTentativeAssignmentDate } from '../../util/assignmentDates' import { formatTimeZoneLabel, formatTimeZoneList } from '../../util/timezones' +import { autowriteDescription } from '../../services/workflowAI' +import { toastSuccess, toastFailure } from '../../util/toaster' import styles from './EngagementEditor.module.scss' const ANY_OPTION = { label: 'Any', value: 'Any' } @@ -131,6 +133,7 @@ const EngagementEditor = ({ const [assignRate, setAssignRate] = useState('') const [assignOtherRemarks, setAssignOtherRemarks] = useState('') const [assignErrors, setAssignErrors] = useState({}) + const [isGeneratingDescription, setIsGeneratingDescription] = useState(false) const { timeZoneOptions, timeZoneOptionByZone } = useMemo(() => { const optionByLabel = new Map() moment.tz.names().forEach((zone) => { @@ -396,6 +399,39 @@ const EngagementEditor = ({ resetAssignState() } + const handleAIAutowrite = async () => { + if (isGeneratingDescription) return + + setIsGeneratingDescription(true) + + try { + const input = engagement.description + const result = await autowriteDescription(input) + + const generatedDescription = result.formattedDescription + + if (!generatedDescription) { + throw new Error('No formattedDescription returned') + } + + onUpdateDescription(generatedDescription) + + toastSuccess( + 'Description Generated', + 'AI generated description has been added.' + ) + } catch (error) { + console.error('AI autowrite error:', error) + + toastFailure( + 'Error', + 'Failed to generate description. Please try again.' + ) + } finally { + setIsGeneratingDescription(false) + } + } + const handleAssignSubmit = () => { if (!assignModal) { return @@ -605,7 +641,7 @@ const EngagementEditor = ({