Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
76c5d6e
feat(ai): map translated compliance issues instead of re-analyzing ta…
ge94zec May 5, 2026
9adc3e9
updated openapi
ge94zec May 5, 2026
7946480
refactor: code
ge94zec May 5, 2026
941bd88
feat: add interactive compliance suggestions
ge94zec May 8, 2026
7433eeb
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec May 8, 2026
5579180
updates:
ge94zec May 9, 2026
97f4653
Merge remote-tracking branch 'origin/chore/2346-enhance-performance-f…
ge94zec May 9, 2026
bd80f96
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec May 9, 2026
2d02039
updates:
ge94zec May 9, 2026
23549fd
Merge remote-tracking branch 'origin/chore/2346-enhance-performance-f…
ge94zec May 9, 2026
102ac7b
chore: update OpenAPI spec and generated client
github-actions[bot] May 9, 2026
b0c19fb
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec May 9, 2026
8d001be
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec May 9, 2026
ee463f7
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec May 11, 2026
b7e0862
change requests
ge94zec May 12, 2026
11e553a
chore: update OpenAPI spec and generated client
github-actions[bot] May 12, 2026
26f3cbd
updated client
ge94zec May 12, 2026
702dd6c
added AiServiceTest for analyze and map
ge94zec May 12, 2026
3aa2f39
Merge remote-tracking branch 'origin/main' into chore/2346-enhance-pe…
ge94zec May 12, 2026
b76eb11
Merge remote-tracking branch 'origin/main' into feat/2348-add-action-…
ge94zec May 12, 2026
dced024
\`Bugfix\`: Restore full entity graph on findByIdWithCompliance
az108 May 12, 2026
9f21677
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec May 13, 2026
630a36f
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec May 14, 2026
fedcb06
feat(compliance): apply AI suggestions to job description editor
ge94zec May 14, 2026
f09b600
Merge remote-tracking branch 'origin/main' into feat/2348-add-action-…
ge94zec May 14, 2026
01a92c8
fix tests
ge94zec May 14, 2026
589edd1
- make prompt robuster to find all issues at once
ge94zec May 15, 2026
643223b
Merge branch 'main' into feat/2348-add-action-buttons-in-popover-for-…
ge94zec May 15, 2026
afe6710
prettier
ge94zec May 15, 2026
8950a33
Merge remote-tracking branch 'origin' into feat/2348-add-action-butto…
ge94zec May 24, 2026
0c89ea9
Merge branch 'main' into feat/2348-add-action-buttons-in-popover-for-…
ge94zec Jun 3, 2026
e25c8d8
Merge branch 'main' into feat/2348-add-action-buttons-in-popover-for-…
ge94zec Jun 10, 2026
ba210c9
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec Jun 10, 2026
37077aa
Fix compliance popover styling and scroll behavior
ge94zec Aug 1, 2026
bfd1434
fix merge conflict
ge94zec Aug 1, 2026
c47b7b7
Merge remote-tracking branch 'origin/main' into feat/2348-add-action-…
ge94zec Aug 1, 2026
a035c0d
Merge branch 'main' into chore/2346-enhance-performance-for-compliance
ge94zec Aug 1, 2026
71a0b76
feat: replace second compliance analysis with snippet mapping for tra…
ge94zec Aug 1, 2026
8820354
ffix server test
ge94zec Aug 1, 2026
e683867
Merge remote-tracking branch 'origin/chore/2346-enhance-performance-f…
ge94zec Aug 1, 2026
e9d0069
fix(compliance): preserve bilingual issue mapping across repeated ana…
ge94zec Aug 2, 2026
39d10d0
lint fix
ge94zec Aug 4, 2026
3c44084
prettier
ge94zec Aug 4, 2026
00bb4ea
Merge branch 'main' into feat/2348-add-action-buttons-in-popover-for-…
ge94zec Aug 17, 2026
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 openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3019,6 +3019,7 @@ components:
explanation: {type: string}
id: {type: string}
language: {type: string}
suggestion: {type: string}
text: {type: string}
ConflictDataDTO:
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ public class ComplianceIssue {
@Enumerated(EnumType.STRING)
private ComplianceAction action;

private String suggestion;
private String language;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class ComplianceScoreService {
/**
* Calculates a legal compliance score based on a hierarchical risk model.
* * The calculation follows the Gatekeeper-Principle for severe risks and Exponential Decay
* for minor issues. If a CRITICAL_AGG violation is detected, the score is immediately 0
* for minor issues. If a CRITICAL or DSGVO violation is detected, the score is immediately 0
* (Veto-Principle), as these represent non-negotiable legal liabilities.
* * For transparency issues, the score is reduced multiplicatively using the formula
* S(n) = 100 * 0.85^n. The decay factor of 0.85 is set to trigger a critical
Expand All @@ -39,16 +39,32 @@ protected int calculateLegalScore(List<ComplianceIssue> compliance) {
.filter(i -> ComplianceCategory.CRITICAL_AGG == i.getCategory())
.count();

long dsgvoCount = compliance
.stream()
.filter(i -> ComplianceCategory.DSGVO_MINIMIZATION == i.getCategory())
.count();
Comment thread
ge94zec marked this conversation as resolved.

if (criticalCount > 0) {
return 0;
}

if (dsgvoCount > 0) {
return 0;
}
Comment thread
ge94zec marked this conversation as resolved.

long transparencyCount = compliance
.stream()
.filter(i -> ComplianceCategory.TRANSPARENCY == i.getCategory())
.count();

double score = 100.0 * Math.pow(PENALTY_FACTOR, transparencyCount);
long publicSectorCount = compliance
.stream()
.filter(i -> ComplianceCategory.PUBLIC_SECTOR == i.getCategory())
.count();
Comment thread
ge94zec marked this conversation as resolved.

long totalCount = transparencyCount + publicSectorCount;

double score = 100.0 * Math.pow(PENALTY_FACTOR, totalCount);
return (int) Math.max(0, Math.round(score));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

<changeSet id="042_add_suggestion_field_to_complianceIssue" author="melissa">
<addColumn tableName="job_compliance_issues">
<column name="suggestion" type="text"/>
</addColumn>
</changeSet>

</databaseChangeLog>
1 change: 1 addition & 0 deletions src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<include file="changelog/00000000000039_add_job_arrangement_and_extension_flags.xml" relativeToChangelogFile="true"/>
<include file="changelog/00000000000040_add_reference_letters.xml" relativeToChangelogFile="true"/>
<include file="changelog/00000000000041_add_reference_letter_document.xml" relativeToChangelogFile="true"/>
<include file="changelog/00000000000042_add_suggestion_field_to_complianceIssue.xml" relativeToChangelogFile="true"/>

<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints
Expand Down
69 changes: 42 additions & 27 deletions src/main/resources/prompts/AnalyzeComplianceText.st
Original file line number Diff line number Diff line change
@@ -1,51 +1,66 @@
You are the TUMApply ComplianceReader, an advanced AI legal and compliance expert for academic job postings at a prestigious university.
Your core capability is "Contextual Compliance Detection" based on German and EU law.
You analyze the semantic intent and the exclusionary effect of sentences, not just keywords.
Your core capability is "Contextual Compliance Detection" based on German and EU law. You ignore typos and spelling errors.
you ONLY act on legal compliance violations.

TASK:
Deeply analyze the job posting for legal risks (AGG, DSGVO, WissZeitVG) based on German and EU law.
The input description language is: {descriptionLanguage}.
The explanation must be written in: {userLang}.
EXPLANATIONS must be written in: {userLang}.
SUGGESTIONS must be written in: {descriptionLanguage}.

STRICT ANALYSIS RULES:

1. NO SUMMARIES: Every single violation must be a separate JSON object.
2. ALWAYS deeply analyze for CONTEXTUAL and IMPLICIT Discrimination in context of a recruitment system.
If a text implies SUBTEXT and EXCLUSIONARY EFFECT, it is a violation
If a text implies SUBTEXT and EXCLUSIONARY EFFECT, even if they sound positive it is a violation.
If a phrase implies that a certain protected group (age, origin, disability or gender) would not "fit" the subtext of the description, it is a CRITICAL_AGG violation.
3. MINIMAL SNIPPETS (NO OVERLAPS): Extract the ABSOLUTE MINIMUM amount of words needed to identify the violation. Never extract whole sentences if a phrase suffices. Two issues MUST NEVER have overlapping text snippets.
4. SEAMLESS GRAMMAR: Suggestions must perfectly match the capitalization, punctuation, and grammatical case of the original text. Inserting the suggestion must not break the surrounding sentence.
----------------------
3. CATEGORY CRITICAL_AGG:
CATEGORIES & ACTIONS:
1. CRITICAL_AGG (action: REPLACE):
- Detect discrimination by protected characteristics under section 1 AGG.
- Detect exclusion based on disability, age, origin, or gender.
- JOB TITLE CHECK: If title "{title}" is not gender-neutral with "(m/f/d)" or "(m/w/d)", flag it.
- action: REPLACE
----------------------
4. CATEGORY DSGVO_MINIMIZATION:
2. DSGVO_MINIMIZATION (action: REMOVE):
- Scan for violations of Data Minimization (Art. 5 DSGVO).
- Detect for keyword that specifically ask for personal DATA (e.g., photos, ID copies, criminal records, marital status, religion)
- Detect for keyword that specifically ask for DATA (e.g., photos, ID copies, criminal records, marital status, religion)
- EXTERNAL LINK CHECK: Detect for external application forms or websites and flag them
- action: REMOVE
----------------------
5. CATEGORY TRANSPARENCY:
3. TRANSPARENCY (action: ADD):
- Detect mentions of THIRD PARTIES/TOOLS (Workday, Headhunter, Partner Uni, consortia) without stating that data is shared with them.
- If entire jobDescription does NOT contain a sentence clarifying that applicant data is shared with external recipients, flag detected mention
- NOT triggered by: When explicit DATA is requested from the applicant.
- action: Suggest to ADD a message that clarifies whether applicant data is shared with external recipients (Art. 13 DSGVO)
----------------------
6. CATEGORY PUBLIC_SECTOR: (WissZeitVG)
- PhD Specific QUALIFICATION PURPOSE: For PhD/Academic positions, the text MUST semantically imply that the position serves scientific qualification (Wissenschaftliche Qualifizierung/Promotion).
- If the intent of individual research/promotion is missing, it is a violation.
- action: ADD
----------------------
- TRANSPARENCY SUGGESTION: suggestion must be a consent-based sentence like: By applying, you consent to your data being shared with "third party" for the purpose of the application process (Art. 13 DSGVO)
4. PUBLIC_SECTOR (action: ADD):
- PhD Specific QUALIFICATION PURPOSE (WissZeitVG): For PhD/Academic positions, the text MUST semantically imply that the position serves scientific qualification (Wissenschaftliche Qualifizierung/Promotion).
- If the intent of individual research/promotion is missing, you must use an anchor text to attach your suggestion. Use the heading "Description" or "Beschreibung" as anchor.
-----------
-----------
INPUT TEXT:
{title}
{jobDescription}
----------------------

COMPLETENESS REQUIREMENT:
Before returning the JSON array, complete all five checks internally:
1. Explicit discrimination
2. Implicit AGG patterns
3. DSGVO minimization violations
4. Third-party transparency issues
5. PhD/scientific qualification context
The final JSON array must reflect the result of all five checks.
Do not output the checklist, reasoning, summaries, or any field named completeness_requirement.

OUTPUT:
- Return ONLY a valid JSON array. No markdown. No prose.
- One object per issue
- Object fields must be exactly:
id, text, category, article, explanation, action
Return ONLY a valid JSON array. No markdown. No prose.
Object fields must be exactly:
id, text, category, article, explanation, action, suggestion
- category must be exactly CRITICAL_AGG, TRANSPARENCY, DSGVO_MINIMIZATION or PUBLIC_SECTOR
- article: the relevant legal article
- text: The exact, shortest possible substring from the input triggering the violation (plain text).
- explanation: A single, formal SHORT sentence stating the legal violation directly. Do not include subordinate clauses. Write for a very small UI popover.
- action must be exactly REPLACE or ADD or REMOVE.
- text: MAXIMUM 2-4 WORDS. Extract exact snippet from input text that uniquely identifies the violation (plain text, no HTML tags).
- explanation must be a single SHORT sentence stating the legal violation directly. Sound like a formal legal notice.
- If no issues exist, return exactly [].
- suggestion: The exact string to fix the issue
- For action is ADD: Return one concise sentence to append the text snippet as an grammatical continuation that starts exactly where the "text" snippet ends.
- For action is REPLACE: The exact safe alternative. Preserve the original meaning as far as legally possible. Match original capitalization exactly.
- For action is REMOVE: return an empty string ""
If no issues exist, return exactly [].
1 change: 1 addition & 0 deletions src/main/webapp/app/generated/model/compliance-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ComplianceIssue {
readonly explanation?: string;
readonly id?: string;
readonly language?: string;
readonly suggestion?: string;
readonly text?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,14 @@ <h1 jhiTranslate="{{ pageTitle() }}"></h1>
height="20rem"
(highlightHovered)="onHighlightHovered($event)"
/>
<jhi-compliance-popover [issue]="activePopoverIssue()" [x]="popoverX()" [y]="popoverY()" />
<jhi-compliance-popover
[issue]="activePopoverIssue()"
[x]="popoverX()"
[y]="popoverY()"
(hovered)="onPopoverHovered($event)"
(accept)="onComplianceSuggestionAccepted($event)"
(dismiss)="onComplianceIssueDismissed($event)"
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@ export class JobCreationFormComponent {
/** When set, only issues of this category are highlighted in the editor. (undefined = all categories shown) */
readonly activeComplianceFilter = signal<string | undefined>(undefined);

/** Dismiss hides the marker, but keeps the issue in score/count. */
readonly dismissedComplianceHighlights = signal<ComplianceIssue[]>([]);

readonly isCompliancePopoverHovered = signal(false);

/** Returns the explanation of a compliance issue whose text appears in the job title, if any. */
readonly titleComplianceError = computed(() => {
this.basicInfoFormValueSignal();
Expand Down Expand Up @@ -863,8 +868,11 @@ export class JobCreationFormComponent {
* @param lang The current language of the editor content
*/
private applyHighlights(compliance: ComplianceIssue[] | undefined, lang: string): void {
const dismissedIssues = this.dismissedComplianceHighlights();
const highlights = (compliance ?? []).flatMap(issue =>
issue.text && issue.category && (!issue.language || issue.language === lang) ? [{ text: issue.text, category: issue.category }] : [],
issue.text && issue.category && (!issue.language || issue.language === lang) && !dismissedIssues.includes(issue)
? [{ text: issue.text, category: issue.category }]
: [],
);
this.jobDescriptionEditor()?.highlightTexts(highlights);
}
Expand All @@ -875,7 +883,6 @@ export class JobCreationFormComponent {
*/
onHighlightHovered(event: { text: string; x: number; y: number } | undefined): void {
if (!event) {
this.activePopoverIssue.set(undefined);
return;
}
const lang = this.currentDescriptionLanguage();
Expand All @@ -885,6 +892,68 @@ export class JobCreationFormComponent {
this.popoverY.set(event.y);
}

onPopoverHovered(isHovered: boolean): void {
this.isCompliancePopoverHovered.set(isHovered);
if (!isHovered) {
this.closeCompliancePopover();
}
}
Comment thread
ge94zec marked this conversation as resolved.
Outdated

/**
* Applies the action of an accepted AI compliance suggestion to the editor.
* Cancels any in-flight translation, syncs the new HTML into the form,
* and removes the issue from the pills so it stops showing in the sidebar.
*/
onComplianceSuggestionAccepted(issue: ComplianceIssue): void {
const updatedHtml = this.jobDescriptionEditor()?.applyComplianceSuggestion(issue);
if (updatedHtml === undefined) return;

this.cancelTranslation();

const lang = this.currentDescriptionLanguage();
this.basicInfoForm.get('jobDescription')?.setValue(updatedHtml);
if (lang === 'en') {
this.jobDescriptionEN.set(updatedHtml);
} else {
this.jobDescriptionDE.set(updatedHtml);
}

this.complianceIssues.update(issues => issues.filter(i => i !== issue));
this.closeCompliancePopover();
this.refreshComplianceHighlights();
}

/**
* Dismisses a compliance issue without applying it.
* The highlight disappears from the editor, but the issue still counts
* toward the score and the sidebar total.
*/
onComplianceIssueDismissed(issue: ComplianceIssue): void {
this.dismissedComplianceHighlights.update(issues => issues.concat(issue));
this.closeCompliancePopover();
this.refreshComplianceHighlights();
}

/**
* Renders compliance highlights in the editor based on the current
* language and active category filter. Called after issues change
* when action state or filter changes.
*/
private refreshComplianceHighlights(): void {
const lang = this.currentDescriptionLanguage();
const category = this.activeComplianceFilter();
const visibleIssues = category
? this.complianceIssues().filter(currentIssue => currentIssue.category === category)
: this.complianceIssues();
this.applyHighlights(visibleIssues, lang);
}

/** Hides the active compliance popover and clears its hover state. */
private closeCompliancePopover(): void {
this.activePopoverIssue.set(undefined);
this.isCompliancePopoverHovered.set(false);
}

/**
* Handles category filter changes from the AI assistant sidebar.
* Updates filter signal to show only the selected category
Expand Down Expand Up @@ -1573,6 +1642,10 @@ export class JobCreationFormComponent {
saved = await firstValueFrom(this.jobApi.createJob(currentData));
this.jobId.set(saved.jobId ?? '');
}
// Ignore stale auto-save responses if the description changed while the request was in flight.
if ((this.basicInfoForm.get('jobDescription')?.value ?? '').trim() !== description.trim()) {
return true;
}

// 3) Sync local state with server response
this.lastSavedData.set(saved);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import { InfoIconComponent } from 'app/shared/components/atoms/info-icon/info-ic
import { ChangeDetectorRef } from '@angular/core';
import { viewChild } from '@angular/core';
import { TranslateDirective } from 'app/shared/language';
import { ComplianceIssueCategoryEnum, ComplianceIssueCategoryEnumValues } from 'app/generated/model/compliance-issue';
import {
ComplianceIssue,
ComplianceIssueActionEnum,
ComplianceIssueCategoryEnum,
ComplianceIssueCategoryEnumValues,
} from 'app/generated/model/compliance-issue';

import { BaseInputDirective } from '../base-input/base-input.component';

Expand Down Expand Up @@ -415,6 +420,47 @@ export class EditorComponent extends BaseInputDirective<string> {
}
}

/**
* Applies an AI compliance suggestion to the current editor content based on compliance action.
* - `Replace`: swaps the target snippet with the suggestion
* - `Remove`: deletes the target snippet
* - `Add`: inserts the suggestion after the target snippet
*
* @param issue The compliance issue containing the action, snippet text, and suggestion.
* @returns The updated editor HTML for issued text snippet
*/
public applyComplianceSuggestion(issue: ComplianceIssue): string | undefined {
const editor = this.quillEditorComponent()?.quillEditor;
if (!editor) return undefined;

const targetSnippet = issue.text?.trim() ?? '';
const replacement = issue.suggestion?.trim() ?? '';
const originalText = editor.getText();
const targetIndex = targetSnippet ? originalText.toLowerCase().indexOf(targetSnippet.toLowerCase()) : -1;

switch (issue.action) {
case ComplianceIssueActionEnum.Replace: {
if (targetIndex === -1) return undefined;
editor.deleteText(targetIndex, targetSnippet.length);
editor.insertText(targetIndex, replacement);
return editor.root.innerHTML;
}
case ComplianceIssueActionEnum.Remove: {
if (targetIndex === -1) return undefined;
editor.deleteText(targetIndex, targetSnippet.length);
return editor.root.innerHTML;
}
case ComplianceIssueActionEnum.Add: {
const insertAt = targetIndex === -1 ? editor.getLength() : targetIndex + targetSnippet.length;
const separator = targetIndex === -1 ? '\n' : ' ';
editor.insertText(insertAt, separator + replacement);
return editor.root.innerHTML;
}
default:
return undefined;
}
}

/**
* Sends the text and position of a highlighted item
* when the mouse hovers over it, so a popover can be shown.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
@if (issue()) {
<div
class="fixed z-50 w-72 rounded-md bg-background-default shadow-lg p-3 text-sm pointer-events-none text-xs"
class="fixed z-[1001] w-72"
Comment thread
ge94zec marked this conversation as resolved.
Outdated
Comment thread
ge94zec marked this conversation as resolved.
Outdated
[style.left.px]="x()"
[style.top.px]="y()"
(mouseenter)="hovered.emit(true)"
(mouseleave)="hovered.emit(false)"
>
<p class="m-0 font-semibold mb-1 text-text-tertiary">{{ issue()?.article }}</p>
<p class="m-0 text-text-secondary leading-relaxed">{{ issue()?.explanation }}</p>
<jhi-suggestion-system
[article]="issue()?.article"
[suggestion]="issue()?.suggestion"
[explanation]="issue()?.explanation"
[actionLabel]="actionButtonLabel()"
(accepted)="onAccept()"
(dismissed)="onDismiss()"
/>
</div>
}
Loading
Loading