Skip to content

Commit 2541c58

Browse files
Merge pull request #101 from raccoongang/fix/admin-dark-mode-previews
fix: Improve Django admin preview readability in dark mode
2 parents ebc2416 + e286e73 commit 2541c58

File tree

2 files changed

+99
-100
lines changed

2 files changed

+99
-100
lines changed

backend/openedx_ai_extensions/admin.py

Lines changed: 57 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from django import forms
77
from django.contrib import admin
88
from django.core.exceptions import ValidationError
9-
from django.utils.html import format_html
9+
from django.utils.html import escape, format_html
1010
from django.utils.safestring import mark_safe
1111

1212
from openedx_ai_extensions.models import PromptTemplate
@@ -30,7 +30,7 @@ def get_fieldsets(self, request, obj=None):
3030
if obj and obj.pk:
3131
# Editing existing - show UUID example
3232
identification_description = (
33-
f'Slug is human-readable, ID is the stable UUID reference. <br/>'
33+
f'Slug is human-readable, ID is the stable UUID reference.<br/>'
3434
f'Use in profile: <code>"prompt_template": "{obj.pk}"</code> or '
3535
f'<code>"prompt_template": "{obj.slug}"</code>'
3636
)
@@ -43,15 +43,15 @@ def get_fieldsets(self, request, obj=None):
4343
return (
4444
('Identification', {
4545
'fields': ('slug', 'id'),
46-
'description': format_html(identification_description)
46+
'description': format_html(identification_description),
4747
}),
4848
('Prompt Content', {
4949
'fields': ('body',),
50-
'description': 'The prompt template text - edit in the big textbox below.'
50+
'description': 'The prompt template text - edit in the big textbox below.',
5151
}),
5252
('Timestamps', {
5353
'fields': ('created_at', 'updated_at'),
54-
'classes': ('collapse',)
54+
'classes': ('collapse',),
5555
}),
5656
)
5757

@@ -63,7 +63,7 @@ def get_form(self, request, obj=None, change=False, **kwargs):
6363
'rows': 25,
6464
'cols': 120,
6565
'class': 'vLargeTextField',
66-
'style': 'font-family: monospace; font-size: 14px;'
66+
'style': 'font-family: monospace; font-size: 14px;',
6767
})
6868
return form
6969

@@ -73,6 +73,7 @@ def body_preview(self, obj):
7373
preview = obj.body[:80].replace('\n', ' ')
7474
return preview + ('...' if len(obj.body) > 80 else '')
7575
return '-'
76+
7677
body_preview.short_description = 'Prompt Preview'
7778

7879

@@ -89,7 +90,7 @@ class Meta:
8990
'rows': 20,
9091
'cols': 80,
9192
'class': 'vLargeTextField',
92-
'style': 'font-family: monospace;'
93+
'style': 'font-family: monospace;',
9394
}),
9495
}
9596

@@ -120,11 +121,9 @@ def clean_content_patch(self):
120121
# Validate JSON5 syntax
121122
try:
122123
parse_json5_string(content_patch_raw)
123-
except Exception as e:
124-
# json5 library may not expose JSON5DecodeError in all versions
125-
raise ValidationError(f'Invalid JSON5 syntax: {e}') from e
124+
except Exception as exc:
125+
raise ValidationError(f'Invalid JSON5 syntax: {exc}') from exc
126126

127-
# Return the raw string (we store it as text, parse at runtime)
128127
return content_patch_raw
129128

130129

@@ -142,7 +141,7 @@ class AIWorkflowProfileAdmin(admin.ModelAdmin):
142141

143142
fieldsets = (
144143
('Basic Information', {
145-
'fields': ('slug', 'description')
144+
'fields': ('slug', 'description'),
146145
}),
147146
('Profile Template Configuration', {
148147
'fields': ('base_filepath', 'base_template_preview', 'content_patch'),
@@ -168,12 +167,10 @@ def is_valid(self, obj):
168167
"""Show validation status with icon."""
169168
is_valid, errors = obj.validate()
170169
if is_valid:
171-
return format_html(
172-
'<span style="color: green;">✓ Valid</span>'
173-
)
170+
return format_html('<span class="ai-admin-preview--success">✓ Valid</span>')
174171
return format_html(
175-
'<span style="color: red;">✗ {} errors</span>',
176-
len(errors)
172+
'<span class="ai-admin-preview--error">✗ {} errors</span>',
173+
len(errors),
177174
)
178175
is_valid.short_description = 'Status'
179176

@@ -189,73 +186,40 @@ def base_template_preview(self, obj):
189186

190187
if not is_safe_template_path(obj.base_filepath):
191188
return format_html(
192-
'<div style="background: #fee; padding: 10px; border-radius: 4px; color: #c00;">'
189+
'<div class="ai-admin-preview ai-admin-preview--error">'
193190
'<strong>Error:</strong> Invalid or unsafe template path'
194191
'</div>'
195192
)
196193

197-
try:
198-
# Find and read the template file as-is
199-
template_dirs = get_template_directories()
200-
file_content = None
201-
202-
for base_dir in template_dirs:
203-
full_path = base_dir / obj.base_filepath
204-
if full_path.exists():
205-
with open(full_path, 'r', encoding='utf-8') as f:
206-
file_content = f.read()
207-
break
208-
209-
if file_content is None:
210-
return format_html(
211-
'<div style="background: #fee; padding: 10px; '
212-
'border-radius: 4px; color: #c00;">'
213-
'<strong>Error:</strong> Template file not found'
214-
'</div>'
215-
)
216-
217-
# Generate unique ID for this preview
218-
preview_id = f"base-template-{obj.pk or 'new'}"
194+
file_content = None
195+
for base_dir in get_template_directories():
196+
full_path = base_dir / obj.base_filepath
197+
if full_path.exists():
198+
file_content = full_path.read_text(encoding='utf-8')
199+
break
219200

201+
if file_content is None:
220202
return format_html(
221-
'<div>'
222-
'<a href="#" onclick="'
223-
'var el = document.getElementById(\'{id}\'); '
224-
'var link = this; '
225-
'if (el.style.display === \'none\') {{ '
226-
' el.style.display = \'block\'; '
227-
' link.textContent = '
228-
'\'▼ Hide Base Template ({filepath})\'; '
229-
'}} else {{ '
230-
' el.style.display = \'none\'; '
231-
' link.textContent = '
232-
'\'▶ Show Base Template ({filepath})\'; '
233-
'}} '
234-
'return false;" '
235-
'style="text-decoration: none; color: #447e9b; font-weight: bold;">'
236-
'▶ Show Base Template ({filepath})'
237-
'</a>'
238-
'<div id="{id}" style="display: none; '
239-
'background: #f0f8ff; padding: 10px; '
240-
'border-radius: 4px; border: 1px solid #ddd; '
241-
'margin-top: 10px;">'
242-
'<pre style="margin: 0; font-family: monospace; '
243-
'font-size: 12px; max-height: 400px; '
244-
'overflow-y: auto;">{content}</pre>'
203+
'<div class="ai-admin-preview ai-admin-preview--error">'
204+
'<strong>Error:</strong> Template file not found'
245205
'</div>'
246-
'</div>',
247-
id=preview_id,
248-
filepath=obj.base_filepath,
249-
content=file_content
250-
)
251-
except Exception as e: # pylint: disable=broad-exception-caught
252-
return format_html(
253-
'<div style="background: #fee; padding: 10px; '
254-
'border-radius: 4px; color: #c00;">'
255-
'<strong>Error loading template:</strong> {}'
256-
'</div>',
257-
str(e)
258206
)
207+
208+
preview_id = f'base-template-{obj.pk or "new"}'
209+
210+
return format_html(
211+
'<a href="#" class="ai-admin-toggle" '
212+
'onclick="var el=document.getElementById(\'{id}\');'
213+
'el.style.display = el.style.display === \'none\' ? \'block\' : \'none\';'
214+
'return false;">'
215+
'▶ Toggle Base Template ({path})</a>'
216+
'<div id="{id}" class="ai-admin-preview" style="display:none;">'
217+
'<pre>{content}</pre>'
218+
'</div>',
219+
id=preview_id,
220+
path=obj.base_filepath,
221+
content=escape(file_content),
222+
)
259223
base_template_preview.short_description = 'Base Template (Read-Only)'
260224

261225
def effective_config_preview(self, obj):
@@ -264,23 +228,20 @@ def effective_config_preview(self, obj):
264228
return '-'
265229

266230
try:
267-
config = obj.config
268-
formatted_json = json.dumps(config, indent=2, sort_keys=True)
269-
231+
formatted = json.dumps(obj.config, indent=2, sort_keys=True)
270232
return format_html(
271-
'<div style="background: #f8f8f8; padding: 10px; border-radius: 4px;">'
272-
'<strong>Effective Configuration (Base Template + Overrides):</strong><br>'
273-
'<pre style="margin-top: 10px; font-family: monospace; font-size: 12px;">{}</pre>'
233+
'<div class="ai-admin-preview">'
234+
'<strong>Effective Configuration:</strong>'
235+
'<pre>{}</pre>'
274236
'</div>',
275-
formatted_json
237+
formatted,
276238
)
277-
except Exception as e: # pylint: disable=broad-exception-caught
239+
except Exception as exc: # pylint: disable=broad-exception-caught
278240
return format_html(
279-
'<div style="background: #fee; padding: 10px; '
280-
'border-radius: 4px; color: #c00;">'
281-
'<strong>Error loading configuration:</strong> {}'
241+
'<div class="ai-admin-preview ai-admin-preview--error">'
242+
'<strong>Error:</strong> {}'
282243
'</div>',
283-
str(e)
244+
exc,
284245
)
285246
effective_config_preview.short_description = 'Effective Configuration'
286247

@@ -293,25 +254,25 @@ def validation_status(self, obj):
293254

294255
if is_valid:
295256
return format_html(
296-
'<div style="background: #efe; padding: 10px; border-radius: 4px; color: #060;">'
297-
'<strong>✓ Configuration is valid</strong>'
257+
'<div class="ai-admin-preview ai-admin-preview--success">'
258+
'✓ Configuration is valid'
298259
'</div>'
299260
)
300261

301-
error_list = '<br>'.join(f'• {error}' for error in errors)
262+
error_list = '<br>'.join(f'• {escape(e)}' for e in errors)
302263
return format_html(
303-
'<div style="background: #fee; padding: 10px; border-radius: 4px; color: #c00;">'
304-
'<strong>Validation Errors:</strong><br>{}'
264+
'<div class="ai-admin-preview ai-admin-preview--error">'
265+
'<strong>Validation errors:</strong><br>{}'
305266
'</div>',
306-
mark_safe(error_list)
267+
mark_safe(error_list),
307268
)
308269
validation_status.short_description = 'Validation Status'
309270

310271
class Media:
311272
"""Admin media assets."""
312273

313274
css = {
314-
'all': ('admin/css/forms.css',)
275+
'all': ('openedx_ai_extensions/admin.css',),
315276
}
316277

317278

@@ -321,11 +282,7 @@ class AIWorkflowSessionAdmin(admin.ModelAdmin):
321282
Admin interface for managing AI Workflow Sessions.
322283
"""
323284

324-
list_display = (
325-
"user",
326-
"course_id",
327-
"location_id",
328-
)
285+
list_display = ("user", "course_id", "location_id")
329286
search_fields = ("user__username", "course_id", "location_id")
330287
readonly_fields = ("local_submission_id", "remote_response_id", "metadata")
331288

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* Base preview container */
2+
.ai-admin-preview {
3+
background: var(--body-bg);
4+
color: var(--body-fg);
5+
border: 1px solid var(--hairline-color);
6+
padding: 10px;
7+
border-radius: 4px;
8+
margin-top: 8px;
9+
}
10+
11+
/* Code blocks */
12+
.ai-admin-preview pre {
13+
background: transparent;
14+
color: inherit;
15+
font-family: monospace;
16+
font-size: 12px;
17+
margin: 0;
18+
max-height: 400px;
19+
overflow-y: auto;
20+
}
21+
22+
/* Toggle link */
23+
.ai-admin-toggle {
24+
color: var(--primary);
25+
text-decoration: none;
26+
font-weight: bold;
27+
}
28+
29+
.ai-admin-toggle:hover {
30+
text-decoration: underline;
31+
}
32+
33+
/* Status blocks */
34+
.ai-admin-preview--success {
35+
background: var(--message-success-bg);
36+
color: var(--message-success-fg);
37+
}
38+
39+
.ai-admin-preview--error {
40+
background: var(--message-error-bg);
41+
color: var(--message-error-fg);
42+
}

0 commit comments

Comments
 (0)