Skip to content

Commit e931448

Browse files
committed
feat(settings): Improve image moderation settings UI and fix display issues
- Simplify moderation mode options to three (Basic, Strict, Full) - Add detailed descriptions for each mode with focus, performance, and use cases - Fix property name transformation between unprefixed backend keys and prefixed UI keys - Fix ESLint errors by escaping quotes in JSX text - Ensure UI correctly displays values from the backend Known issue: When modifying a single setting, other values may become undefined when saving. This needs further investigation to better preserve existing values during save operations.
1 parent 137d5bf commit e931448

3 files changed

Lines changed: 142 additions & 72 deletions

File tree

src/components/settings/ImageModerationSettings.tsx

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { Form, Input, InputNumber, Switch, Select, Tooltip } from 'antd';
33
import { QuestionCircleOutlined } from '@ant-design/icons';
44
import useGenericSettings from '@app/hooks/useGenericSettings';
@@ -18,35 +18,39 @@ const ImageModerationSettings: React.FC = () => {
1818
} = useGenericSettings('image_moderation');
1919

2020
const [form] = Form.useForm();
21+
const [isUserEditing, setIsUserEditing] = useState(false);
2122

22-
// Update form values when settings change
23+
// Update form values when settings change, but only if the user isn't currently editing
2324
useEffect(() => {
24-
if (settings) {
25+
if (settings && !isUserEditing) {
2526
console.log('ImageModerationSettings - Received settings:', settings);
2627

2728
// Transform property names to match form field names
2829
// The API returns properties without the prefix, but the form expects prefixed names
2930
const settingsObj = settings as Record<string, any>;
3031

32+
// Log the mode value specifically to debug
33+
console.log('Mode field from settings:', settingsObj.mode);
34+
3135
const formValues = {
3236
image_moderation_api: settingsObj.api,
33-
image_moderation_check_interval: typeof settingsObj.check_interval === 'string'
34-
? parseFloat(settingsObj.check_interval)
37+
image_moderation_check_interval: typeof settingsObj.check_interval === 'string'
38+
? parseFloat(settingsObj.check_interval)
3539
: settingsObj.check_interval,
36-
image_moderation_concurrency: typeof settingsObj.concurrency === 'string'
37-
? parseFloat(settingsObj.concurrency)
40+
image_moderation_concurrency: typeof settingsObj.concurrency === 'string'
41+
? parseFloat(settingsObj.concurrency)
3842
: settingsObj.concurrency,
3943
image_moderation_enabled: settingsObj.enabled,
40-
image_moderation_mode: settingsObj.mode,
44+
image_moderation_mode: settingsObj.mode || 'basic', // Default to basic if mode is undefined
4145
image_moderation_temp_dir: settingsObj.temp_dir,
42-
image_moderation_threshold: typeof settingsObj.threshold === 'string'
43-
? parseFloat(settingsObj.threshold)
46+
image_moderation_threshold: typeof settingsObj.threshold === 'string'
47+
? parseFloat(settingsObj.threshold)
4448
: settingsObj.threshold,
45-
image_moderation_timeout: typeof settingsObj.timeout === 'string'
46-
? parseFloat(settingsObj.timeout)
49+
image_moderation_timeout: typeof settingsObj.timeout === 'string'
50+
? parseFloat(settingsObj.timeout)
4751
: settingsObj.timeout
4852
};
49-
53+
5054
console.log('ImageModerationSettings - Transformed form values:', formValues);
5155

5256
// Set form values with a slight delay to ensure the form is ready
@@ -55,27 +59,40 @@ const ImageModerationSettings: React.FC = () => {
5559
console.log('ImageModerationSettings - Form values after set:', form.getFieldsValue());
5660
}, 100);
5761
}
58-
}, [settings, form]);
62+
}, [settings, form, isUserEditing]);
5963

6064
// Handle form value changes
6165
const handleValuesChange = (changedValues: Partial<SettingsGroupType<'image_moderation'>>) => {
66+
setIsUserEditing(true); // Mark that user is currently editing
6267
updateSettings(changedValues);
6368
};
6469

70+
// Modified save function to reset the editing flag
71+
const handleSave = async () => {
72+
await saveSettings();
73+
setIsUserEditing(false); // Reset after saving
74+
};
75+
6576
return (
6677
<BaseSettingsForm
6778
title="Image Moderation Settings"
6879
loading={loading}
6980
error={error}
70-
onSave={saveSettings}
71-
onReset={fetchSettings}
81+
onSave={handleSave}
82+
onReset={() => {
83+
fetchSettings();
84+
setIsUserEditing(false);
85+
}}
7286
>
7387
<Form
7488
form={form}
7589
layout="vertical"
7690
onValuesChange={handleValuesChange}
7791
initialValues={settings || {}}
78-
onFinish={(values) => console.log('Form submitted with values:', values)}
92+
onFinish={(values) => {
93+
console.log('Form submitted with values:', values);
94+
setIsUserEditing(false);
95+
}}
7996
>
8097
<Form.Item
8198
name="image_moderation_enabled"
@@ -90,20 +107,35 @@ const ImageModerationSettings: React.FC = () => {
90107
label={
91108
<span>
92109
Moderation Mode&nbsp;
93-
<Tooltip title="Choose between 'full' for comprehensive moderation or other available modes">
110+
<Tooltip title="Select the appropriate moderation mode based on your needs for accuracy vs. performance">
94111
<QuestionCircleOutlined />
95112
</Tooltip>
96113
</span>
97114
}
98115
>
99-
<Select>
100-
<Option value="full">Full (Check all images)</Option>
101-
<Option value="basic">Basic (Limited checks)</Option>
102-
<Option value="minimal">Minimal (Essential checks only)</Option>
103-
<Option value="sample">Sample (Check random images)</Option>
104-
<Option value="flagged">Flagged Only (Check only reported images)</Option>
116+
<Select
117+
allowClear={true}
118+
placeholder="Select a moderation mode"
119+
>
120+
<Option value="basic">Basic Mode (Fastest, detects explicit content only)</Option>
121+
<Option value="strict">Strict Mode (Fast, blocks all buttocks)</Option>
122+
<Option value="full">Full Mode (Most accurate, uses Llama Vision)</Option>
105123
</Select>
106124
</Form.Item>
125+
126+
<Form.Item>
127+
<div style={{
128+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
129+
padding: '12px',
130+
borderRadius: '4px',
131+
marginBottom: '16px'
132+
}}>
133+
<h4 style={{ marginTop: 0 }}>Moderation Mode Details:</h4>
134+
<p><strong>Basic Mode:</strong> Only detects genitals, anus, and exposed breasts. Fastest processing (no Llama Vision). Best for high-volume applications.</p>
135+
<p><strong>Strict Mode:</strong> Includes all &quot;basic&quot; detection plus automatic blocking of all detected buttocks. Fast processing. Best for zero-tolerance platforms.</p>
136+
<p><strong>Full Mode (Default):</strong> Complete analysis with contextual evaluation. Slower due to Llama Vision, but most accurate. Reduces false positives.</p>
137+
</div>
138+
</Form.Item>
107139

108140
<Form.Item
109141
name="image_moderation_api"

src/components/settings/panels/ImageModerationPanel.tsx

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { Form, Input, InputNumber, Switch, Tooltip, Select } from 'antd';
33
import { QuestionCircleOutlined, FolderOutlined, ApiOutlined } from '@ant-design/icons';
44
import useGenericSettings from '@app/hooks/useGenericSettings';
@@ -16,35 +16,39 @@ const ImageModerationPanel: React.FC = () => {
1616
} = useGenericSettings('image_moderation');
1717

1818
const [form] = Form.useForm();
19+
const [isUserEditing, setIsUserEditing] = useState(false);
1920

20-
// Update form values when settings change
21+
// Update form values when settings change, but only if user isn't actively editing
2122
useEffect(() => {
22-
if (settings) {
23+
if (settings && !isUserEditing) {
2324
console.log('ImageModerationPanel - Received settings:', settings);
2425

2526
// Transform property names to match form field names
2627
// The API returns properties without the prefix, but the form expects prefixed names
2728
const settingsObj = settings as Record<string, any>;
2829

30+
// Log the mode value specifically to debug
31+
console.log('Mode field from settings:', settingsObj.mode);
32+
2933
const formValues = {
3034
image_moderation_api: settingsObj.api,
31-
image_moderation_check_interval: typeof settingsObj.check_interval === 'string'
32-
? parseFloat(settingsObj.check_interval)
35+
image_moderation_check_interval: typeof settingsObj.check_interval === 'string'
36+
? parseFloat(settingsObj.check_interval)
3337
: settingsObj.check_interval,
34-
image_moderation_concurrency: typeof settingsObj.concurrency === 'string'
35-
? parseFloat(settingsObj.concurrency)
38+
image_moderation_concurrency: typeof settingsObj.concurrency === 'string'
39+
? parseFloat(settingsObj.concurrency)
3640
: settingsObj.concurrency,
3741
image_moderation_enabled: settingsObj.enabled,
38-
image_moderation_mode: settingsObj.mode,
42+
image_moderation_mode: settingsObj.mode || 'basic', // Default to basic if mode is undefined
3943
image_moderation_temp_dir: settingsObj.temp_dir,
40-
image_moderation_threshold: typeof settingsObj.threshold === 'string'
41-
? parseFloat(settingsObj.threshold)
44+
image_moderation_threshold: typeof settingsObj.threshold === 'string'
45+
? parseFloat(settingsObj.threshold)
4246
: settingsObj.threshold,
43-
image_moderation_timeout: typeof settingsObj.timeout === 'string'
44-
? parseFloat(settingsObj.timeout)
47+
image_moderation_timeout: typeof settingsObj.timeout === 'string'
48+
? parseFloat(settingsObj.timeout)
4549
: settingsObj.timeout
4650
};
47-
51+
4852
console.log('ImageModerationPanel - Transformed form values:', formValues);
4953

5054
// Set form values with a slight delay to ensure the form is ready
@@ -53,10 +57,11 @@ const ImageModerationPanel: React.FC = () => {
5357
console.log('ImageModerationPanel - Form values after set:', form.getFieldsValue());
5458
}, 100);
5559
}
56-
}, [settings, form]);
60+
}, [settings, form, isUserEditing]);
5761

5862
// Handle form value changes
5963
const handleValuesChange = (changedValues: Partial<SettingsGroupType<'image_moderation'>>) => {
64+
setIsUserEditing(true); // Mark that user is currently editing
6065
updateSettings(changedValues);
6166
};
6267

@@ -70,7 +75,10 @@ const ImageModerationPanel: React.FC = () => {
7075
layout="vertical"
7176
onValuesChange={handleValuesChange}
7277
initialValues={settings || {}}
73-
onFinish={(values) => console.log('Form submitted with values:', values)}
78+
onFinish={(values) => {
79+
console.log('Form submitted with values:', values);
80+
setIsUserEditing(false);
81+
}}
7482
>
7583
<Form.Item
7684
name="image_moderation_enabled"
@@ -112,21 +120,39 @@ const ImageModerationPanel: React.FC = () => {
112120
label={
113121
<span>
114122
Moderation Mode&nbsp;
115-
<Tooltip title="How images should be moderated">
123+
<Tooltip title="Select the appropriate moderation mode based on your needs for accuracy vs. performance">
116124
<QuestionCircleOutlined />
117125
</Tooltip>
118126
</span>
119127
}
120128
>
121-
<Select placeholder="Select a moderation mode">
122-
<Option value="full">Full (Check all images)</Option>
123-
<Option value="basic">Basic (Limited checks)</Option>
124-
<Option value="minimal">Minimal (Essential checks only)</Option>
125-
<Option value="sample">Sample (Check random images)</Option>
126-
<Option value="flagged">Flagged Only (Check only reported images)</Option>
129+
<Select
130+
placeholder="Select a moderation mode"
131+
allowClear={true}
132+
>
133+
<Option value="basic">Basic Mode (Fastest, detects explicit content only)</Option>
134+
<Option value="strict">Strict Mode (Fast, blocks all buttocks)</Option>
135+
<Option value="full">Full Mode (Most accurate, uses Llama Vision)</Option>
127136
</Select>
128137
</Form.Item>
129138

139+
<Form.Item>
140+
<div style={{
141+
color: 'rgba(255, 255, 255, 0.8)',
142+
fontSize: '0.9em',
143+
padding: '0.75rem',
144+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
145+
borderLeft: '3px solid rgba(82, 196, 255, 0.8)',
146+
borderRadius: '0 4px 4px 0',
147+
marginBottom: '16px'
148+
}}>
149+
<h4 style={{ marginTop: 0, color: 'rgba(82, 196, 255, 1)' }}>Moderation Mode Details:</h4>
150+
<p><strong>Basic Mode:</strong> Only detects genitals, anus, and exposed breasts. Fastest processing (no Llama Vision used). Best for initial screening in high-volume applications.</p>
151+
<p><strong>Strict Mode:</strong> Includes all &quot;basic&quot; detection plus automatic blocking of all detected buttocks with confidence ≥ 0.4. Fast processing (no Llama Vision used). Best for zero-tolerance platforms.</p>
152+
<p><strong>Full Mode (Default):</strong> Complete analysis with nuanced context evaluation. Slower due to Llama Vision processing, but most accurate and reduces false positives.</p>
153+
</div>
154+
</Form.Item>
155+
130156
<Form.Item
131157
name="image_moderation_threshold"
132158
label={

src/hooks/useGenericSettings.ts

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,37 +56,17 @@ const useGenericSettings = <T extends SettingsGroupName>(
5656

5757
// The API returns data in the format { [groupName]: settings }
5858
const settingsData = data[groupName] as SettingsGroupType<T>;
59-
59+
6060
if (!settingsData) {
6161
console.warn(`No settings data found for group: ${groupName}`);
6262
// Return empty object instead of null to prevent errors when accessing properties
6363
setSettings({} as SettingsGroupType<T>);
6464
} else {
6565
console.log(`Processed ${groupName} settings:`, settingsData);
6666

67-
// Ensure all numeric values are properly parsed
68-
const processedSettings = { ...settingsData };
69-
70-
// Convert string numbers to actual numbers for specific fields
71-
if (groupName === 'image_moderation') {
72-
const numericFields = [
73-
'image_moderation_threshold',
74-
'image_moderation_check_interval',
75-
'image_moderation_concurrency',
76-
'image_moderation_timeout'
77-
];
78-
79-
numericFields.forEach(field => {
80-
if (field in processedSettings && typeof processedSettings[field] === 'string') {
81-
console.log(`Converting ${field} from string to number:`, processedSettings[field]);
82-
processedSettings[field] = parseFloat(processedSettings[field] as string);
83-
}
84-
});
85-
86-
console.log(`Processed numeric fields for ${groupName}:`, processedSettings);
87-
}
88-
89-
setSettings(processedSettings as SettingsGroupType<T>);
67+
// Handle all settings groups the same way - no special handling for image_moderation
68+
// This approach works for all other settings groups, so it should work for image_moderation too
69+
setSettings(settingsData as SettingsGroupType<T>);
9070
}
9171
} catch (error) {
9272
console.error(`Error fetching ${groupName} settings:`, error);
@@ -105,6 +85,8 @@ const useGenericSettings = <T extends SettingsGroupName>(
10585
console.log(`No previous ${groupName} settings, using updated settings as initial`);
10686
return updatedSettings as SettingsGroupType<T>;
10787
}
88+
89+
// No special handling for image_moderation - treat all settings groups the same
10890
const newSettings = { ...prevSettings, ...updatedSettings };
10991
console.log(`New ${groupName} settings after update:`, newSettings);
11092
return newSettings;
@@ -121,15 +103,45 @@ const useGenericSettings = <T extends SettingsGroupName>(
121103
setLoading(true);
122104
setError(null);
123105

124-
console.log(`Saving ${groupName} settings:`, settings);
106+
// For image_moderation, we need to transform the prefixed keys back to unprefixed keys
107+
let dataToSave = settings;
108+
109+
if (groupName === 'image_moderation') {
110+
// Map from prefixed to unprefixed keys
111+
const prefixMap: Record<string, string> = {
112+
'image_moderation_api': 'api',
113+
'image_moderation_check_interval': 'check_interval',
114+
'image_moderation_concurrency': 'concurrency',
115+
'image_moderation_enabled': 'enabled',
116+
'image_moderation_mode': 'mode',
117+
'image_moderation_temp_dir': 'temp_dir',
118+
'image_moderation_threshold': 'threshold',
119+
'image_moderation_timeout': 'timeout'
120+
};
121+
122+
// Create a new object with only the unprefixed keys
123+
const unprefixedSettings: Record<string, any> = {};
124+
const settingsObj = settings as Record<string, any>;
125+
126+
// Transform all prefixed keys to unprefixed keys
127+
Object.entries(prefixMap).forEach(([prefixedKey, unprefixedKey]) => {
128+
// Set the value in the unprefixed object, preserving the data type
129+
unprefixedSettings[unprefixedKey] = settingsObj[prefixedKey];
130+
});
131+
132+
console.log(`Transformed settings for backend (unprefixed keys):`, unprefixedSettings);
133+
dataToSave = unprefixedSettings as SettingsGroupType<T>;
134+
}
135+
136+
console.log(`Saving ${groupName} settings:`, dataToSave);
125137

126138
const response = await fetch(`${config.baseURL}/api/settings/${groupName}`, {
127139
method: 'POST',
128140
headers: {
129141
'Content-Type': 'application/json',
130142
'Authorization': `Bearer ${token}`,
131143
},
132-
body: JSON.stringify({ [groupName]: settings }),
144+
body: JSON.stringify({ [groupName]: dataToSave }),
133145
});
134146

135147
if (response.status === 401) {

0 commit comments

Comments
 (0)