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
1 change: 1 addition & 0 deletions config/constants/development.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add this also to production config.

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`,
Expand Down
1 change: 1 addition & 0 deletions config/constants/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Comment thread
himaniraghav3 marked this conversation as resolved.
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`,
Expand Down
5 changes: 3 additions & 2 deletions config/constants/production.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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}`,
Expand Down
28 changes: 27 additions & 1 deletion src/components/EngagementEditor/DescriptionField/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
himaniraghav3 marked this conversation as resolved.
}

const prevDescription = prevProps.engagement.description
const newDescription = engagement.description

if (prevDescription !== newDescription) {
const editorValue = this.easyMDE.value()

if (editorValue !== newDescription) {
cm.setValue(newDescription || '')
Comment thread
himaniraghav3 marked this conversation as resolved.
this.currentValue = newDescription
}
}
}
}

/**
* Convert the first char to Uppercase
* @param str
Expand Down Expand Up @@ -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
12 changes: 11 additions & 1 deletion src/components/EngagementEditor/EngagementEditor.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
margin-bottom: 0;
padding-top: 0;
align-items: flex-start;
display: flex;
flex-direction: column;
}

&.col2 {
Expand Down Expand Up @@ -112,9 +114,17 @@
display: flex;
align-items: center;

span {
.required {
color: $tc-red;
}

.aiButtonRow {
width: 100%;
}

.aiRewriteButton {
margin-top: 10px;
}
}

&.col2 {
Expand Down
73 changes: 64 additions & 9 deletions src/components/EngagementEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -396,6 +399,39 @@ const EngagementEditor = ({
resetAssignState()
}

const handleAIAutowrite = async () => {
if (isGeneratingDescription) return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
Consider adding a debounce mechanism to the handleAIAutowrite function to prevent multiple rapid clicks from triggering multiple requests. This can help reduce unnecessary API calls and improve performance.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are disabling the button when it generates description so multiple clicks cannot be made


setIsGeneratingDescription(true)

try {
const input = engagement.description
const result = await autowriteDescription(input)
Comment thread
himaniraghav3 marked this conversation as resolved.

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
Expand Down Expand Up @@ -605,7 +641,7 @@ const EngagementEditor = ({
<form>
<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label htmlFor='title'>Title <span>*</span> :</label>
<label htmlFor='title'>Title <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
{canEdit ? (
Expand All @@ -628,7 +664,25 @@ const EngagementEditor = ({

<div className={cn(styles.row, styles.descriptionRow)}>
<div className={cn(styles.field, styles.col1)}>
<label htmlFor='description'>Description <span>*</span> :</label>
<label htmlFor='description'>Description <span className={styles.required}>*</span> :</label>
<div className={styles.aiButtonRow}>
<OutlineButton
type='info'
onClick={handleAIAutowrite}
disabled={isGeneratingDescription || !engagement.description.trim()}
Comment thread
himaniraghav3 marked this conversation as resolved.
text='AI Autowrite'
className={styles.aiRewriteButton}
/>
</div>

{isGeneratingDescription && (
<div>
<Loader />
<span className={styles.loadingText}>
Generating description...
</span>
</div>
)}
</div>
<div className={cn(styles.field, styles.col2)}>
<DescriptionField
Expand All @@ -637,6 +691,7 @@ const EngagementEditor = ({
onUpdateDescription={onUpdateDescription}
readOnly={!canEdit}
isPrivate={engagement.isPrivate}
isGeneratingDescription={isGeneratingDescription}
/>
{submitTriggered && validationErrors.description && (
<div className={styles.error}>{validationErrors.description}</div>
Expand All @@ -646,7 +701,7 @@ const EngagementEditor = ({

<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label htmlFor='durationWeeks'>Duration (Weeks) <span>*</span> :</label>
<label htmlFor='durationWeeks'>Duration (Weeks) <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
{canEdit ? (
Expand Down Expand Up @@ -746,7 +801,7 @@ const EngagementEditor = ({

<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label>Time Zone <span>*</span> :</label>
<label>Time Zone <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
{canEdit ? (
Expand Down Expand Up @@ -799,7 +854,7 @@ const EngagementEditor = ({

<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label>Country <span>*</span> :</label>
<label>Country <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
{canEdit ? (
Expand Down Expand Up @@ -834,7 +889,7 @@ const EngagementEditor = ({

<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label>Required Skills <span>*</span> :</label>
<label>Required Skills <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
<SkillsField
Expand All @@ -851,7 +906,7 @@ const EngagementEditor = ({

<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label>Anticipated Start <span>*</span> :</label>
<label>Anticipated Start <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
{canEdit ? (
Expand Down Expand Up @@ -881,7 +936,7 @@ const EngagementEditor = ({

<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label>Status <span>*</span> :</label>
<label>Status <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
{canEdit ? (
Expand Down Expand Up @@ -1049,7 +1104,7 @@ const EngagementEditor = ({
return (
<div key={`assign-member-${index}`} className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label>{assignmentLabel} <span>*</span> :</label>
<label>{assignmentLabel} <span className={styles.required}>*</span> :</label>
</div>
<div className={cn(styles.field, styles.col2)}>
<Select
Expand Down
1 change: 1 addition & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const {
TC_FINANCE_API_URL,
TC_AI_API_BASE_URL,
TC_AI_SKILLS_EXTRACTION_WORKFLOW_ID,
TC_AI_AUTOWRITE_WORKFLOW_ID,
ENGAGEMENTS_APP_URL
} = process.env

Expand Down
34 changes: 32 additions & 2 deletions src/services/workflowAI.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
TC_AI_API_BASE_URL,
TC_AI_SKILLS_EXTRACTION_WORKFLOW_ID,
TC_AI_AUTOWRITE_WORKFLOW_ID,
AI_WORKFLOW_POLL_INTERVAL,
AI_WORKFLOW_POLL_TIMEOUT
} from '../config/constants'
Expand All @@ -14,7 +15,7 @@ import { axiosInstance } from './axiosWithAuth'
* @param {String} input - The input data for the workflow
* @returns {Promise<String>} - The run ID
*/
async function startWorkflowRun (workflowId, input) {
async function startWorkflowRun (workflowId, input, payloadKey = 'jobDescription') {
try {
// Step 1: Create the run
const runResponse = await axiosInstance.post(
Expand All @@ -29,7 +30,7 @@ async function startWorkflowRun (workflowId, input) {
// Step 2: Start the run with input
await axiosInstance.post(
`${TC_AI_API_BASE_URL}/workflows/${workflowId}/start?runId=${runId}`,
{ inputData: { jobDescription: input } }
{ inputData: { [payloadKey]: input } }
)

return runId
Expand Down Expand Up @@ -140,3 +141,32 @@ export async function extractSkillsFromText (description, workflowId = null) {
throw error
}
}

/**
* Auto-generate job description using AI workflow
*
* @param {String} input - Prompt or base input for JD
* @param {String} workflowId - Optional override workflow ID
* @returns {Promise<Object>}
*/
export async function autowriteDescription (input, workflowId = null) {
if (!input || typeof input !== 'string') {
Comment thread
himaniraghav3 marked this conversation as resolved.
throw new Error('Input must be a non-empty string')
}

const workflowIdToUse = workflowId || TC_AI_AUTOWRITE_WORKFLOW_ID

try {
console.log(`Starting JD autowrite workflow: ${workflowIdToUse}`)
const runId = await startWorkflowRun(workflowIdToUse, input, 'rawDescription')
Comment thread
himaniraghav3 marked this conversation as resolved.

console.log(`JD autowrite workflow started with runId: ${runId}`)
const result = await pollWorkflowRunStatus(workflowIdToUse, runId)

console.log('JD autowrite workflow completed successfully')
return result.result || {}
} catch (error) {
console.error('JD autowrite workflow failed:', error.message)
throw error
}
}
Loading