Skip to content

Commit 246192f

Browse files
authored
feat(ui): Add Auto Rubric feature for automatic grading criteria gene… (#92)
* feat(ui): Add Auto Rubric feature for automatic grading criteria generation Add new Auto Rubric feature that automatically generates evaluation rubrics for LLM applications. Includes simple and iterative generation modes, data upload, history management, and export functionality. * fix: Rename test_panel.py to rubric_tester.py to avoid pytest collection Pytest was treating test_panel.py as a test file due to the 'test_' prefix, causing import errors in CI. Renamed to rubric_tester.py to fix this issue. * fix(ui): stabilize widget state across language switch and persist language preference - Use stable values (e.g., "_custom_", "python") instead of translated labels as selectbox options - Prevents widget state loss when UI language changes - Add localStorage persistence for language preference - Add inject_language_loader() to restore language setting on page load - Update navigation to use stable feature_ids for feature selector
1 parent b789dc4 commit 246192f

25 files changed

Lines changed: 4156 additions & 40 deletions

ui/app.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
from core.feature_registry import FeatureRegistry # noqa: E402
2121
from core.navigation import Navigation # noqa: E402
2222
from features.auto_arena import AutoArenaFeature # noqa: E402
23+
from features.auto_rubric import AutoRubricFeature # noqa: E402
2324

2425
# Import feature modules
2526
from features.grader import GraderFeature # noqa: E402
2627
from shared.components.common import render_footer # noqa: E402
2728
from shared.components.logo import render_logo_and_title # noqa: E402
28-
from shared.i18n import render_language_selector, t # noqa: E402
29+
from shared.i18n import ( # noqa: E402
30+
inject_language_loader,
31+
render_language_selector,
32+
t,
33+
)
2934
from shared.styles.theme import inject_css # noqa: E402
3035

3136
# pylint: enable=wrong-import-position
@@ -38,9 +43,7 @@
3843
# Add new features here as they are implemented
3944
FeatureRegistry.register(GraderFeature)
4045
FeatureRegistry.register(AutoArenaFeature)
41-
# Future features:
42-
# from features.autorubric import AutoRubricFeature
43-
# FeatureRegistry.register(AutoRubricFeature)
46+
FeatureRegistry.register(AutoRubricFeature)
4447

4548
# ============================================================================
4649
# Page Configuration (must be first Streamlit command)
@@ -58,6 +61,9 @@ def main() -> None:
5861
# Inject custom CSS
5962
inject_css()
6063

64+
# Load language preference from browser localStorage
65+
inject_language_loader()
66+
6167
# ========================================================================
6268
# Sidebar Configuration
6369
# ========================================================================

ui/core/navigation.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,31 @@ def render_feature_selector() -> str:
3838
st.warning("No features registered")
3939
return ""
4040

41-
# Build options - use display_label property for i18n support
42-
# Note: get_all() returns classes, but display_label is a property that requires instances
41+
# Build options - use stable feature_ids (not translated labels)
4342
feature_ids = [f.feature_id for f in features]
44-
feature_labels = {f.feature_id: FeatureRegistry.get_instance(f.feature_id).display_label for f in features}
4543

4644
# Get default feature id
4745
default_id = FeatureRegistry.get_default_feature_id()
4846

49-
# Use widget key directly for state management
47+
# Widget key for selectbox
5048
widget_key = "_nav_feature_selector"
5149

5250
# Initialize widget state if not exists
5351
if widget_key not in st.session_state:
5452
st.session_state[widget_key] = default_id
5553

56-
# Ensure current value is valid
54+
# Ensure current value is valid (in case features changed)
5755
if st.session_state[widget_key] not in feature_ids:
5856
st.session_state[widget_key] = default_id
5957

6058
# Get previous value for lifecycle hooks
6159
previous_id = st.session_state.get(CURRENT_FEATURE_KEY)
6260

63-
# Render selectbox (dropdown) for feature selection
61+
# Build labels dynamically (these change with language, but values stay stable)
62+
feature_labels = {f.feature_id: FeatureRegistry.get_instance(f.feature_id).display_label for f in features}
63+
64+
# Render selectbox with stable feature_ids as options
65+
# No index parameter - let Streamlit manage state via key
6466
selected_id = st.selectbox(
6567
t("app.features"),
6668
options=feature_ids,

ui/features/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
from features.auto_arena import AutoArenaFeature
10+
from features.auto_rubric import AutoRubricFeature
1011
from features.grader import GraderFeature
1112

12-
__all__ = ["GraderFeature", "AutoArenaFeature"]
13+
__all__ = ["GraderFeature", "AutoArenaFeature", "AutoRubricFeature"]

ui/features/auto_arena/components/config_panel.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,24 +192,30 @@ def _render_single_endpoint(
192192
)
193193

194194
with col2:
195+
# Use stable value for custom option to survive UI language switch
196+
CUSTOM_VALUE = "_custom_"
197+
195198
# Check if there's a custom model value already set (e.g., from preset)
196199
current_model = st.session_state.get(f"arena_ep_model_{endpoint_id}", "")
197200

198201
# Build options list - include current model if it's custom
199202
model_options = list(DEFAULT_MODELS)
200-
custom_label = t("model.custom")
201-
if current_model and current_model not in model_options and current_model != custom_label:
203+
if current_model and current_model not in model_options and current_model != CUSTOM_VALUE:
202204
model_options.insert(0, current_model)
203-
model_options.append(custom_label)
205+
model_options.append(CUSTOM_VALUE)
206+
207+
def format_model(x: str) -> str:
208+
return t("model.custom") if x == CUSTOM_VALUE else x
204209

205210
model_option = st.selectbox(
206211
t("model.select"),
207212
options=model_options,
213+
format_func=format_model,
208214
key=f"arena_ep_model_{endpoint_id}",
209215
)
210216

211217
# Row 1.5: Custom model input (only if "Custom..." selected)
212-
if model_option == t("model.custom"):
218+
if model_option == CUSTOM_VALUE:
213219
model = st.text_input(
214220
t("model.custom_input"),
215221
placeholder=t("model.custom_placeholder"),

ui/features/auto_arena/components/sidebar.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ def _apply_preset_sidebar_data() -> None:
3232

3333
st.session_state["arena_judge_api_key"] = preset_data.get("judge_api_key", "")
3434

35-
# Judge model
35+
# Judge model - use stable value for custom option
36+
CUSTOM_VALUE = "_custom_"
3637
judge_model = preset_data.get("judge_model", "")
3738
if judge_model in DEFAULT_MODELS:
38-
st.session_state["arena_judge_model"] = judge_model
39+
st.session_state["arena_judge_model_value"] = judge_model
3940
else:
40-
st.session_state["arena_judge_model"] = "Custom..."
41+
st.session_state["arena_judge_model_value"] = CUSTOM_VALUE
4142
st.session_state["arena_judge_custom_model"] = judge_model
4243

4344
# Evaluation settings
@@ -59,10 +60,13 @@ def _render_judge_settings(config: dict[str, Any]) -> None:
5960
"""Render judge model settings section."""
6061
st.markdown(f'<div class="section-header">{t("arena.sidebar.judge_model")}</div>', unsafe_allow_html=True)
6162

63+
provider_options = list(DEFAULT_API_ENDPOINTS.keys())
64+
if "arena_judge_provider" not in st.session_state:
65+
st.session_state["arena_judge_provider"] = provider_options[0]
66+
6267
endpoint_choice = st.selectbox(
6368
t("api.provider"),
64-
options=list(DEFAULT_API_ENDPOINTS.keys()),
65-
index=0,
69+
options=provider_options,
6670
help=t("arena.sidebar.judge_provider_help"),
6771
key="arena_judge_provider",
6872
)
@@ -90,14 +94,25 @@ def _render_judge_settings(config: dict[str, Any]) -> None:
9094
else:
9195
st.warning(t("api.key_required"))
9296

97+
# Use stable value for custom option to survive UI language switch
98+
CUSTOM_VALUE = "_custom_"
99+
model_options = DEFAULT_MODELS + [CUSTOM_VALUE]
100+
101+
def format_model_option(x: str) -> str:
102+
return t("model.custom") if x == CUSTOM_VALUE else x
103+
104+
# Initialize default value in session state if not exists
105+
if "arena_judge_model_value" not in st.session_state:
106+
st.session_state["arena_judge_model_value"] = DEFAULT_MODELS[0] if DEFAULT_MODELS else CUSTOM_VALUE
107+
93108
model_option = st.selectbox(
94109
t("model.select"),
95-
options=DEFAULT_MODELS + [t("model.custom")],
96-
index=0,
97-
key="arena_judge_model",
110+
options=model_options,
111+
format_func=format_model_option,
112+
key="arena_judge_model_value",
98113
)
99114

100-
if model_option == t("model.custom"):
115+
if model_option == CUSTOM_VALUE:
101116
model_name = st.text_input(
102117
t("model.custom_input"),
103118
placeholder=t("model.custom_placeholder"),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# -*- coding: utf-8 -*-
2+
"""Auto Rubric feature module for OpenJudge Studio.
3+
4+
This feature provides automatic rubric generation from task descriptions
5+
or labeled data, eliminating manual rubric design.
6+
7+
Phase 1 implements:
8+
- Simple Rubric mode (zero-shot from task description)
9+
- Basic result display
10+
- Export functionality (Python/YAML/JSON)
11+
"""
12+
13+
from features.auto_rubric.feature import AutoRubricFeature
14+
15+
__all__ = ["AutoRubricFeature"]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
"""UI components for Auto Rubric feature."""
3+
4+
from features.auto_rubric.components.data_upload_panel import render_data_upload_panel
5+
from features.auto_rubric.components.history_panel import (
6+
render_history_panel,
7+
render_task_detail,
8+
)
9+
from features.auto_rubric.components.iterative_config_panel import (
10+
render_iterative_config_panel,
11+
validate_iterative_config,
12+
)
13+
from features.auto_rubric.components.result_panel import render_result_panel
14+
from features.auto_rubric.components.rubric_tester import (
15+
render_test_panel,
16+
render_test_section_compact,
17+
)
18+
from features.auto_rubric.components.sidebar import render_rubric_sidebar
19+
from features.auto_rubric.components.simple_config_panel import (
20+
render_simple_config_panel,
21+
validate_simple_config,
22+
)
23+
24+
__all__ = [
25+
"render_rubric_sidebar",
26+
"render_simple_config_panel",
27+
"validate_simple_config",
28+
"render_iterative_config_panel",
29+
"validate_iterative_config",
30+
"render_data_upload_panel",
31+
"render_result_panel",
32+
"render_history_panel",
33+
"render_task_detail",
34+
"render_test_panel",
35+
"render_test_section_compact",
36+
]

0 commit comments

Comments
 (0)