@@ -52,6 +52,7 @@ import {
5252import { AiAssistantCardComponent } from 'app/shared/components/molecules/ai-assistant-card/ai-assistant-card.component' ;
5353import { UserShortDTORolesEnum } from 'app/generated/model/user-short-dto' ;
5454import { ComplianceIssue , ComplianceIssueCategoryEnum } from 'app/generated/model/compliance-issue' ;
55+ import { CompliancePopoverComponent } from 'app/shared/components/molecules/ai-compliance-popover/ai-compliance-popover.component' ;
5556
5657import { JobDetailComponent } from '../job-detail/job-detail.component' ;
5758import * as DropdownOptions from '.././dropdown-options' ;
@@ -103,6 +104,7 @@ type JobFormMode = 'create' | 'edit';
103104 ImageUploadButtonComponent ,
104105 CheckboxComponent ,
105106 AiAssistantCardComponent ,
107+ CompliancePopoverComponent ,
106108 ] ,
107109 providers : [ JobResourceApi ] ,
108110} )
@@ -114,6 +116,10 @@ export class JobCreationFormComponent {
114116 // ═══════════════════════════════════════════════════════════════════════════
115117 readonly publishButtonSeverity = 'primary' as ButtonColor ;
116118 readonly publishButtonIcon = 'paper-plane' ;
119+ /** Width of the compliance popover, used to clamp its position within the viewport.
120+ * matches the width w-72 set in ai-compliance-popover.component.html.
121+ */
122+ private readonly POPOVER_WIDTH = 288 ;
117123
118124 // ═══════════════════════════════════════════════════════════════════════════
119125 // MODE & META SIGNALS
@@ -173,8 +179,8 @@ export class JobCreationFormComponent {
173179 /** Last successfully translated German text (used to avoid redundant translations) */
174180 lastTranslatedDE = signal < string > ( '' ) ;
175181
176- /** Last analyzed description text (used to avoid redundant compliance analysis) */
177- private lastAnalyzedText = '' ;
182+ /** Last analyzed description text per language (used to avoid redundant compliance analysis) */
183+ private lastAnalyzedText : Record < string , string > = { } ;
178184
179185 // ═══════════════════════════════════════════════════════════════════════════
180186 // AI GENERATION SIGNALS
@@ -262,6 +268,30 @@ export class JobCreationFormComponent {
262268 /** List of detected compliance issues to update the UI and editor highlights */
263269 readonly complianceIssues = signal < ComplianceIssue [ ] > ( [ ] ) ;
264270
271+ /** The compliance issue currently shown in the popover (undefined = none is hovered). */
272+ readonly activePopoverIssue = signal < ComplianceIssue | undefined > ( undefined ) ;
273+
274+ /** Horizontal screen position of popover. */
275+ readonly popoverX = signal < number > ( 0 ) ;
276+
277+ /** Vertical screen position of popover. */
278+ readonly popoverY = signal < number > ( 0 ) ;
279+
280+ /** When set, only issues of this category are highlighted in the editor. (undefined = all categories shown) */
281+ readonly activeComplianceFilter = signal < string | undefined > ( undefined ) ;
282+
283+ /** Returns the explanation of a compliance issue whose text appears in the job title, if any. */
284+ readonly titleComplianceError = computed ( ( ) => {
285+ const title = ( this . basicInfoForm . get ( 'title' ) ?. value ?? '' ) . toLowerCase ( ) ;
286+ if ( ! title ) return undefined ;
287+ for ( const issue of this . complianceIssues ( ) ) {
288+ if ( issue . text && title . includes ( issue . text . toLowerCase ( ) ) ) {
289+ return issue . explanation ;
290+ }
291+ }
292+ return undefined ;
293+ } ) ;
294+
265295 // ═══════════════════════════════════════════════════════════════════════════
266296 // FORM GROUPS
267297 // ═══════════════════════════════════════════════════════════════════════════
@@ -782,6 +812,33 @@ export class JobCreationFormComponent {
782812 this . jobDescriptionEditor ( ) ?. highlightTexts ( highlights ) ;
783813 }
784814
815+ /**
816+ * Handles hover events from highlighted spans in the editor.
817+ * Finds the matching compliance issue and positions the popover.
818+ */
819+ onHighlightHovered ( event : { text : string ; x : number ; y : number } | undefined ) : void {
820+ if ( ! event ) {
821+ this . activePopoverIssue . set ( undefined ) ;
822+ return ;
823+ }
824+ const lang = this . currentDescriptionLanguage ( ) ;
825+ const match = this . complianceIssues ( ) . find ( i => i . language === lang && i . text ?. toLowerCase ( ) === event . text . toLowerCase ( ) ) ;
826+ this . activePopoverIssue . set ( match ) ;
827+ this . popoverX . set ( Math . min ( event . x , window . innerWidth - this . POPOVER_WIDTH ) ) ;
828+ this . popoverY . set ( event . y ) ;
829+ }
830+
831+ /**
832+ * Handles category filter changes from the AI assistant sidebar.
833+ * Filters highlights to show only the selected category, or all if cleared.
834+ */
835+ onComplianceFilterChange ( category : string | undefined ) : void {
836+ this . activeComplianceFilter . set ( category ) ;
837+ const lang = this . currentDescriptionLanguage ( ) ;
838+ const filtered = category ? this . complianceIssues ( ) . filter ( i => i . category === category ) : this . complianceIssues ( ) ;
839+ this . applyHighlights ( filtered , lang ) ;
840+ }
841+
785842 // ═══════════════════════════════════════════════════════════════════════════
786843 // AI GENERATION METHODS
787844 // ═══════════════════════════════════════════════════════════════════════════
@@ -1152,7 +1209,9 @@ export class JobCreationFormComponent {
11521209 const content = lang === 'en' ? this . jobDescriptionEN ( ) : this . jobDescriptionDE ( ) ;
11531210 this . basicInfoForm . get ( 'jobDescription' ) ?. setValue ( content , { emitEvent : false } ) ;
11541211 this . jobDescriptionSignal . set ( content ) ;
1155- this . jobDescriptionEditor ( ) ?. forceUpdate ( content ) ;
1212+ this . jobDescriptionEditor ( ) ?. forceUpdate ( content , ( ) => {
1213+ this . applyHighlights ( this . complianceIssues ( ) , lang ) ;
1214+ } ) ;
11561215 }
11571216
11581217 // ═══════════════════════════════════════════════════════════════════════════
@@ -1239,11 +1298,15 @@ export class JobCreationFormComponent {
12391298 this . lastTranslatedEN . set ( en ) ;
12401299 this . jobDescriptionDE . set ( de ) ;
12411300 this . lastTranslatedDE . set ( de ) ;
1242- this . lastAnalyzedText = en ;
1301+ this . lastAnalyzedText [ 'en' ] = en ;
1302+ this . lastAnalyzedText [ 'de' ] = de ;
12431303
12441304 if ( job ?. genderBiasScore !== undefined ) {
12451305 this . aiScore . set ( job . genderBiasScore ) ;
12461306 }
1307+ if ( job ?. complianceIssues ) {
1308+ this . complianceIssues . set ( job . complianceIssues ) ;
1309+ }
12471310
12481311 this . basicInfoForm . patchValue ( {
12491312 title : job ?. title ?? '' ,
@@ -1255,7 +1318,9 @@ export class JobCreationFormComponent {
12551318 } ) ;
12561319
12571320 this . jobDescriptionSignal . set ( en ) ;
1258- this . jobDescriptionEditor ( ) ?. forceUpdate ( en ) ;
1321+ this . jobDescriptionEditor ( ) ?. forceUpdate ( en , ( ) => {
1322+ this . applyHighlights ( this . complianceIssues ( ) , 'en' ) ;
1323+ } ) ;
12591324
12601325 this . positionDetailsForm . patchValue ( {
12611326 startDate : job ?. startDate ?? '' ,
@@ -1560,17 +1625,18 @@ export class JobCreationFormComponent {
15601625
15611626 // 1) Build a fresh DTO and skip if the description hasn't changed since last analysis
15621627 const jobForm = this . createJobDTO ( JobFormDTOStateEnum . Draft ) ;
1628+ const userLang = this . translate . currentLang ;
15631629 const descriptionText = lang === 'en' ? ( jobForm . jobDescriptionEN ?? '' ) : ( jobForm . jobDescriptionDE ?? '' ) ;
1564- if ( ! descriptionText . trim ( ) || descriptionText === this . lastAnalyzedText ) {
1630+ if ( ! descriptionText . trim ( ) || descriptionText === this . lastAnalyzedText [ lang ] ) {
15651631 this . isAnalyzing . set ( false ) ; // Clear flag in case caller pre-set it
15661632 return ;
15671633 }
15681634
15691635 this . isAnalyzing . set ( true ) ;
15701636 try {
15711637 // 2) Send the description to the analysis endpoint (persists score on the backend)
1572- const compliance = await firstValueFrom ( this . aiApi . analyzeJobDescriptionForCompliance ( lang , jobForm ) ) ;
1573- this . lastAnalyzedText = descriptionText ;
1638+ const compliance = await firstValueFrom ( this . aiApi . analyzeJobDescriptionForCompliance ( lang , jobForm , userLang ) ) ;
1639+ this . lastAnalyzedText [ lang ] = descriptionText ;
15741640 // Keep issues from other languages, but replace all issues for the current language with the latest analysis.
15751641 const otherLang = lang === 'en' ? 'de' : 'en' ;
15761642 const existingLang = this . complianceIssues ( ) . filter ( issue => issue . language === otherLang ) ;
0 commit comments