feat: Enhance training parameters handling and documentation
- Introduced new fields for descriptions in training parameters, improving clarity for AI context in `training_sessions_recent_json`.
- Added a glossary placeholder `{{training_parameters_glossary_md}}` to provide a Markdown table of active training parameters, including names and descriptions.
- Updated the `placeholder_resolver.py` and `activity_metrics.py` to support the new glossary functionality.
- Enhanced the `AdminActivityAttributeProfilesPage` to allow input for descriptions in both German and English, ensuring better context for metrics.
- Revised tests to validate the inclusion of description fields in parameter schema merges and metrics handling.
This commit is contained in:
parent
c3be745efa
commit
bc8e9fb7fa
|
|
@ -57,6 +57,8 @@
|
|||
- Platzhalter / Charts, die Session-Details brauchen: **nur** diese Layer-1-Helfer erweitern oder aufrufen (z. B. `activity_metrics.get_training_sessions_recent_weeks_data` nutzt `enrich_sessions_with_metrics`).
|
||||
- Router: `get_db`, `get_cursor`, Auth; Business-Validierung delegieren an `activity_session_metrics`.
|
||||
|
||||
**KI-Kontext:** In `training_sessions_recent_json` enthält jedes Element von `session_metrics` neben `key`/`value` die Felder `name_de`, `name_en`, `description_de`, `description_en` (aus dem effektiven Schema). Für nicht selbsterklärende Keys soll im Katalog `training_parameters.description_*` gepflegt werden (Admin). Ergänzend liefert der Platzhalter `{{training_parameters_glossary_md}}` die gesamte aktive Parameter-Legende als Markdown-Tabelle (`get_training_parameters_ki_glossary_data` → `get_training_parameters_glossary_md`).
|
||||
|
||||
---
|
||||
|
||||
## 4. API (Ist / geplant)
|
||||
|
|
@ -137,6 +139,7 @@ Abdeckung: reine Merge-Logik (`merge_parameter_schema_rows`), Validierung (`_val
|
|||
- Migration 004/014: `training_types`, `activity_log`-Erweiterungen
|
||||
- Pattern Admin-Katalog: `routers/admin_reference_value_types.py`
|
||||
- Platzhalter Session-JSON: `data_layer/activity_metrics.py` → `get_training_sessions_recent_weeks_data`
|
||||
- KI-Legende: `get_training_parameters_ki_glossary_data`, Platzhalter `{{training_parameters_glossary_md}}`
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ frontend/src/
|
|||
|
||||
- **Agent-Guide:** `.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md` (Prod: nur additive Migration **054**; Layer1 `data_layer/activity_session_metrics.py`).
|
||||
- **DB:** `training_category_parameter`, `training_type_parameter`, `activity_session_metrics`; `activity_log.started_at` / `ended_at` (nullable).
|
||||
- **API:** Admin `/api/admin/training-parameters`, `/api/admin/training-category-parameters`, `/api/admin/training-type-parameters`; Nutzer `GET /api/activity/{id}`, `PUT /api/activity/{id}/metrics`; Platzhalter-Pfad `training_sessions_recent_json` liefert pro Session `session_metrics` (wenn befüllt).
|
||||
- **API:** Admin `/api/admin/training-parameters`, `/api/admin/training-category-parameters`, `/api/admin/training-type-parameters`; Nutzer `GET /api/activity/{id}`, `PUT /api/activity/{id}/metrics`; Platzhalter-Pfad `training_sessions_recent_json` liefert pro Session `session_metrics` inkl. `name_*` / `description_*`; **`{{training_parameters_glossary_md}}`** = Markdown-Legende aller aktiven Parameter (KI).
|
||||
- **Frontend:** Admin `/admin/activity-attribute-profiles`; Aktivität → Verlauf → Bearbeiten: Profil-Kennwerte; `api.js` ergänzt.
|
||||
|
||||
### Updates (16.04.2026 - Aktivität Phase A abgeschlossen, Phase B gestartet)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Functions:
|
|||
- get_training_frequency_by_type_data(): Häufigkeit & Intensität pro activity_type
|
||||
- get_training_inter_session_gap_data(): Pausen zwischen Einheiten (Stunden)
|
||||
- get_training_sessions_recent_weeks_data(): Wochen-JSON für KI-Kontext
|
||||
- get_training_parameters_ki_glossary_data(): Parameter-Katalog (Feld, Namen, Beschreibungen) für KI
|
||||
|
||||
All functions return structured data (dict) without formatting.
|
||||
Use placeholder_resolver.py for formatted strings for AI.
|
||||
|
|
@ -1179,3 +1180,32 @@ def get_training_sessions_recent_weeks_data(
|
|||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_training_parameters_ki_glossary_data(profile_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Alle aktiven ``training_parameters`` für KI-Kontext (z. B. neben ``training_sessions_recent_json``).
|
||||
|
||||
Enthält technischen key, name_de/name_en, description_de/description_en, data_type, unit, category.
|
||||
|
||||
Args:
|
||||
profile_id: Reserviert für spätere Einschränkung (z. B. nur im Profil vorkommende Keys);
|
||||
aktuell ungenutzt, Signatur bleibt für Platzhalter-Resolver.
|
||||
"""
|
||||
_ = profile_id
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT key, name_de, name_en, description_de, description_en,
|
||||
data_type, unit, category
|
||||
FROM training_parameters
|
||||
WHERE is_active = true
|
||||
ORDER BY category, key
|
||||
"""
|
||||
)
|
||||
rows = [r2d(r) for r in cur.fetchall()]
|
||||
return {
|
||||
"parameters": rows,
|
||||
"meta": {"count": len(rows), "scope": "global_active_catalog"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ def merge_parameter_schema_rows(
|
|||
"key": r["key"],
|
||||
"name_de": r["name_de"],
|
||||
"name_en": r["name_en"],
|
||||
"description_de": r.get("description_de"),
|
||||
"description_en": r.get("description_en"),
|
||||
"param_category": r["param_category"],
|
||||
"data_type": r["data_type"],
|
||||
"unit": r["unit"],
|
||||
|
|
@ -90,6 +92,8 @@ def merge_parameter_schema_rows(
|
|||
"key": r["key"],
|
||||
"name_de": r["name_de"],
|
||||
"name_en": r["name_en"],
|
||||
"description_de": r.get("description_de"),
|
||||
"description_en": r.get("description_en"),
|
||||
"param_category": r["param_category"],
|
||||
"data_type": r["data_type"],
|
||||
"unit": r["unit"],
|
||||
|
|
@ -133,7 +137,9 @@ def resolve_activity_attribute_schema(
|
|||
tcp.sort_order AS cat_sort,
|
||||
tcp.required AS cat_required,
|
||||
tcp.ui_group AS cat_ui_group,
|
||||
tp.key, tp.name_de, tp.name_en, tp.category AS param_category,
|
||||
tp.key, tp.name_de, tp.name_en,
|
||||
tp.description_de, tp.description_en,
|
||||
tp.category AS param_category,
|
||||
tp.data_type, tp.unit, tp.validation_rules, tp.source_field
|
||||
FROM training_category_parameter tcp
|
||||
JOIN training_parameters tp ON tp.id = tcp.training_parameter_id
|
||||
|
|
@ -151,7 +157,9 @@ def resolve_activity_attribute_schema(
|
|||
ttp.sort_order AS typ_sort,
|
||||
ttp.required AS typ_required,
|
||||
ttp.ui_group AS typ_ui_group,
|
||||
tp.key, tp.name_de, tp.name_en, tp.category AS param_category,
|
||||
tp.key, tp.name_de, tp.name_en,
|
||||
tp.description_de, tp.description_en,
|
||||
tp.category AS param_category,
|
||||
tp.data_type, tp.unit, tp.validation_rules, tp.source_field
|
||||
FROM training_type_parameter ttp
|
||||
JOIN training_parameters tp ON tp.id = ttp.training_parameter_id
|
||||
|
|
@ -164,6 +172,16 @@ def resolve_activity_attribute_schema(
|
|||
return merge_parameter_schema_rows(category_rows, type_rows)
|
||||
|
||||
|
||||
def _metric_human_labels(schema_row: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
"""Bezeichnung + Kurzbeschreibung aus training_parameters (KI / Export)."""
|
||||
return {
|
||||
"name_de": schema_row.get("name_de"),
|
||||
"name_en": schema_row.get("name_en"),
|
||||
"description_de": schema_row.get("description_de"),
|
||||
"description_en": schema_row.get("description_en"),
|
||||
}
|
||||
|
||||
|
||||
def _validation_rules_dict(raw: Any) -> Dict[str, Any]:
|
||||
if isinstance(raw, dict):
|
||||
return raw
|
||||
|
|
@ -357,6 +375,7 @@ def merge_column_backed_and_eav_metrics(
|
|||
"data_type": dt,
|
||||
"unit": unit,
|
||||
"value": val,
|
||||
**_metric_human_labels(s),
|
||||
}
|
||||
)
|
||||
used_column = True
|
||||
|
|
@ -377,6 +396,7 @@ def merge_column_backed_and_eav_metrics(
|
|||
"data_type": dt,
|
||||
"unit": unit,
|
||||
"value": val,
|
||||
**_metric_human_labels(s),
|
||||
}
|
||||
)
|
||||
keys_handled.add(k)
|
||||
|
|
@ -395,6 +415,7 @@ def merge_column_backed_and_eav_metrics(
|
|||
"data_type": dt,
|
||||
"unit": unit,
|
||||
"value": val,
|
||||
**_metric_human_labels(s),
|
||||
}
|
||||
)
|
||||
keys_handled.add(k)
|
||||
|
|
@ -403,7 +424,9 @@ def merge_column_backed_and_eav_metrics(
|
|||
pass
|
||||
|
||||
if k in eav_by_key:
|
||||
merged.append(dict(eav_by_key[k]))
|
||||
row = dict(eav_by_key[k])
|
||||
row.update(_metric_human_labels(s))
|
||||
merged.append(row)
|
||||
keys_handled.add(k)
|
||||
|
||||
merged.sort(key=lambda x: x["key"])
|
||||
|
|
@ -699,6 +722,15 @@ def enrich_sessions_with_metrics(cur, sessions: List[Dict[str, Any]]) -> None:
|
|||
eav_list = by_act.get(aid, [])
|
||||
merged = merge_column_backed_and_eav_metrics(header, schema, eav_list)
|
||||
s["session_metrics"] = [
|
||||
{"key": m["key"], "data_type": m["data_type"], "unit": m["unit"], "value": m["value"]}
|
||||
{
|
||||
"key": m["key"],
|
||||
"data_type": m["data_type"],
|
||||
"unit": m["unit"],
|
||||
"value": m["value"],
|
||||
"name_de": m.get("name_de"),
|
||||
"name_en": m.get("name_en"),
|
||||
"description_de": m.get("description_de"),
|
||||
"description_en": m.get("description_en"),
|
||||
}
|
||||
for m in merged
|
||||
]
|
||||
|
|
|
|||
|
|
@ -143,7 +143,8 @@ def register_activity_session_insights():
|
|||
"duration_min, kcal_active, hr_avg, hr_max, rpe, training_category, training_type_name, "
|
||||
"session_metrics[]. "
|
||||
"session_metrics: effektive Liste nach merge_column_backed_and_eav_metrics — Einträge mit "
|
||||
"training_parameter_id, key, data_type, unit, value; nur Parameter aus Attributschema "
|
||||
"training_parameter_id, key, data_type, unit, value, name_de/name_en, description_de/description_en; "
|
||||
"nur Parameter aus Attributschema "
|
||||
"(training_category_parameter + training_type_parameter Overrides), keys sortiert. "
|
||||
"Kanon Lesen: activity_log-Spalte vor EAV bei Konflikt. "
|
||||
"meta: weeks_requested, days_loaded, session_count, confidence. "
|
||||
|
|
@ -170,6 +171,7 @@ def register_activity_session_insights():
|
|||
"session_metrics oft [] (kein Typ, kein Profil, keine gespeicherten Werte). "
|
||||
"Anzahl und Namen der Metrik-Keys sind instanz-/adminabhängig — JSON nicht als festes Schema "
|
||||
"für Downstream-Parsing harter Logik verwenden. "
|
||||
"Für KI-Semantik zusätzlich {{training_parameters_glossary_md}} (gesamter aktiver Katalog) in den Prompt legen. "
|
||||
"Composite-Parameter (JSON in EAV) noch nicht im MVP expandiert; ggf. Roh-value_text in späterer Phase."
|
||||
),
|
||||
layer_1_decision="activity_metrics.get_training_sessions_recent_weeks_data",
|
||||
|
|
@ -192,5 +194,61 @@ def register_activity_session_insights():
|
|||
_ev(pj, "known_limitations", EvidenceType.MIXED)
|
||||
register_placeholder(pj)
|
||||
|
||||
md_gloss = PlaceholderMetadata(
|
||||
key="training_parameters_glossary_md",
|
||||
category="Aktivität",
|
||||
description=(
|
||||
"Markdown-Tabelle: alle aktiven training_parameters (key, DE/EN, Beschreibungen, Typ, Einheit, Kategorie). "
|
||||
"Ergänzung zu training_sessions_recent_json für KI (Bedeutung dynamischer Metrik-Keys)."
|
||||
),
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="get_training_parameters_glossary_md",
|
||||
data_layer_module="backend/data_layer/activity_metrics.py",
|
||||
data_layer_function="get_training_parameters_ki_glossary_data",
|
||||
source_tables=["training_parameters"],
|
||||
semantic_contract=(
|
||||
"SELECT auf training_parameters WHERE is_active; sortiert category, key. "
|
||||
"profile_id-Parameter im Resolver reserviert, aktuell globaler Katalog."
|
||||
),
|
||||
business_meaning="KI: Legende zu session_metrics-Keys und Custom-Parametern",
|
||||
unit="Markdown",
|
||||
time_window="n/a (Katalog-Snapshot)",
|
||||
output_type=OutputType.TEXT_SUMMARY,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="GitHub-Flavored Markdown-Tabelle",
|
||||
example_output="| Feld (key) | DE | EN | Beschreibung DE | … |",
|
||||
minimum_data_requirements="Optional leer → Kurztext statt Tabelle",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Immer verfügbar wenn DB erreichbar",
|
||||
missing_value_policy=MissingValuePolicy(
|
||||
available=False,
|
||||
value_raw=None,
|
||||
missing_reason="no_data",
|
||||
legacy_display="Keine aktiven Trainingsparameter im Katalog.",
|
||||
),
|
||||
known_limitations=(
|
||||
"Keine profil-spezifische Einschränkung auf tatsächlich genutzte Keys (V2). "
|
||||
"Tabellen können bei großem Katalog lang werden."
|
||||
),
|
||||
layer_1_decision="activity_metrics.get_training_parameters_ki_glossary_data",
|
||||
layer_2a_decision="get_training_parameters_glossary_md",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 2a",
|
||||
evidence={},
|
||||
)
|
||||
for f in (
|
||||
"key", "category", "description", "resolver_module", "resolver_function",
|
||||
"data_layer_module", "data_layer_function", "source_tables", "semantic_contract",
|
||||
"unit", "time_window", "output_type", "placeholder_type", "format_hint",
|
||||
"example_output", "minimum_data_requirements", "confidence_logic",
|
||||
"missing_value_policy", "layer_1_decision", "layer_2a_decision",
|
||||
"layer_2b_reuse_possible", "architecture_alignment", "issue_53_alignment",
|
||||
):
|
||||
_ev(md_gloss, f)
|
||||
_ev(md_gloss, "business_meaning", EvidenceType.DRAFT_DERIVED)
|
||||
_ev(md_gloss, "known_limitations", EvidenceType.MIXED)
|
||||
register_placeholder(md_gloss)
|
||||
|
||||
|
||||
register_activity_session_insights()
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from data_layer.activity_metrics import (
|
|||
get_training_frequency_by_type_data,
|
||||
get_training_inter_session_gap_data,
|
||||
get_training_sessions_recent_weeks_data,
|
||||
get_training_parameters_ki_glossary_data,
|
||||
)
|
||||
from data_layer.recovery_metrics import (
|
||||
get_sleep_duration_data,
|
||||
|
|
@ -426,7 +427,8 @@ def get_activity_detail(profile_id: str, days: int = 14) -> str:
|
|||
k, v = m.get("key"), m.get("value")
|
||||
if k is None or v is None:
|
||||
continue
|
||||
eav_parts.append(f"{k}={v}")
|
||||
label = m.get("name_de") or m.get("name_en") or k
|
||||
eav_parts.append(f"{label} ({k})={v}")
|
||||
eav_str = f" | EAV: {'; '.join(eav_parts)}" if eav_parts else ""
|
||||
lines.append(
|
||||
f"{activity['date']}: {activity['activity_type']} "
|
||||
|
|
@ -456,6 +458,45 @@ def get_trainingstyp_verteilung(profile_id: str, days: int = 14) -> str:
|
|||
return ", ".join(parts)
|
||||
|
||||
|
||||
def get_training_parameters_glossary_md(profile_id: str) -> str:
|
||||
"""
|
||||
Markdown-Tabelle: alle aktiven training_parameters (key, Namen, Beschreibungen, Typ, Einheit).
|
||||
Für KI neben session_metrics / training_sessions_recent_json.
|
||||
"""
|
||||
data = get_training_parameters_ki_glossary_data(profile_id)
|
||||
params = data.get("parameters") or []
|
||||
if not params:
|
||||
return "Keine aktiven Trainingsparameter im Katalog."
|
||||
|
||||
def cell(x: object) -> str:
|
||||
if x is None:
|
||||
return "—"
|
||||
return str(x).replace("|", "·").replace("\n", " ").strip()[:400]
|
||||
|
||||
lines = [
|
||||
"| Feld (key) | DE | EN | Beschreibung DE | Beschreibung EN | Typ | Einheit | Kategorie |",
|
||||
"|---|---|---|---|---|---|---|---|",
|
||||
]
|
||||
for p in params:
|
||||
lines.append(
|
||||
"| "
|
||||
+ " | ".join(
|
||||
[
|
||||
cell(p.get("key")),
|
||||
cell(p.get("name_de")),
|
||||
cell(p.get("name_en")),
|
||||
cell(p.get("description_de")),
|
||||
cell(p.get("description_en")),
|
||||
cell(p.get("data_type")),
|
||||
cell(p.get("unit")),
|
||||
cell(p.get("category")),
|
||||
]
|
||||
)
|
||||
+ " |"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def get_training_frequency_by_type_md(profile_id: str, days: int = 28) -> str:
|
||||
"""
|
||||
Markdown-Tabelle: pro Trainingsart (Roh-Label) Ø Sessions/Woche, Dauer, kcal, HF, RPE, kcal/min.
|
||||
|
|
@ -1545,6 +1586,7 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
|
|||
'{{training_frequency_by_type_md}}': get_training_frequency_by_type_md,
|
||||
'{{training_inter_session_gap_md}}': get_training_inter_session_gap_md,
|
||||
'{{training_sessions_recent_json}}': lambda pid: _safe_json('training_sessions_recent_json', pid),
|
||||
'{{training_parameters_glossary_md}}': get_training_parameters_glossary_md,
|
||||
|
||||
# Schlaf & Erholung (10 Registry-Keys; recovery_score hier, nicht unter Meta Scores)
|
||||
'{{sleep_avg_duration}}': lambda pid: get_sleep_avg_duration(pid, 7),
|
||||
|
|
@ -1749,6 +1791,7 @@ def get_available_placeholders(categories: Optional[List[str]] = None) -> Dict[s
|
|||
'{{proxy_internal_load_7d}}', '{{monotony_score}}', '{{strain_score}}',
|
||||
'{{rest_day_compliance}}', '{{vo2max_trend_28d}}', '{{activity_score}}',
|
||||
'{{training_frequency_by_type_md}}', '{{training_inter_session_gap_md}}', '{{training_sessions_recent_json}}',
|
||||
'{{training_parameters_glossary_md}}',
|
||||
],
|
||||
'schlaf': [
|
||||
'{{sleep_avg_duration}}', '{{sleep_avg_quality}}', '{{rest_days_count}}',
|
||||
|
|
|
|||
|
|
@ -97,6 +97,52 @@ def test_merge_type_overrides_required_and_sort():
|
|||
assert merged[0]["required"] is True
|
||||
|
||||
|
||||
def test_merge_parameter_schema_includes_descriptions():
|
||||
cat = [
|
||||
{
|
||||
"training_parameter_id": 1,
|
||||
"cat_sort": 0,
|
||||
"cat_required": False,
|
||||
"cat_ui_group": None,
|
||||
"key": "custom_w",
|
||||
"name_de": "Leistung",
|
||||
"name_en": "Watts",
|
||||
"description_de": "Mittlere 5-Min-Leistung",
|
||||
"description_en": "5 min average power",
|
||||
"param_category": "performance",
|
||||
"data_type": "integer",
|
||||
"unit": "W",
|
||||
"validation_rules": {},
|
||||
"source_field": None,
|
||||
}
|
||||
]
|
||||
merged = merge_parameter_schema_rows(cat, [])
|
||||
assert merged[0]["description_de"] == "Mittlere 5-Min-Leistung"
|
||||
assert merged[0]["description_en"] == "5 min average power"
|
||||
|
||||
|
||||
def test_merge_column_backed_includes_human_labels_from_schema():
|
||||
schema = [
|
||||
{
|
||||
"training_parameter_id": 1,
|
||||
"key": "watts",
|
||||
"data_type": "integer",
|
||||
"unit": "W",
|
||||
"validation_rules": {},
|
||||
"source_field": "avg_power",
|
||||
"name_de": "Leistung",
|
||||
"name_en": "Power",
|
||||
"description_de": "Gerätewert",
|
||||
"description_en": "Device reading",
|
||||
}
|
||||
]
|
||||
out = merge_column_backed_and_eav_metrics({"avg_power": 200}, schema, [])
|
||||
assert len(out) == 1
|
||||
assert out[0]["value"] == 200
|
||||
assert out[0]["name_de"] == "Leistung"
|
||||
assert out[0]["description_en"] == "Device reading"
|
||||
|
||||
|
||||
def test_merge_type_adds_parameter_not_in_category():
|
||||
typ = [_ttp_row(7, "cadence", typ_sort=1, typ_required=True, data_type="integer")]
|
||||
merged = merge_parameter_schema_rows([], typ)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ const emptyParamForm = () => ({
|
|||
key: '',
|
||||
name_de: '',
|
||||
name_en: '',
|
||||
description_de: '',
|
||||
description_en: '',
|
||||
category: 'physical',
|
||||
data_type: 'float',
|
||||
unit: '',
|
||||
|
|
@ -130,6 +132,8 @@ export default function AdminActivityAttributeProfilesPage() {
|
|||
key: paramForm.key.trim().toLowerCase(),
|
||||
name_de: paramForm.name_de.trim(),
|
||||
name_en: paramForm.name_en.trim(),
|
||||
description_de: paramForm.description_de.trim() || null,
|
||||
description_en: paramForm.description_en.trim() || null,
|
||||
category: paramForm.category,
|
||||
data_type: paramForm.data_type,
|
||||
unit: paramForm.unit.trim() || null,
|
||||
|
|
@ -153,6 +157,8 @@ export default function AdminActivityAttributeProfilesPage() {
|
|||
await api.adminUpdateTrainingParameter(editParam.id, {
|
||||
name_de: editParam.name_de.trim(),
|
||||
name_en: editParam.name_en.trim(),
|
||||
description_de: editParam.description_de?.trim() || null,
|
||||
description_en: editParam.description_en?.trim() || null,
|
||||
category: editParam.category,
|
||||
data_type: editParam.data_type,
|
||||
unit: editParam.unit?.trim() || null,
|
||||
|
|
@ -302,6 +308,11 @@ export default function AdminActivityAttributeProfilesPage() {
|
|||
Nach Migration <strong>055</strong> werden Standard-Parameter allen Kategorien zugeordnet und vorhandene{' '}
|
||||
<code>activity_log</code>-Spalten idempotent nach EAV gespiegelt (sofern noch keine EAV-Zeile existiert).
|
||||
</li>
|
||||
<li>
|
||||
<strong>KI:</strong> Bei eigenen / unklaren Metriken kurze <strong>Beschreibung DE/EN</strong> im Katalog
|
||||
pflegen — sie erscheinen in Export/Platzhalter-Kontext (<code>training_sessions_recent_json</code>,{' '}
|
||||
<code>{'{{training_parameters_glossary_md}}'}</code>).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -394,6 +405,23 @@ export default function AdminActivityAttributeProfilesPage() {
|
|||
onChange={(e) => setParamForm((f) => ({ ...f, name_en: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Beschreibung DE / EN (optional, für KI)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
placeholder="Was bedeutet der Wert? Einheit/Skala?"
|
||||
value={paramForm.description_de}
|
||||
onChange={(e) => setParamForm((f) => ({ ...f, description_de: e.target.value }))}
|
||||
/>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
placeholder="Short meaning for prompts / EN users"
|
||||
value={paramForm.description_en}
|
||||
onChange={(e) => setParamForm((f) => ({ ...f, description_en: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Gruppe / Datentyp</label>
|
||||
<select
|
||||
|
|
@ -468,6 +496,21 @@ export default function AdminActivityAttributeProfilesPage() {
|
|||
onChange={(e) => setEditParam((p) => ({ ...p, name_en: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Beschreibung DE / EN (optional, für KI)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={editParam.description_de || ''}
|
||||
onChange={(e) => setEditParam((p) => ({ ...p, description_de: e.target.value }))}
|
||||
/>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={editParam.description_en || ''}
|
||||
onChange={(e) => setEditParam((p) => ({ ...p, description_en: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Gruppe / Typ</label>
|
||||
<select
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user