From 1ff776c503e3809eb8955e34f0c8675248cdd012 Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Mon, 23 Feb 2026 11:21:32 +0100 Subject: [PATCH] added neutralization view --- src/api/neutralizationApi.ts | 107 ++++ src/config/pageRegistry.tsx | 7 + src/pages/FeatureView.tsx | 7 + .../neutralization/NeutralizationView.tsx | 517 ++++++++++++++++++ .../NeutralizationViews.module.css | 340 ++++++++++++ src/pages/views/neutralization/index.ts | 1 + src/types/mandate.ts | 11 + 7 files changed, 990 insertions(+) create mode 100644 src/api/neutralizationApi.ts create mode 100644 src/pages/views/neutralization/NeutralizationView.tsx create mode 100644 src/pages/views/neutralization/NeutralizationViews.module.css create mode 100644 src/pages/views/neutralization/index.ts diff --git a/src/api/neutralizationApi.ts b/src/api/neutralizationApi.ts new file mode 100644 index 0000000..6bf799b --- /dev/null +++ b/src/api/neutralizationApi.ts @@ -0,0 +1,107 @@ +/** + * Neutralization API + * + * API functions for the Neutralization feature. + * Endpoints use /api/neutralization/*. Context headers (X-Mandate-Id, X-Instance-Id) + * are set automatically by the api interceptor when on a feature instance page. + */ + +import api from '../api'; + +// ============================================================================ +// TYPES +// ============================================================================ + +export interface NeutralizationConfig { + id?: string; + mandateId: string; + featureInstanceId: string; + userId: string; + enabled: boolean; + namesToParse: string; + sharepointSourcePath: string; + sharepointTargetPath: string; +} + +export interface NeutralizationResult { + neutralized_text?: string; + neutralized_file_base64?: string; + neutralized_file_name?: string; + mime_type?: string; + original_file_id?: string; + neutralized_file_id?: string; + mapping?: Record; + attributes?: Array<{ + id: string; + originalText: string; + patternType: string; + fileId?: string; + }>; + processed_info?: Record; +} + +export interface NeutralizationAttribute { + id: string; + mandateId: string; + featureInstanceId: string; + userId: string; + originalText: string; + fileId?: string; + patternType: string; +} + +// ============================================================================ +// API FUNCTIONS +// ============================================================================ + +export async function getNeutralizationConfig(): Promise { + const { data } = await api.get('/api/neutralization/config'); + return data; +} + +export async function saveNeutralizationConfig( + configData: Partial & { enabled: boolean; namesToParse: string; sharepointSourcePath: string; sharepointTargetPath: string } +): Promise { + const { data } = await api.post('/api/neutralization/config', configData); + return data; +} + +export async function neutralizeText(text: string, fileId?: string): Promise { + const { data } = await api.post('/api/neutralization/neutralize-text', { + text, + ...(fileId && { fileId }), + }); + return data; +} + +export async function resolveText(text: string): Promise<{ resolved_text: string }> { + const { data } = await api.post<{ resolved_text: string }>('/api/neutralization/resolve-text', { + text, + }); + return data; +} + +export async function getNeutralizationAttributes(fileId?: string): Promise { + const params = fileId ? { fileId } : {}; + const { data } = await api.get('/api/neutralization/attributes', { params }); + return data; +} + +export async function neutralizeFile(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + // Do NOT set Content-Type - axios sets it with boundary for FormData + const { data } = await api.post('/api/neutralization/neutralize-file', formData); + return data; +} + +export async function processSharepointFiles( + sourcePath: string, + targetPath: string +): Promise<{ success: boolean; message?: string; [key: string]: unknown }> { + const { data } = await api.post('/api/neutralization/process-sharepoint', { + sourcePath, + targetPath, + }); + return data; +} diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx index b20d8bf..78a4db5 100644 --- a/src/config/pageRegistry.tsx +++ b/src/config/pageRegistry.tsx @@ -92,7 +92,14 @@ export const PAGE_ICONS: Record = { 'page.feature.teamsbot.sessions': , 'page.feature.teamsbot.settings': , + // Feature pages - Neutralization + 'page.feature.neutralization.dashboard': , + 'page.feature.neutralization.playground': , + 'page.feature.neutralization.config': , + 'page.feature.neutralization.attributes': , + // Feature icons (for feature grouping in navigation) + 'feature.neutralization': , 'feature.trustee': , 'feature.realestate': , 'feature.chatworkflow': , diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index 4bc0dd8..5c906b7 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -40,6 +40,9 @@ import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView'; import { TeamsbotSessionView } from './views/teamsbot/TeamsbotSessionView'; import { TeamsbotSettingsView } from './views/teamsbot/TeamsbotSettingsView'; +// Neutralization Views +import { NeutralizationView } from './views/neutralization'; + import styles from './FeatureView.module.css'; // ============================================================================= @@ -135,6 +138,10 @@ const VIEW_COMPONENTS: Record> = { sessions: TeamsbotSessionView, settings: TeamsbotSettingsView, }, + neutralization: { + dashboard: NeutralizationView, + playground: NeutralizationView, + }, }; // ============================================================================= diff --git a/src/pages/views/neutralization/NeutralizationView.tsx b/src/pages/views/neutralization/NeutralizationView.tsx new file mode 100644 index 0000000..d4d5a66 --- /dev/null +++ b/src/pages/views/neutralization/NeutralizationView.tsx @@ -0,0 +1,517 @@ +/** + * NeutralizationView + * + * Combined view for the Neutralization feature with two tabs: + * - Configuration: Enable/disable neutralization, configure names to parse, SharePoint paths. + * - Playground: Manual text neutralization and placeholder resolution. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; +import { useToast } from '../../../contexts/ToastContext'; +import { useFileContext } from '../../../contexts/FileContext'; +import { + getNeutralizationConfig, + saveNeutralizationConfig, + processSharepointFiles, + neutralizeText, + neutralizeFile, + resolveText, + type NeutralizationConfig, +} from '../../../api/neutralizationApi'; +import { Tabs } from '../../../components/UiComponents/Tabs'; +import styles from './NeutralizationViews.module.css'; + +// ============================================================================= +// CONFIGURATION TAB +// ============================================================================= + +const ConfigTab: React.FC = () => { + const { instance } = useCurrentInstance(); + const { showSuccess, showError } = useToast(); + + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [processing, setProcessing] = useState(false); + const [error, setError] = useState(null); + + const [enabled, setEnabled] = useState(true); + const [namesToParse, setNamesToParse] = useState(''); + const [sharepointSourcePath, setSharepointSourcePath] = useState(''); + const [sharepointTargetPath, setSharepointTargetPath] = useState(''); + + const loadConfig = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await getNeutralizationConfig(); + setConfig(data); + setEnabled(data.enabled); + setNamesToParse(data.namesToParse || ''); + setSharepointSourcePath(data.sharepointSourcePath || ''); + setSharepointTargetPath(data.sharepointTargetPath || ''); + } catch (err: unknown) { + const errObj = err as { response?: { data?: { detail?: string | { msg?: string }[] } }; message?: string }; + const detail = errObj.response?.data?.detail; + const message = + (typeof detail === 'string' ? detail : null) || + (Array.isArray(detail) ? detail.map((e: { msg?: string }) => e.msg || JSON.stringify(e)).join(', ') : null) || + errObj.message || + 'Error loading configuration'; + setError(message); + showError('Error', message); + } finally { + setLoading(false); + } + }, [showError]); + + useEffect(() => { + loadConfig(); + }, [loadConfig]); + + const handleSave = async () => { + setSaving(true); + setError(null); + try { + const mandateId = instance?.mandateId; + const featureInstanceId = instance?.id; + if (!mandateId || !featureInstanceId) { + throw new Error('Missing mandate or instance context'); + } + await saveNeutralizationConfig({ + mandateId, + featureInstanceId, + userId: config?.userId || '', + enabled, + namesToParse, + sharepointSourcePath, + sharepointTargetPath, + }); + showSuccess('Saved', 'Configuration saved successfully.'); + await loadConfig(); + } catch (err: unknown) { + const errObj = err as { response?: { data?: { detail?: string } }; message?: string }; + const message = + (typeof errObj.response?.data?.detail === 'string' ? errObj.response.data.detail : null) || + errObj.message || + 'Failed to save configuration'; + setError(message); + showError('Error', message); + } finally { + setSaving(false); + } + }; + + const handleProcessSharepoint = async () => { + if (!sharepointSourcePath.trim() || !sharepointTargetPath.trim()) { + showError('Error', 'Both SharePoint source and target paths are required.'); + return; + } + setProcessing(true); + setError(null); + try { + const result = await processSharepointFiles(sharepointSourcePath, sharepointTargetPath); + if (result.success) { + showSuccess('Done', result.message || 'SharePoint files processed successfully.'); + } else { + setError(result.message || 'Processing failed'); + showError('Error', result.message || 'Processing failed'); + } + } catch (err: unknown) { + const errObj = err as { response?: { data?: { detail?: string } }; message?: string }; + const message = + (typeof errObj.response?.data?.detail === 'string' ? errObj.response.data.detail : null) || + errObj.message || + 'Failed to process SharePoint files'; + setError(message); + showError('Error', message); + } finally { + setProcessing(false); + } + }; + + const dismissError = () => setError(null); + + if (loading) { + return
Loading configuration...
; + } + + return ( +
+

Neutralization Configuration

+

+ Configure data neutralization settings for this instance. +

+ +
+ {error && ( +
+ {error} + +
+ )} + +
+
+ setEnabled(e.target.checked)} + /> + +
+
+ +
+ +