Commit 134a9c5c authored by Kantz's avatar Kantz
Browse files

subsection struktur elemente entfertn

parent f3444904
...@@ -79,7 +79,7 @@ You can open a socratic chat directly with URL query parameters: ...@@ -79,7 +79,7 @@ You can open a socratic chat directly with URL query parameters:
Example: Example:
`http://localhost:5173/chat?orchestrator=socratic&subsection_key=quadratische-gleichungen` `http://localhost:5173/chat?orchestrator=socratic&subsection_key=mengen`
Notes: Notes:
......
...@@ -29,7 +29,7 @@ class ChatRequest(BaseModel): ...@@ -29,7 +29,7 @@ class ChatRequest(BaseModel):
messages: List[ChatMessage] messages: List[ChatMessage]
draft: Optional[str] = None draft: Optional[str] = None
selected_task: Optional[dict[str, str]] = None selected_task: Optional[dict[str, str]] = None
selected_subsection: Optional[dict[str, str]] = None selected_topic: Optional[dict[str, str]] = None
orchestrator: Optional[str] = None orchestrator: Optional[str] = None
...@@ -56,8 +56,8 @@ class SelectedTaskRef(BaseModel): ...@@ -56,8 +56,8 @@ class SelectedTaskRef(BaseModel):
task_id: str task_id: str
class SelectedSubsectionRef(BaseModel): class SelectedTopicRef(BaseModel):
subsection_key: str topic_key: str
class ChatArchiveDetail(BaseModel): class ChatArchiveDetail(BaseModel):
...@@ -65,13 +65,13 @@ class ChatArchiveDetail(BaseModel): ...@@ -65,13 +65,13 @@ class ChatArchiveDetail(BaseModel):
saved_at: str saved_at: str
history: List[ChatMessage] history: List[ChatMessage]
selected_task: Optional[SelectedTaskRef] = None selected_task: Optional[SelectedTaskRef] = None
selected_subsection: Optional[SelectedSubsectionRef] = None selected_topic: Optional[SelectedTopicRef] = None
orchestrator: str orchestrator: str
class SocraticBootstrapRequest(BaseModel): class SocraticBootstrapRequest(BaseModel):
draft: str = Field(..., min_length=1) draft: str = Field(..., min_length=1)
subsection_key: str = Field(..., min_length=1) topic_key: str = Field(..., min_length=1)
class SocraticBootstrapResponse(BaseModel): class SocraticBootstrapResponse(BaseModel):
...@@ -99,7 +99,7 @@ def chat(request: ChatRequest) -> ChatResponse: ...@@ -99,7 +99,7 @@ def chat(request: ChatRequest) -> ChatResponse:
payload_messages, payload_messages,
draft=request.draft, draft=request.draft,
selected_task=request.selected_task, selected_task=request.selected_task,
selected_subsection=request.selected_subsection, selected_topic=request.selected_topic,
) )
else: else:
result = orchestrator_impl.run_chat( result = orchestrator_impl.run_chat(
...@@ -145,19 +145,19 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail: ...@@ -145,19 +145,19 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail:
if file_id and task_id: if file_id and task_id:
selected_task = SelectedTaskRef(file_id=file_id, task_id=task_id) selected_task = SelectedTaskRef(file_id=file_id, task_id=task_id)
selected_subsection_raw = record.get("selected_subsection") selected_topic_raw = record.get("selected_topic")
selected_subsection: Optional[SelectedSubsectionRef] = None selected_topic: Optional[SelectedTopicRef] = None
if isinstance(selected_subsection_raw, dict): if isinstance(selected_topic_raw, dict):
subsection_key = str(selected_subsection_raw.get("subsection_key", "")).strip() topic_key = str(selected_topic_raw.get("topic_key", "")).strip()
if subsection_key: if topic_key:
selected_subsection = SelectedSubsectionRef(subsection_key=subsection_key) selected_topic = SelectedTopicRef(topic_key=topic_key)
return ChatArchiveDetail( return ChatArchiveDetail(
chat_id=record["chat_id"], chat_id=record["chat_id"],
saved_at=record.get("saved_at", ""), saved_at=record.get("saved_at", ""),
history=[ChatMessage(role=item["role"], text=item["text"]) for item in record["history"]], history=[ChatMessage(role=item["role"], text=item["text"]) for item in record["history"]],
selected_task=selected_task, selected_task=selected_task,
selected_subsection=selected_subsection, selected_topic=selected_topic,
orchestrator=record.get("orchestrator") or get_default_orchestrator(), orchestrator=record.get("orchestrator") or get_default_orchestrator(),
) )
...@@ -192,10 +192,13 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe ...@@ -192,10 +192,13 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
if not sheet: if not sheet:
sheet = context_store.context_store_new.init_sheet(chat_id, []) sheet = context_store.context_store_new.init_sheet(chat_id, [])
if not task_catalog.select_subsection_by_key(sheet, request.subsection_key): try:
raise HTTPException(status_code=404, detail="subsection not found") topic_entry = socratic_oranisator.get_topic_entry(request.topic_key)
except ValueError as exc:
raise HTTPException(status_code=404, detail="topic not found") from exc
refs = task_catalog.get_selected_subsection_parent_refs(sheet) task_catalog.set_selected_topic(sheet, {}, topic_entry)
refs = task_catalog.get_selected_topic_parent_refs(sheet)
sources = retrieval_store.retrieve_for_parent_refs( sources = retrieval_store.retrieve_for_parent_refs(
pg_url=config.get_postgres_url(), pg_url=config.get_postgres_url(),
parent_refs=refs, parent_refs=refs,
...@@ -205,7 +208,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe ...@@ -205,7 +208,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
context_store.save_sheet(sheet) context_store.save_sheet(sheet)
try: try:
reply = socratic_oranisator.get_initial_message(request.subsection_key) reply = socratic_oranisator.get_initial_message(request.topic_key)
except ValueError as exc: except ValueError as exc:
logger.exception("Socratic bootstrap prompt lookup failed") logger.exception("Socratic bootstrap prompt lookup failed")
raise HTTPException(status_code=500, detail=str(exc)) from exc raise HTTPException(status_code=500, detail=str(exc)) from exc
......
...@@ -23,8 +23,8 @@ class TaskItem(BaseModel): ...@@ -23,8 +23,8 @@ class TaskItem(BaseModel):
task_id: str task_id: str
class SubsectionEntry(BaseModel): class TopicEntry(BaseModel):
subsection_key: str topic_key: str
label: str label: str
level: str = "" level: str = ""
refs: List[List[int]] refs: List[List[int]]
...@@ -54,7 +54,7 @@ class TaskDetailsResponse(BaseModel): ...@@ -54,7 +54,7 @@ class TaskDetailsResponse(BaseModel):
class SocraticResponse(BaseModel): class SocraticResponse(BaseModel):
orchestrator: str orchestrator: str
enabled: bool enabled: bool
subsections: List[SubsectionEntry] = Field(default_factory=list) topics: List[TopicEntry] = Field(default_factory=list)
class SelectTaskRequest(BaseModel): class SelectTaskRequest(BaseModel):
draft: str = Field(..., min_length=1) draft: str = Field(..., min_length=1)
...@@ -68,14 +68,23 @@ class SelectTaskResponse(BaseModel): ...@@ -68,14 +68,23 @@ class SelectTaskResponse(BaseModel):
task_id: str task_id: str
class SelectSubsectionRequest(BaseModel): class SelectTopicRequest(BaseModel):
draft: str = Field(..., min_length=1) draft: str = Field(..., min_length=1)
subsection_key: str = Field(..., min_length=1) topic_key: str = Field(..., min_length=1)
class SelectSubsectionResponse(BaseModel): class SelectTopicResponse(BaseModel):
status: str status: str
subsection_key: str topic_key: str
def _set_selected_socratic_topic(sheet: dict[str, object], topic_key: str) -> str:
entry = socratic_oranisator.get_topic_entry(topic_key)
task_catalog.set_selected_topic(sheet, {}, entry)
_, selected_topic_key = task_catalog.get_selected_topic_ids(sheet)
if not selected_topic_key:
raise HTTPException(status_code=404, detail="topic not found")
return selected_topic_key
@router.get("/api/tasks/assets/{asset_path:path}") @router.get("/api/tasks/assets/{asset_path:path}")
...@@ -122,14 +131,14 @@ def get_task_details(file_id: str, task_id: str) -> TaskDetailsResponse: ...@@ -122,14 +131,14 @@ def get_task_details(file_id: str, task_id: str) -> TaskDetailsResponse:
return TaskDetailsResponse(**payload) return TaskDetailsResponse(**payload)
@router.get("/api/tasks/socratic-subsections", response_model=SocraticResponse) @router.get("/api/tasks/socratic-topics", response_model=SocraticResponse)
def list_socratic_subsections() -> SocraticResponse: def list_socratic_topics() -> SocraticResponse:
orchestrator = config.get_orchestrator() orchestrator = config.get_orchestrator()
subsections = socratic_oranisator.build_subsection_catalog() topics = socratic_oranisator.build_topic_catalog()
return SocraticResponse( return SocraticResponse(
orchestrator=orchestrator, orchestrator=orchestrator,
enabled=orchestrator == "socratic", enabled=orchestrator == "socratic",
subsections=subsections, topics=topics,
) )
@router.post("/api/tasks/select", response_model=SelectTaskResponse) @router.post("/api/tasks/select", response_model=SelectTaskResponse)
...@@ -156,20 +165,20 @@ def select_task(request: SelectTaskRequest) -> SelectTaskResponse: ...@@ -156,20 +165,20 @@ def select_task(request: SelectTaskRequest) -> SelectTaskResponse:
) )
@router.post("/api/tasks/select-subsection", response_model=SelectSubsectionResponse) @router.post("/api/tasks/select-topic", response_model=SelectTopicResponse)
def select_subsection(request: SelectSubsectionRequest) -> SelectSubsectionResponse: def select_topic(request: SelectTopicRequest) -> SelectTopicResponse:
chat_id = context_store.get_chat_id([], draft=request.draft) chat_id = context_store.get_chat_id([], draft=request.draft)
sheet = context_store.load_sheet(chat_id) sheet = context_store.load_sheet(chat_id)
if not sheet: if not sheet:
sheet = context_store.context_store_new.init_sheet(chat_id, []) sheet = context_store.context_store_new.init_sheet(chat_id, [])
updated = task_catalog.select_subsection_by_key(sheet, subsection_key=request.subsection_key) try:
if not updated: topic_key = _set_selected_socratic_topic(sheet, request.topic_key)
raise HTTPException(status_code=404, detail="subsection not found") except ValueError as exc:
raise HTTPException(status_code=404, detail="topic not found") from exc
context_store.save_sheet(sheet) context_store.save_sheet(sheet)
_, subsection_key = task_catalog.get_selected_subsection_ids(sheet) return SelectTopicResponse(
return SelectSubsectionResponse(
status="ok", status="ok",
subsection_key=subsection_key or request.subsection_key, topic_key=topic_key,
) )
...@@ -6,22 +6,22 @@ from app.deterministic_services import context_store, retrieval_store, task_cata ...@@ -6,22 +6,22 @@ from app.deterministic_services import context_store, retrieval_store, task_cata
from app.deterministic_services.orchestrators import orchestrator_base as base from app.deterministic_services.orchestrators import orchestrator_base as base
def _apply_selected_subsection( def _apply_selected_topic(
state: base.ChatState, state: base.ChatState,
selected_subsection: dict | None, selected_topic: dict | None,
) -> None: ) -> None:
if not selected_subsection: if not selected_topic:
return return
subsection_key = str(selected_subsection.get("subsection_key", "")).strip() topic_key = str(selected_topic.get("topic_key", "")).strip()
if not subsection_key: if not topic_key:
return return
task_catalog.select_subsection_by_key(state.sheet, subsection_key) task_catalog.select_topic_by_key(state.sheet, topic_key)
def _retrieve_context_for_subsection(state: base.ChatState, query_text: str) -> int: def _retrieve_context_for_topic(state: base.ChatState, query_text: str) -> int:
refs = task_catalog.get_selected_subsection_parent_refs(state.sheet) refs = task_catalog.get_selected_topic_parent_refs(state.sheet)
if not refs: if not refs:
return 0 return 0
...@@ -49,17 +49,17 @@ def _retrieve_context_for_subsection(state: base.ChatState, query_text: str) -> ...@@ -49,17 +49,17 @@ def _retrieve_context_for_subsection(state: base.ChatState, query_text: str) ->
def _on_bootstrap(state: base.ChatState, query_text: str) -> None: def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
_retrieve_context_for_subsection(state, query_text) _retrieve_context_for_topic(state, query_text)
def _on_turn_logic(state: base.ChatState) -> None: def _on_turn_logic(state: base.ChatState) -> None:
if not task_catalog.get_selected_subsection_parent_refs(state.sheet): if not task_catalog.get_selected_topic_parent_refs(state.sheet):
return return
def _on_build_reply(state: base.ChatState) -> str | None: def _on_build_reply(state: base.ChatState) -> str | None:
history_turns = context_store.get_history_turns(state.sheet) history_turns = context_store.get_history_turns(state.sheet)
parent_refs = task_catalog.get_selected_subsection_parent_refs(state.sheet) parent_refs = task_catalog.get_selected_topic_parent_refs(state.sheet)
args = { args = {
"query": state.last_user, "query": state.last_user,
"parent_refs": parent_refs, "parent_refs": parent_refs,
...@@ -77,11 +77,11 @@ def _on_build_reply(state: base.ChatState) -> str | None: ...@@ -77,11 +77,11 @@ def _on_build_reply(state: base.ChatState) -> str | None:
def run_chat( def run_chat(
messages: list[dict], messages: list[dict],
draft: str | None = None, draft: str | None = None,
selected_subsection: dict | None = None, selected_topic: dict | None = None,
selected_task: dict | None = None, selected_task: dict | None = None,
) -> dict: ) -> dict:
def _apply_selected_context(state: base.ChatState) -> None: def _apply_selected_context(state: base.ChatState) -> None:
_apply_selected_subsection(state, selected_subsection) _apply_selected_topic(state, selected_topic)
def on_bootstrap(state: base.ChatState, query_text: str) -> None: def on_bootstrap(state: base.ChatState, query_text: str) -> None:
_apply_selected_context(state) _apply_selected_context(state)
......
...@@ -123,7 +123,7 @@ def run_chat( ...@@ -123,7 +123,7 @@ def run_chat(
messages: list[dict], messages: list[dict],
draft: str | None = None, draft: str | None = None,
selected_task: dict | None = None, selected_task: dict | None = None,
selected_subsection: dict | None = None, selected_topic: dict | None = None,
) -> dict: ) -> dict:
def _apply_selected_task(state: base.ChatState) -> None: def _apply_selected_task(state: base.ChatState) -> None:
if not selected_task: if not selected_task:
......
...@@ -26,11 +26,11 @@ def _extract_selected_task(sheet: dict[str, Any]) -> dict[str, str] | None: ...@@ -26,11 +26,11 @@ def _extract_selected_task(sheet: dict[str, Any]) -> dict[str, str] | None:
return {"file_id": file_id, "task_id": task_id} return {"file_id": file_id, "task_id": task_id}
def _extract_selected_subsection(sheet: dict[str, Any]) -> dict[str, str] | None: def _extract_selected_topic(sheet: dict[str, Any]) -> dict[str, str] | None:
subsection_key = str(sheet.get("selected_subsection_key", "")).strip() topic_key = str(sheet.get("selected_topic_key", "")).strip()
if not subsection_key: if not topic_key:
return None return None
return {"subsection_key": subsection_key} return {"topic_key": topic_key}
def archive_chat( def archive_chat(
...@@ -56,7 +56,7 @@ def archive_chat( ...@@ -56,7 +56,7 @@ def archive_chat(
"math_solutions": sheet.get("math_solutions", []), "math_solutions": sheet.get("math_solutions", []),
"sources": sheet.get("sources", []), "sources": sheet.get("sources", []),
"selected_task": _extract_selected_task(sheet), "selected_task": _extract_selected_task(sheet),
"selected_subsection": _extract_selected_subsection(sheet), "selected_topic": _extract_selected_topic(sheet),
} }
os.makedirs(_LOG_DIR, exist_ok=True) os.makedirs(_LOG_DIR, exist_ok=True)
...@@ -134,12 +134,12 @@ def load_archive(chat_id: str) -> dict[str, Any] | None: ...@@ -134,12 +134,12 @@ def load_archive(chat_id: str) -> dict[str, Any] | None:
if file_id and task_id: if file_id and task_id:
selected_task = {"file_id": file_id, "task_id": task_id} selected_task = {"file_id": file_id, "task_id": task_id}
selected_subsection_raw = record.get("selected_subsection") selected_topic_raw = record.get("selected_topic")
selected_subsection: dict[str, str] | None = None selected_topic: dict[str, str] | None = None
if isinstance(selected_subsection_raw, dict): if isinstance(selected_topic_raw, dict):
subsection_key = str(selected_subsection_raw.get("subsection_key", "")).strip() topic_key = str(selected_topic_raw.get("topic_key", "")).strip()
if subsection_key: if topic_key:
selected_subsection = {"subsection_key": subsection_key} selected_topic = {"topic_key": topic_key}
return { return {
"chat_id": record.get("chat_id", chat_id), "chat_id": record.get("chat_id", chat_id),
...@@ -147,6 +147,6 @@ def load_archive(chat_id: str) -> dict[str, Any] | None: ...@@ -147,6 +147,6 @@ def load_archive(chat_id: str) -> dict[str, Any] | None:
"orchestrator": record.get("orchestrator"), "orchestrator": record.get("orchestrator"),
"history": history, "history": history,
"selected_task": selected_task, "selected_task": selected_task,
"selected_subsection": selected_subsection, "selected_topic": selected_topic,
} }
return None return None
...@@ -35,60 +35,75 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str, ...@@ -35,60 +35,75 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
return items return items
def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str: def get_initial_message(topic_key: str, path: Path = PROMPTS_PATH) -> str:
normalized_key = task_catalog._normalize_parent_ref_key(subsection_key) normalized_key = task_catalog._normalize_parent_ref_key(topic_key)
if not normalized_key: if not normalized_key:
raise ValueError("invalid subsection key") raise ValueError("invalid topic key")
items = load_initial_prompt_items(path) items = load_initial_prompt_items(path)
item = items.get(normalized_key) item = items.get(normalized_key)
if item is None: if item is None:
raise ValueError(f"missing socratic bootstrap prompt for subsection '{normalized_key}'") raise ValueError(f"missing socratic bootstrap prompt for topic '{normalized_key}'")
message = str(item.get("inital_message") or "").strip() message = str(item.get("inital_message") or "").strip()
if not message: if not message:
raise ValueError(f"empty socratic bootstrap prompt for subsection '{normalized_key}'") raise ValueError(f"empty socratic bootstrap prompt for topic '{normalized_key}'")
return message return message
def build_subsection_catalog(path: Path = PROMPTS_PATH) -> list[dict[str, Any]]: def get_topic_entry(topic_key: str, path: Path = PROMPTS_PATH) -> dict[str, Any]:
normalized_key = task_catalog._normalize_parent_ref_key(topic_key)
if not normalized_key:
raise ValueError("invalid topic key")
items = load_initial_prompt_items(path)
item = items.get(normalized_key)
if item is None:
raise ValueError(f"missing socratic bootstrap prompt for topic '{normalized_key}'")
refs: list[list[int]] = []
refs_raw = item.get("refs", [])
if isinstance(refs_raw, list):
for raw_ref in refs_raw:
parsed = task_catalog._parse_parent_ref(raw_ref)
if parsed is not None:
refs.append(task_catalog._parent_ref_to_list(parsed))
if not refs:
parsed_index = task_catalog._parse_parent_ref(str(item.get("index") or ""))
if parsed_index is not None:
refs.append(task_catalog._parent_ref_to_list(parsed_index))
if not refs:
raise ValueError(f"missing refs for topic '{normalized_key}'")
label_source = str(item.get("label") or item.get("topic") or item.get("subsection") or normalized_key)
return {
"topic_key": normalized_key,
"label": task_catalog._format_topic_label(label_source) or task_catalog._format_topic_label(normalized_key),
"level": str(item.get("level") or task_catalog._parent_ref_level(tuple(refs[0]))).strip(),
"refs": refs,
}
def build_topic_catalog(path: Path = PROMPTS_PATH) -> list[dict[str, Any]]:
items = load_initial_prompt_items(path) items = load_initial_prompt_items(path)
topic_summaries = task_catalog.load_topic_summaries() topic_summaries = task_catalog.load_topic_summaries()
response: list[dict[str, Any]] = [] response: list[dict[str, Any]] = []
for raw_key, item in items.items(): for raw_key, item in items.items():
subsection_key = task_catalog._normalize_parent_ref_key(str(raw_key)) topic_key = task_catalog._normalize_parent_ref_key(str(raw_key))
if not subsection_key: if not topic_key:
continue continue
try:
label_source = str(item.get("label") or item.get("subsection") or subsection_key) entry = get_topic_entry(topic_key, path)
label = task_catalog._format_subsection_label(label_source) except ValueError:
refs: list[list[int]] = [] continue
refs_raw = item.get("refs", []) entry["summary"] = topic_summaries.get(topic_key, "")
if isinstance(refs_raw, list): response.append(entry)
for raw_ref in refs_raw:
parsed = task_catalog._parse_parent_ref(raw_ref)
if parsed is not None:
refs.append(task_catalog._parent_ref_to_list(parsed))
if not refs:
parsed_index = task_catalog._parse_parent_ref(str(item.get("index") or ""))
if parsed_index is not None:
refs.append(task_catalog._parent_ref_to_list(parsed_index))
response.append(
{
"subsection_key": subsection_key,
"label": label or task_catalog._format_subsection_label(subsection_key),
"level": str(item.get("level") or (task_catalog._parent_ref_level(tuple(refs[0])) if refs else "")).strip(),
"refs": refs,
"summary": topic_summaries.get(subsection_key, ""),
}
)
response.sort( response.sort(
key=lambda item: ( key=lambda item: (
item["refs"][0] if item["refs"] else (9999, 9999, 9999, 9999), item["refs"][0] if item["refs"] else (9999, 9999, 9999, 9999),
item["subsection_key"], item["topic_key"],
) )
) )
return response return response
...@@ -18,13 +18,12 @@ TASKS_DIR = config.get_task_folder() ...@@ -18,13 +18,12 @@ TASKS_DIR = config.get_task_folder()
TASK_IMAGES_DIR = TASKS_DIR / "images" TASK_IMAGES_DIR = TASKS_DIR / "images"
SOURCES_DIR = Path(__file__).resolve().parents[2] / "sources" SOURCES_DIR = Path(__file__).resolve().parents[2] / "sources"
TOPIC_MAP_PATH = TASKS_DIR / "_topic_to_index_map.yaml" TOPIC_MAP_PATH = TASKS_DIR / "_topic_to_index_map.yaml"
SUBSECTION_MAP_PATH = TOPIC_MAP_PATH
TASK_ASSET_URL_PREFIX = "/api/tasks/assets" TASK_ASSET_URL_PREFIX = "/api/tasks/assets"
ParentRef = tuple[int, int, int, int] ParentRef = tuple[int, int, int, int]
def _normalize_parent_ref_key(value: str) -> str: def _normalize_parent_ref_key(value: str) -> str:
return _normalize_subsection_key(value) return _normalize_topic_key(value)
def _normalize_text(value: str) -> str: def _normalize_text(value: str) -> str:
return re.sub(r"\s+", " ", value.strip().lower()) return re.sub(r"\s+", " ", value.strip().lower())
...@@ -34,7 +33,7 @@ def _tokenize(value: str) -> set[str]: ...@@ -34,7 +33,7 @@ def _tokenize(value: str) -> set[str]:
return set(re.findall(r"[a-z0-9_]+", _normalize_text(value))) return set(re.findall(r"[a-z0-9_]+", _normalize_text(value)))
def _normalize_subsection_key(value: str) -> str: def _normalize_topic_key(value: str) -> str:
normalized = unicodedata.normalize("NFKD", str(value)) normalized = unicodedata.normalize("NFKD", str(value))
ascii_value = normalized.encode("ascii", "ignore").decode("ascii") ascii_value = normalized.encode("ascii", "ignore").decode("ascii")
collapsed = re.sub(r"[-_]+", " ", ascii_value.strip().lower()) collapsed = re.sub(r"[-_]+", " ", ascii_value.strip().lower())
...@@ -42,13 +41,6 @@ def _normalize_subsection_key(value: str) -> str: ...@@ -42,13 +41,6 @@ def _normalize_subsection_key(value: str) -> str:
return re.sub(r"\s+", " ", collapsed).strip() return re.sub(r"\s+", " ", collapsed).strip()
def _parse_subsection_ref(value: str) -> tuple[int, int, int] | None:
match = re.match(r"^\s*(\d+)\s*[-.]\s*(\d+)\s*[-.]\s*(\d+)\s*$", str(value))
if not match:
return None
return int(match.group(1)), int(match.group(2)), int(match.group(3))
def _parse_parent_ref(value: Any) -> ParentRef | None: def _parse_parent_ref(value: Any) -> ParentRef | None:
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
parts = [str(item).strip() for item in value if str(item).strip()] parts = [str(item).strip() for item in value if str(item).strip()]
...@@ -142,18 +134,7 @@ def load_topic_map(path: Path = TOPIC_MAP_PATH) -> dict[str, ParentRef]: ...@@ -142,18 +134,7 @@ def load_topic_map(path: Path = TOPIC_MAP_PATH) -> dict[str, ParentRef]:
return mapped return mapped
def load_subsection_map(path: Path = SUBSECTION_MAP_PATH) -> dict[str, tuple[int, int, int]]: def _extract_topic_summary(text: str) -> str:
topic_map = load_topic_map(path)
mapped: dict[str, tuple[int, int, int]] = {}
for key, ref in topic_map.items():
chap, sec, sub, subsub = ref
if sub <= 0 or subsub > 0:
continue
mapped[key] = (chap, sec, sub)
return mapped
def _extract_subsection_summary(text: str) -> str:
body = text.replace("\r\n", "\n").strip() body = text.replace("\r\n", "\n").strip()
if not body: if not body:
return "" return ""
...@@ -204,17 +185,13 @@ def load_topic_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]: ...@@ -204,17 +185,13 @@ def load_topic_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
or "" or ""
).strip() ).strip()
key = _normalize_parent_ref_key(title) key = _normalize_parent_ref_key(title)
summary = _extract_subsection_summary(body) summary = _extract_topic_summary(body)
if not key or not summary: if not key or not summary:
continue continue
summaries.setdefault(key, summary) summaries.setdefault(key, summary)
return summaries return summaries
def load_subsection_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
return load_topic_summaries(base_dir)
def _normalize_topic_ref_entries( def _normalize_topic_ref_entries(
raw_entries: Any, raw_entries: Any,
topic_map: dict[str, ParentRef], topic_map: dict[str, ParentRef],
...@@ -232,10 +209,10 @@ def _normalize_topic_ref_entries( ...@@ -232,10 +209,10 @@ def _normalize_topic_ref_entries(
if isinstance(raw_entry, str): if isinstance(raw_entry, str):
normalized_key = _normalize_parent_ref_key(raw_entry) normalized_key = _normalize_parent_ref_key(raw_entry)
label = _format_subsection_label(raw_entry) label = _format_topic_label(raw_entry)
ref = topic_map.get(normalized_key) ref = topic_map.get(normalized_key)
elif isinstance(raw_entry, dict): elif isinstance(raw_entry, dict):
raw_key = str(raw_entry.get("key") or raw_entry.get("topic_key") or raw_entry.get("subsection_key") or "").strip() raw_key = str(raw_entry.get("key") or raw_entry.get("topic_key") or "").strip()
normalized_key = _normalize_parent_ref_key(raw_key) if raw_key else "" normalized_key = _normalize_parent_ref_key(raw_key) if raw_key else ""
label = str(raw_entry.get("label") or "").strip() label = str(raw_entry.get("label") or "").strip()
summary = str(raw_entry.get("summary") or "").strip() summary = str(raw_entry.get("summary") or "").strip()
...@@ -273,9 +250,9 @@ def _resolve_task_topic_options( ...@@ -273,9 +250,9 @@ def _resolve_task_topic_options(
summaries = topic_summaries if topic_summaries is not None else load_topic_summaries() summaries = topic_summaries if topic_summaries is not None else load_topic_summaries()
raw_entries: list[Any] = [] raw_entries: list[Any] = []
legacy_topics = task_file.get("subsections", []) topics = task_file.get("topics", [])
if isinstance(legacy_topics, list): if isinstance(topics, list):
raw_entries.extend(legacy_topics) raw_entries.extend(topics)
explicit_refs = task_file.get("topic_refs", []) explicit_refs = task_file.get("topic_refs", [])
if isinstance(explicit_refs, list): if isinstance(explicit_refs, list):
raw_entries.extend(explicit_refs) raw_entries.extend(explicit_refs)
...@@ -303,25 +280,7 @@ def _resolve_task_topic_refs( ...@@ -303,25 +280,7 @@ def _resolve_task_topic_refs(
return sorted(refs) return sorted(refs)
def _resolve_task_subsection_refs( def _format_topic_label(value: str) -> str:
task_file: dict[str, Any],
subsection_map: dict[str, tuple[int, int, int]] | None = None,
) -> list[tuple[int, int, int]]:
mapping: dict[str, ParentRef]
if subsection_map is None:
mapping = load_topic_map()
else:
mapping = {key: (int(ref[0]), int(ref[1]), int(ref[2]), 0) for key, ref in subsection_map.items()}
refs = _resolve_task_topic_refs(task_file, topic_map=mapping)
normalized: list[tuple[int, int, int]] = []
for chap, sec, sub, subsub in refs:
if sub <= 0 or subsub > 0:
continue
normalized.append((chap, sec, sub))
return sorted(normalized)
def _format_subsection_label(value: str) -> str:
cleaned = re.sub(r"[-_]+", " ", value.strip()) cleaned = re.sub(r"[-_]+", " ", value.strip())
cleaned = re.sub(r"\s+", " ", cleaned).strip() cleaned = re.sub(r"\s+", " ", cleaned).strip()
if not cleaned: if not cleaned:
...@@ -337,7 +296,7 @@ def build_topic_catalog(path: Path = TOPIC_MAP_PATH) -> list[dict[str, Any]]: ...@@ -337,7 +296,7 @@ def build_topic_catalog(path: Path = TOPIC_MAP_PATH) -> list[dict[str, Any]]:
response.append( response.append(
{ {
"topic_key": key, "topic_key": key,
"label": _format_subsection_label(key), "label": _format_topic_label(key),
"level": _parent_ref_level(ref), "level": _parent_ref_level(ref),
"refs": [_parent_ref_to_list(ref)], "refs": [_parent_ref_to_list(ref)],
"summary": topic_summaries.get(key, ""), "summary": topic_summaries.get(key, ""),
...@@ -346,54 +305,6 @@ def build_topic_catalog(path: Path = TOPIC_MAP_PATH) -> list[dict[str, Any]]: ...@@ -346,54 +305,6 @@ def build_topic_catalog(path: Path = TOPIC_MAP_PATH) -> list[dict[str, Any]]:
return response return response
def build_subsection_catalog(path: Path = SUBSECTION_MAP_PATH) -> list[dict[str, Any]]:
response: list[dict[str, Any]] = []
for item in build_topic_catalog(path):
refs = item.get("refs", [])
if not refs:
continue
ref = refs[0]
response.append(
{
"subsection_key": item["topic_key"],
"label": item["label"],
"level": item["level"],
"refs": [[int(ref[0]), int(ref[1]), int(ref[2]), int(ref[3])]],
"summary": item["summary"],
}
)
return response
def _resolve_task_subsection_options(
task_file: dict[str, Any],
subsection_map: dict[str, tuple[int, int, int]] | None = None,
) -> list[dict[str, Any]]:
mapping: dict[str, ParentRef]
if subsection_map is None:
mapping = load_topic_map()
else:
mapping = {key: (int(ref[0]), int(ref[1]), int(ref[2]), 0) for key, ref in subsection_map.items()}
options = _resolve_task_topic_options(task_file, topic_map=mapping)
response: list[dict[str, Any]] = []
for option in options:
refs = option.get("refs", [])
if not refs:
continue
ref = refs[0]
response.append(
{
"subsection_key": option["topic_key"],
"label": option["label"],
"level": option["level"],
"refs": [[int(ref[0]), int(ref[1]), int(ref[2]), int(ref[3])]],
"summary": option.get("summary", ""),
}
)
return response
def _match_score(query_text: str, candidate_text: str) -> int: def _match_score(query_text: str, candidate_text: str) -> int:
query_tokens = _tokenize(query_text) query_tokens = _tokenize(query_text)
if not query_tokens: if not query_tokens:
...@@ -501,7 +412,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None ...@@ -501,7 +412,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
intro = str(content.get("description", "")).strip() intro = str(content.get("description", "")).strip()
file_id = str(content.get("id", "")).strip() or path.stem file_id = str(content.get("id", "")).strip() or path.stem
slug = str(content.get("slug", "")).strip() or _slugify(title or file_id) slug = str(content.get("slug", "")).strip() or _slugify(title or file_id)
subsections = _normalize_exercise_topics(content.get("exercise_topic", [])) topics = _normalize_exercise_topics(content.get("exercise_topic", []))
topic_refs_raw = content.get("topic_refs", []) topic_refs_raw = content.get("topic_refs", [])
if not title or not file_id or not slug: if not title or not file_id or not slug:
...@@ -520,7 +431,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None ...@@ -520,7 +431,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
"title": title, "title": title,
"intro": intro, "intro": intro,
"slug": slug, "slug": slug,
"subsections": subsections, "topics": topics,
"topic_refs": topic_refs_raw if isinstance(topic_refs_raw, list) else [], "topic_refs": topic_refs_raw if isinstance(topic_refs_raw, list) else [],
"tasks": tasks, "tasks": tasks,
} }
...@@ -629,19 +540,17 @@ def set_selected_task( ...@@ -629,19 +540,17 @@ def set_selected_task(
sheet["task_id"] = str(task_entry.get("id", "")).zfill(2) sheet["task_id"] = str(task_entry.get("id", "")).zfill(2)
refs = _resolve_task_topic_refs(task_file) refs = _resolve_task_topic_refs(task_file)
sheet["task_parent_refs"] = [_parent_ref_to_list(ref) for ref in refs] sheet["task_parent_refs"] = [_parent_ref_to_list(ref) for ref in refs]
sheet.pop("task_subsection_refs", None) sheet.pop("selected_topic_key", None)
sheet.pop("selected_subsection_key", None) sheet.pop("selected_topic_label", None)
sheet.pop("selected_subsection_label", None) sheet.pop("selected_topic_parent_refs", None)
sheet.pop("selected_subsection_parent_refs", None)
sheet.pop("selected_subsection_refs", None)
def set_selected_subsection( def set_selected_topic(
sheet: dict[str, Any], sheet: dict[str, Any],
task_file: dict[str, Any], task_file: dict[str, Any],
subsection_option: dict[str, Any], topic_option: dict[str, Any],
) -> None: ) -> None:
refs_raw = subsection_option.get("refs", []) refs_raw = topic_option.get("refs", [])
refs: list[ParentRef] = [] refs: list[ParentRef] = []
if isinstance(refs_raw, list): if isinstance(refs_raw, list):
for item in refs_raw: for item in refs_raw:
...@@ -651,21 +560,15 @@ def set_selected_subsection( ...@@ -651,21 +560,15 @@ def set_selected_subsection(
if not refs: if not refs:
return return
key = str(subsection_option.get("subsection_key") or subsection_option.get("topic_key") or "").strip() key = str(topic_option.get("topic_key") or "").strip()
sheet["selected_subsection_key"] = key sheet["selected_topic_key"] = key
sheet["selected_subsection_label"] = str(subsection_option.get("label", "")).strip() sheet["selected_topic_label"] = str(topic_option.get("label", "")).strip()
parent_refs = sorted({_normalize_parent_ref(ref) for ref in refs}) parent_refs = sorted({_normalize_parent_ref(ref) for ref in refs})
sheet["selected_subsection_parent_refs"] = [_parent_ref_to_list(ref) for ref in parent_refs] sheet["selected_topic_parent_refs"] = [_parent_ref_to_list(ref) for ref in parent_refs]
sheet["selected_subsection_refs"] = [
[chap, sec, sub]
for chap, sec, sub, subsub in parent_refs
if sub > 0 and subsub <= 0
]
sheet.pop("task_file_id", None) sheet.pop("task_file_id", None)
sheet.pop("task_id", None) sheet.pop("task_id", None)
sheet.pop("task_parent_refs", None) sheet.pop("task_parent_refs", None)
sheet.pop("task_subsection_refs", None)
sheet.pop("task", None) sheet.pop("task", None)
sheet.pop("hints", None) sheet.pop("hints", None)
sheet.pop("solution", None) sheet.pop("solution", None)
...@@ -688,10 +591,10 @@ def select_task_by_ids( ...@@ -688,10 +591,10 @@ def select_task_by_ids(
return True return True
def select_subsection_by_ids( def select_topic_by_ids(
sheet: dict[str, Any], sheet: dict[str, Any],
file_id: str, file_id: str,
subsection_key: str, topic_key: str,
task_files: list[dict[str, Any]] | None = None, task_files: list[dict[str, Any]] | None = None,
) -> bool: ) -> bool:
catalog = task_files if task_files is not None else load_cached_task_files() catalog = task_files if task_files is not None else load_cached_task_files()
...@@ -699,48 +602,38 @@ def select_subsection_by_ids( ...@@ -699,48 +602,38 @@ def select_subsection_by_ids(
if not task_file: if not task_file:
return False return False
options = _resolve_task_subsection_options(task_file) options = _resolve_task_topic_options(task_file)
normalized_key = _normalize_subsection_key(subsection_key) normalized_key = _normalize_topic_key(topic_key)
if not normalized_key: if not normalized_key:
return False return False
for option in options: for option in options:
if str(option.get("subsection_key", "")).strip() == normalized_key: if str(option.get("topic_key", "")).strip() == normalized_key:
set_selected_subsection(sheet, task_file, option) set_selected_topic(sheet, task_file, option)
return True return True
return False return False
def select_subsection_by_key( def select_topic_by_key(
sheet: dict[str, Any], sheet: dict[str, Any],
subsection_key: str, topic_key: str,
subsection_map: dict[str, tuple[int, int, int]] | None = None, topic_map: dict[str, ParentRef] | None = None,
) -> bool: ) -> bool:
mapping: dict[str, ParentRef] mapping = topic_map if topic_map is not None else load_topic_map()
if subsection_map is None: normalized_key = _normalize_topic_key(topic_key)
mapping = load_topic_map()
else:
mapping = {key: (int(ref[0]), int(ref[1]), int(ref[2]), 0) for key, ref in subsection_map.items()}
normalized_key = _normalize_subsection_key(subsection_key)
if not normalized_key: if not normalized_key:
return False return False
ref = mapping.get(normalized_key) ref = mapping.get(normalized_key)
if ref is None: if ref is None:
return False return False
sheet["selected_subsection_key"] = normalized_key sheet["selected_topic_key"] = normalized_key
sheet["selected_subsection_label"] = _format_subsection_label(normalized_key) sheet["selected_topic_label"] = _format_topic_label(normalized_key)
normalized_ref = _normalize_parent_ref(ref) normalized_ref = _normalize_parent_ref(ref)
sheet["selected_subsection_parent_refs"] = [_parent_ref_to_list(normalized_ref)] sheet["selected_topic_parent_refs"] = [_parent_ref_to_list(normalized_ref)]
chap, sec, sub, subsub = normalized_ref
if sub > 0 and subsub <= 0:
sheet["selected_subsection_refs"] = [[chap, sec, sub]]
else:
sheet["selected_subsection_refs"] = []
sheet.pop("task_file_id", None) sheet.pop("task_file_id", None)
sheet.pop("task_id", None) sheet.pop("task_id", None)
sheet.pop("task_parent_refs", None) sheet.pop("task_parent_refs", None)
sheet.pop("task_subsection_refs", None)
sheet.pop("task", None) sheet.pop("task", None)
sheet.pop("hints", None) sheet.pop("hints", None)
sheet.pop("solution", None) sheet.pop("solution", None)
...@@ -753,25 +646,13 @@ def get_selected_task_ids(sheet: dict[str, Any]) -> tuple[str | None, str | None ...@@ -753,25 +646,13 @@ def get_selected_task_ids(sheet: dict[str, Any]) -> tuple[str | None, str | None
return (file_id or None, task_id or None) return (file_id or None, task_id or None)
def get_selected_subsection_ids(sheet: dict[str, Any]) -> tuple[str | None, str | None]: def get_selected_topic_ids(sheet: dict[str, Any]) -> tuple[str | None, str | None]:
subsection_key = str(sheet.get("selected_subsection_key", "")).strip() topic_key = str(sheet.get("selected_topic_key", "")).strip()
return (None, subsection_key or None) return (None, topic_key or None)
def get_selected_subsection_refs(sheet: dict[str, Any]) -> list[tuple[int, int, int]]:
refs: list[tuple[int, int, int]] = []
for chap, sec, sub, subsub in get_selected_subsection_parent_refs(sheet):
if sub <= 0 or subsub > 0:
continue
refs.append((chap, sec, sub))
return sorted(set(refs))
def get_selected_subsection_parent_refs(sheet: dict[str, Any]) -> list[ParentRef]:
refs_raw = sheet.get("selected_subsection_parent_refs")
if not isinstance(refs_raw, list):
refs_raw = sheet.get("selected_subsection_refs", [])
def get_selected_topic_parent_refs(sheet: dict[str, Any]) -> list[ParentRef]:
refs_raw = sheet.get("selected_topic_parent_refs", [])
refs: set[ParentRef] = set() refs: set[ParentRef] = set()
for item in refs_raw: for item in refs_raw:
parsed = _parse_parent_ref(item) parsed = _parse_parent_ref(item)
...@@ -793,15 +674,6 @@ def get_selected_task_parent_refs(sheet: dict[str, Any]) -> list[ParentRef]: ...@@ -793,15 +674,6 @@ def get_selected_task_parent_refs(sheet: dict[str, Any]) -> list[ParentRef]:
return sorted(refs) return sorted(refs)
def get_selected_task_subsection_refs(sheet: dict[str, Any]) -> list[tuple[int, int, int]]:
refs: list[tuple[int, int, int]] = []
for chap, sec, sub, subsub in get_selected_task_parent_refs(sheet):
if sub <= 0 or subsub > 0:
continue
refs.append((chap, sec, sub))
return sorted(refs)
def select_task_for_context( def select_task_for_context(
sheet: dict[str, Any], sheet: dict[str, Any],
query_text: str, query_text: str,
...@@ -880,7 +752,7 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d ...@@ -880,7 +752,7 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
title = str(task_file.get("title", "")).strip() title = str(task_file.get("title", "")).strip()
intro = str(task_file.get("intro", "")).strip() intro = str(task_file.get("intro", "")).strip()
topic_options = _resolve_task_topic_options(task_file) topic_options = _resolve_task_topic_options(task_file)
topics_raw = task_file.get("subsections", []) topics_raw = task_file.get("topics", [])
if isinstance(topics_raw, list) and topics_raw: if isinstance(topics_raw, list) and topics_raw:
topics = [str(item).strip() for item in topics_raw if str(item).strip()] topics = [str(item).strip() for item in topics_raw if str(item).strip()]
else: else:
......
...@@ -38,7 +38,7 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -38,7 +38,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"description": "Pfeildiagramm mit einer fehlenden Abbildung.", "description": "Pfeildiagramm mit einer fehlenden Abbildung.",
} }
], ],
"hints": ["Prüfe jedes Element."], "hints": ["Prüfe jedes Element."],
"solution": "Keine Funktion.", "solution": "Keine Funktion.",
} }
...@@ -47,7 +47,7 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -47,7 +47,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
self.assertIn("Grundlagen von Funktionen", task_text) self.assertIn("Grundlagen von Funktionen", task_text)
self.assertIn("Ordnen Sie zu.", task_text) self.assertIn("Ordnen Sie zu.", task_text)
self.assertIn("Bildbeschreibung: Pfeildiagramm mit einer fehlenden Abbildung.", task_text) self.assertIn("Bildbeschreibung: Pfeildiagramm mit einer fehlenden Abbildung.", task_text)
self.assertEqual(hints, ["Prüfe jedes Element."]) self.assertEqual(hints, ["Prüfe jedes Element."])
self.assertEqual(solution, "Keine Funktion.") self.assertEqual(solution, "Keine Funktion.")
self.assertEqual( self.assertEqual(
images, images,
...@@ -65,7 +65,7 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -65,7 +65,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"_file_id": "analysis_1", "_file_id": "analysis_1",
"title": "Analysis", "title": "Analysis",
"intro": "Intro", "intro": "Intro",
"subsections": ["quadratische_gleichungen"], "topics": ["quadratische_gleichungen"],
"topic_refs": [], "topic_refs": [],
"tasks": [ "tasks": [
{ {
...@@ -105,7 +105,7 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -105,7 +105,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"_file_id": "analysis_1", "_file_id": "analysis_1",
"title": "Analysis", "title": "Analysis",
"intro": "Intro", "intro": "Intro",
"subsections": ["quadratische_gleichungen"], "topics": ["quadratische_gleichungen"],
"topic_refs": [], "topic_refs": [],
"tasks": [ "tasks": [
{ {
...@@ -163,7 +163,7 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -163,7 +163,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
self.assertEqual(normalized["hints"], ["Nutze den Graphen."]) self.assertEqual(normalized["hints"], ["Nutze den Graphen."])
self.assertEqual(normalized["images"], []) self.assertEqual(normalized["images"], [])
def test_socratic_build_subsection_catalog_returns_response_shape(self) -> None: def test_socratic_build_topic_catalog_returns_response_shape(self) -> None:
with patch( with patch(
"app.deterministic_services.socratic_oranisator.load_initial_prompt_items", "app.deterministic_services.socratic_oranisator.load_initial_prompt_items",
return_value={ return_value={
...@@ -185,20 +185,20 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -185,20 +185,20 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"quadratische gleichungen": "Quadratische summary text", "quadratische gleichungen": "Quadratische summary text",
}, },
): ):
catalog = socratic_oranisator.build_subsection_catalog() catalog = socratic_oranisator.build_topic_catalog()
self.assertEqual( self.assertEqual(
catalog, catalog,
[ [
{ {
"subsection_key": "mengen", "topic_key": "mengen",
"label": "Mengen", "label": "Mengen",
"level": "section", "level": "section",
"refs": [[1, 1, 0, 0]], "refs": [[1, 1, 0, 0]],
"summary": "Mengen summary text", "summary": "Mengen summary text",
}, },
{ {
"subsection_key": "quadratische gleichungen", "topic_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen", "label": "Quadratische Gleichungen",
"level": "subsection", "level": "subsection",
"refs": [[1, 3, 3, 0]], "refs": [[1, 3, 3, 0]],
...@@ -207,12 +207,12 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -207,12 +207,12 @@ class TaskCatalogSocraticTest(unittest.TestCase):
], ],
) )
def test_build_subsection_catalog_includes_summary(self) -> None: def test_build_topic_catalog_includes_summary(self) -> None:
with patch( with patch(
"app.deterministic_services.task_catalog.load_topic_map", "app.deterministic_services.task_catalog.load_topic_map",
return_value={ return_value={
"quadratische gleichungen": (1, 3, 3, 0), "quadratische gleichungen": (1, 3, 3, 0),
"mengen": (1, 1, 1, 0), "mengen": (1, 1, 0, 0),
}, },
), patch( ), patch(
"app.deterministic_services.task_catalog.load_topic_summaries", "app.deterministic_services.task_catalog.load_topic_summaries",
...@@ -221,20 +221,20 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -221,20 +221,20 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"quadratische gleichungen": "Quadratische summary text", "quadratische gleichungen": "Quadratische summary text",
}, },
): ):
catalog = task_catalog.build_subsection_catalog() catalog = task_catalog.build_topic_catalog()
self.assertEqual( self.assertEqual(
catalog, catalog,
[ [
{ {
"subsection_key": "mengen", "topic_key": "mengen",
"label": "Mengen", "label": "Mengen",
"level": "section", "level": "section",
"refs": [[1, 1, 0, 0]], "refs": [[1, 1, 0, 0]],
"summary": "Mengen summary text", "summary": "Mengen summary text",
}, },
{ {
"subsection_key": "quadratische gleichungen", "topic_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen", "label": "Quadratische Gleichungen",
"level": "subsection", "level": "subsection",
"refs": [[1, 3, 3, 0]], "refs": [[1, 3, 3, 0]],
...@@ -288,7 +288,7 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -288,7 +288,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
], ],
) )
def test_load_subsection_summaries_extracts_body(self) -> None: def test_load_topic_summaries_extracts_body(self) -> None:
temp_dir = Path(__file__).resolve().parent / "_tmp_subsection_summaries" temp_dir = Path(__file__).resolve().parent / "_tmp_subsection_summaries"
summary_root = temp_dir / "child_lvl" / "subsections" summary_root = temp_dir / "child_lvl" / "subsections"
summary_root.mkdir(parents=True, exist_ok=True) summary_root.mkdir(parents=True, exist_ok=True)
...@@ -315,7 +315,7 @@ Zweite Zeile. ...@@ -315,7 +315,7 @@ Zweite Zeile.
) )
try: try:
summaries = task_catalog.load_subsection_summaries(temp_dir) summaries = task_catalog.load_topic_summaries(temp_dir)
finally: finally:
if md_path.exists(): if md_path.exists():
md_path.unlink() md_path.unlink()
...@@ -331,12 +331,12 @@ Zweite Zeile. ...@@ -331,12 +331,12 @@ Zweite Zeile.
self.assertTrue(summaries["mengen"].startswith("### **Zusammenfassung: Mengen**")) self.assertTrue(summaries["mengen"].startswith("### **Zusammenfassung: Mengen**"))
self.assertNotIn("Child-Chunks", summaries["mengen"]) self.assertNotIn("Child-Chunks", summaries["mengen"])
def test_select_subsection_by_ids_sets_sheet_fields(self) -> None: def test_select_topic_by_ids_sets_sheet_fields(self) -> None:
task_files = [ task_files = [
{ {
"_file_id": "analysis_1", "_file_id": "analysis_1",
"title": "Analysis", "title": "Analysis",
"subsections": ["quadratische_gleichungen"], "topics": ["quadratische_gleichungen"],
"tasks": [], "tasks": [],
} }
] ]
...@@ -346,7 +346,7 @@ Zweite Zeile. ...@@ -346,7 +346,7 @@ Zweite Zeile.
"app.deterministic_services.task_catalog.load_topic_map", "app.deterministic_services.task_catalog.load_topic_map",
return_value={"quadratische gleichungen": (1, 3, 3, 1)}, return_value={"quadratische gleichungen": (1, 3, 3, 1)},
): ):
updated = task_catalog.select_subsection_by_ids( updated = task_catalog.select_topic_by_ids(
sheet, sheet,
"analysis_1", "analysis_1",
"quadratische_gleichungen", "quadratische_gleichungen",
...@@ -354,30 +354,28 @@ Zweite Zeile. ...@@ -354,30 +354,28 @@ Zweite Zeile.
) )
self.assertTrue(updated) self.assertTrue(updated)
self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen") self.assertEqual(sheet["selected_topic_key"], "quadratische gleichungen")
self.assertEqual(sheet["selected_subsection_parent_refs"], [[1, 3, 3, 1]]) self.assertEqual(sheet["selected_topic_parent_refs"], [[1, 3, 3, 1]])
self.assertEqual(sheet["selected_subsection_refs"], [])
def test_select_subsection_by_key_sets_sheet_fields(self) -> None: def test_select_topic_by_key_sets_sheet_fields(self) -> None:
sheet: dict[str, object] = {} sheet: dict[str, object] = {}
with patch( with patch(
"app.deterministic_services.task_catalog.load_topic_map", "app.deterministic_services.task_catalog.load_topic_map",
return_value={"quadratische gleichungen": (1, 3, 3, 1)}, return_value={"quadratische gleichungen": (1, 3, 3, 1)},
): ):
updated = task_catalog.select_subsection_by_key(sheet, "quadratische_gleichungen") updated = task_catalog.select_topic_by_key(sheet, "quadratische_gleichungen")
self.assertTrue(updated) self.assertTrue(updated)
self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen") self.assertEqual(sheet["selected_topic_key"], "quadratische gleichungen")
self.assertEqual(sheet["selected_subsection_parent_refs"], [[1, 3, 3, 1]]) self.assertEqual(sheet["selected_topic_parent_refs"], [[1, 3, 3, 1]])
self.assertEqual(sheet["selected_subsection_refs"], [])
def test_get_selected_subsection_parent_refs_reads_normalized_refs(self) -> None: def test_get_selected_topic_parent_refs_reads_normalized_refs(self) -> None:
sheet: dict[str, object] = { sheet: dict[str, object] = {
"selected_subsection_parent_refs": [[1, 1, 0, 0], [1, 3, 2, 1], [1, 3, 2, 1]], "selected_topic_parent_refs": [[1, 1, 0, 0], [1, 3, 2, 1], [1, 3, 2, 1]],
} }
refs = task_catalog.get_selected_subsection_parent_refs(sheet) refs = task_catalog.get_selected_topic_parent_refs(sheet)
self.assertEqual(refs, [(1, 1, 0, 0), (1, 3, 2, 1)]) self.assertEqual(refs, [(1, 1, 0, 0), (1, 3, 2, 1)])
...@@ -386,7 +384,7 @@ Zweite Zeile. ...@@ -386,7 +384,7 @@ Zweite Zeile.
task_file = { task_file = {
"_file_id": "analysis_1", "_file_id": "analysis_1",
"title": "Analysis", "title": "Analysis",
"subsections": ["mengen"], "topics": ["mengen"],
"topic_refs": [ "topic_refs": [
{ {
"chapter_index": 1, "chapter_index": 1,
...@@ -496,10 +494,10 @@ class TaskApiSocraticTest(unittest.TestCase): ...@@ -496,10 +494,10 @@ class TaskApiSocraticTest(unittest.TestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_list_socratic_subsections_returns_catalog(self) -> None: def test_list_socratic_topics_returns_catalog(self) -> None:
subsections = [ topics = [
{ {
"subsection_key": "quadratische gleichungen", "topic_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen", "label": "Quadratische Gleichungen",
"level": "subsubsection", "level": "subsubsection",
"refs": [[1, 3, 3, 1]], "refs": [[1, 3, 3, 1]],
...@@ -508,18 +506,18 @@ class TaskApiSocraticTest(unittest.TestCase): ...@@ -508,18 +506,18 @@ class TaskApiSocraticTest(unittest.TestCase):
] ]
with patch("app.api.tasks.config.get_orchestrator", return_value="socratic"), patch( with patch("app.api.tasks.config.get_orchestrator", return_value="socratic"), patch(
"app.api.tasks.socratic_oranisator.build_subsection_catalog", "app.api.tasks.socratic_oranisator.build_topic_catalog",
return_value=subsections, return_value=topics,
): ):
response = self.client.get("/api/tasks/socratic-subsections") response = self.client.get("/api/tasks/socratic-topics")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
body = response.json() body = response.json()
self.assertEqual(body["orchestrator"], "socratic") self.assertEqual(body["orchestrator"], "socratic")
self.assertTrue(body["enabled"]) self.assertTrue(body["enabled"])
self.assertEqual(body["subsections"], subsections) self.assertEqual(body["topics"], topics)
def test_select_subsection_endpoint_returns_selected_key(self) -> None: def test_select_topic_endpoint_returns_selected_key(self) -> None:
sheet: dict[str, object] = {} sheet: dict[str, object] = {}
with patch("app.api.tasks.context_store.get_chat_id", return_value="chat-1"), patch( with patch("app.api.tasks.context_store.get_chat_id", return_value="chat-1"), patch(
...@@ -529,25 +527,25 @@ class TaskApiSocraticTest(unittest.TestCase): ...@@ -529,25 +527,25 @@ class TaskApiSocraticTest(unittest.TestCase):
"app.api.tasks.context_store.context_store_new.init_sheet", "app.api.tasks.context_store.context_store_new.init_sheet",
return_value=sheet, return_value=sheet,
), patch( ), patch(
"app.api.tasks.task_catalog.select_subsection_by_key", "app.api.tasks.task_catalog.select_topic_by_key",
return_value=True, return_value=True,
) as select_mock, patch( ) as select_mock, patch(
"app.api.tasks.context_store.save_sheet" "app.api.tasks.context_store.save_sheet"
), patch( ), patch(
"app.api.tasks.task_catalog.get_selected_subsection_ids", "app.api.tasks.task_catalog.get_selected_topic_ids",
return_value=(None, "quadratische gleichungen"), return_value=(None, "quadratische gleichungen"),
): ):
response = self.client.post( response = self.client.post(
"/api/tasks/select-subsection", "/api/tasks/select-topic",
json={ json={
"draft": "chat-1", "draft": "chat-1",
"subsection_key": "quadratische_gleichungen", "topic_key": "quadratische_gleichungen",
}, },
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["subsection_key"], "quadratische gleichungen") self.assertEqual(response.json()["topic_key"], "quadratische gleichungen")
select_mock.assert_called_once_with(sheet, subsection_key="quadratische_gleichungen") select_mock.assert_called_once_with(sheet, topic_key="quadratische_gleichungen")
class ChatBootstrapSocraticTest(unittest.TestCase): class ChatBootstrapSocraticTest(unittest.TestCase):
...@@ -585,28 +583,28 @@ class ChatBootstrapSocraticTest(unittest.TestCase): ...@@ -585,28 +583,28 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
"/api/chat/bootstrap-socratic", "/api/chat/bootstrap-socratic",
json={ json={
"draft": "chat-1", "draft": "chat-1",
"subsection_key": "quadratische_gleichungen", "topic_key": "quadratische_gleichungen",
}, },
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["reply"], "Was sind die Themen dieses Abschnitts?") self.assertEqual(response.json()["reply"], "Was sind die Themen dieses Abschnitts?")
retrieve_mock.assert_called_once_with(pg_url="postgresql://localhost/test", parent_refs=[(1, 3, 3, 1)]) retrieve_mock.assert_called_once_with(pg_url="postgresql://localhost/test", parent_refs=[(1, 3, 3, 1)])
self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen") self.assertEqual(sheet["selected_topic_key"], "quadratische gleichungen")
self.assertEqual(sheet["selected_subsection_parent_refs"], [[1, 3, 3, 1]]) self.assertEqual(sheet["selected_topic_parent_refs"], [[1, 3, 3, 1]])
self.assertEqual(sheet["history"], [{"role": "assistant", "content": "Was sind die Themen dieses Abschnitts?"}]) self.assertEqual(sheet["history"], [{"role": "assistant", "content": "Was sind die Themen dieses Abschnitts?"}])
class SessionStoreSocraticTest(unittest.TestCase): class SessionStoreSocraticTest(unittest.TestCase):
def test_load_archive_restores_selected_subsection(self) -> None: def test_load_archive_restores_selected_topic(self) -> None:
record = { record = {
"chat_id": "chat-1", "chat_id": "chat-1",
"saved_at": "2026-03-31T10:00:00Z", "saved_at": "2026-03-31T10:00:00Z",
"orchestrator": "socratic", "orchestrator": "socratic",
"history": [], "history": [],
"selected_task": None, "selected_task": None,
"selected_subsection": { "selected_topic": {
"subsection_key": "quadratische gleichungen", "topic_key": "quadratische gleichungen",
}, },
} }
...@@ -624,7 +622,7 @@ class SessionStoreSocraticTest(unittest.TestCase): ...@@ -624,7 +622,7 @@ class SessionStoreSocraticTest(unittest.TestCase):
temp_dir.rmdir() temp_dir.rmdir()
self.assertIsNotNone(archive) self.assertIsNotNone(archive)
self.assertEqual(archive["selected_subsection"], record["selected_subsection"]) self.assertEqual(archive["selected_topic"], record["selected_topic"])
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -59,7 +59,7 @@ class TaskYamlMigrationTest(unittest.TestCase): ...@@ -59,7 +59,7 @@ class TaskYamlMigrationTest(unittest.TestCase):
self.assertEqual(loaded[0]["_file_id"], "analysis_1") self.assertEqual(loaded[0]["_file_id"], "analysis_1")
self.assertEqual(loaded[0]["title"], "Analysis") self.assertEqual(loaded[0]["title"], "Analysis")
self.assertEqual(loaded[0]["intro"], "Intro") self.assertEqual(loaded[0]["intro"], "Intro")
self.assertEqual(loaded[0]["subsections"], ["quadratische-gleichungen"]) self.assertEqual(loaded[0]["topics"], ["quadratische-gleichungen"])
self.assertEqual(loaded[0]["tasks"][0]["id"], "01") self.assertEqual(loaded[0]["tasks"][0]["id"], "01")
self.assertEqual(loaded[0]["tasks"][0]["statement"], "Bestimme f(x).") self.assertEqual(loaded[0]["tasks"][0]["statement"], "Bestimme f(x).")
self.assertEqual(loaded[0]["tasks"][0]["hints"], ["Nutze die Ableitung."]) self.assertEqual(loaded[0]["tasks"][0]["hints"], ["Nutze die Ableitung."])
...@@ -124,7 +124,7 @@ class TaskYamlMigrationTest(unittest.TestCase): ...@@ -124,7 +124,7 @@ class TaskYamlMigrationTest(unittest.TestCase):
"_file_id": "analysis_1", "_file_id": "analysis_1",
"title": "Analysis", "title": "Analysis",
"intro": "Intro", "intro": "Intro",
"subsections": ["quadratische-gleichungen"], "topics": ["quadratische-gleichungen"],
"tasks": [ "tasks": [
{ {
"id": "01", "id": "01",
......
...@@ -13,10 +13,10 @@ export type TaskFile = { ...@@ -13,10 +13,10 @@ export type TaskFile = {
tasks: TaskListItem[]; tasks: TaskListItem[];
}; };
export type SubsectionOption = { export type TopicOption = {
subsection_key: string; topic_key: string;
label: string; label: string;
refs: [number, number, number][]; refs: [number, number, number, number][];
summary: string; summary: string;
}; };
...@@ -37,7 +37,7 @@ export type TaskDetailsResponse = { ...@@ -37,7 +37,7 @@ export type TaskDetailsResponse = {
export type SocraticResponse = { export type SocraticResponse = {
orchestrator: string; orchestrator: string;
enabled: boolean; enabled: boolean;
subsections: SubsectionOption[]; topics: TopicOption[];
}; };
export type SelectedTaskRef = { export type SelectedTaskRef = {
...@@ -45,8 +45,8 @@ export type SelectedTaskRef = { ...@@ -45,8 +45,8 @@ export type SelectedTaskRef = {
taskId: string; taskId: string;
}; };
export type SelectedSubsectionRef = { export type SelectedTopicRef = {
subsectionKey: string; topicKey: string;
}; };
export type SelectTaskResponse = { export type SelectTaskResponse = {
...@@ -55,9 +55,9 @@ export type SelectTaskResponse = { ...@@ -55,9 +55,9 @@ export type SelectTaskResponse = {
task_id: string; task_id: string;
}; };
export type SelectSubsectionResponse = { export type SelectTopicResponse = {
status: string; status: string;
subsection_key: string; topic_key: string;
}; };
export async function fetchTasks(): Promise<TasksResponse> { export async function fetchTasks(): Promise<TasksResponse> {
...@@ -81,10 +81,10 @@ export async function fetchTaskDetails(input: { ...@@ -81,10 +81,10 @@ export async function fetchTaskDetails(input: {
return response.json(); return response.json();
} }
export async function fetchSocraticSubsections(): Promise<SocraticResponse> { export async function fetchSocraticTopics(): Promise<SocraticResponse> {
const response = await fetch("/api/tasks/socratic-subsections"); const response = await fetch("/api/tasks/socratic-topics");
if (!response.ok) { if (!response.ok) {
throw new Error(`Socratic subsections failed: ${response.status}`); throw new Error(`Socratic topics failed: ${response.status}`);
} }
return response.json(); return response.json();
} }
...@@ -110,21 +110,21 @@ export async function selectTask(input: { ...@@ -110,21 +110,21 @@ export async function selectTask(input: {
return response.json(); return response.json();
} }
export async function selectSubsection(input: { export async function selectTopic(input: {
draft: string; draft: string;
subsectionKey: string; topicKey: string;
}): Promise<SelectSubsectionResponse> { }): Promise<SelectTopicResponse> {
const response = await fetch("/api/tasks/select-subsection", { const response = await fetch("/api/tasks/select-topic", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
draft: input.draft, draft: input.draft,
subsection_key: input.subsectionKey, topic_key: input.topicKey,
}), }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Subsection selection failed: ${response.status}`); throw new Error(`Topic selection failed: ${response.status}`);
} }
return response.json(); return response.json();
} }
...@@ -10,11 +10,11 @@ import TaskPanel from "../components/Task/TaskPanel"; ...@@ -10,11 +10,11 @@ import TaskPanel from "../components/Task/TaskPanel";
import type { ChatMessage } from "../components/Chat/MessageList"; import type { ChatMessage } from "../components/Chat/MessageList";
import type { RetrievedDoc } from "../components/Retrieval/DocPanel"; import type { RetrievedDoc } from "../components/Retrieval/DocPanel";
import type { OrchestratorName } from "../api/orchestratorApi"; import type { OrchestratorName } from "../api/orchestratorApi";
import { selectSubsection, selectTask } from "../api/taskApi"; import { selectTask, selectTopic } from "../api/taskApi";
import { t } from "../i18n"; import { t } from "../i18n";
import { createSessionId, useTutorSession } from "../state/tutorSession"; import { createSessionId, useTutorSession } from "../state/tutorSession";
import { getSelectionRouteForOrchestrator, isSocraticOrchestrator } from "../utils/orchestratorRoutes"; import { getSelectionRouteForOrchestrator, isSocraticOrchestrator } from "../utils/orchestratorRoutes";
import { normalizeSubsectionKey } from "../utils/subsectionKey"; import { normalizeSubsectionKey as normalizeTopicKey } from "../utils/subsectionKey";
import sumintLogo from "../../SuMINT-Logo.png"; import sumintLogo from "../../SuMINT-Logo.png";
const initialMessages: ChatMessage[] = []; const initialMessages: ChatMessage[] = [];
...@@ -51,8 +51,8 @@ type ArchivedChatDetail = { ...@@ -51,8 +51,8 @@ type ArchivedChatDetail = {
file_id: string; file_id: string;
task_id: string; task_id: string;
} | null; } | null;
selected_subsection?: { selected_topic?: {
subsection_key: string; topic_key: string;
} | null; } | null;
}; };
...@@ -191,8 +191,8 @@ export default function ChatPage() { ...@@ -191,8 +191,8 @@ export default function ChatPage() {
selectedTaskRef, selectedTaskRef,
selectedTask, selectedTask,
selectedTaskFile, selectedTaskFile,
selectedSubsectionRef, selectedTopicRef,
selectedSubsection, selectedTopic,
selectedOrchestrator, selectedOrchestrator,
availableOrchestrators, availableOrchestrators,
setSelectedOrchestrator, setSelectedOrchestrator,
...@@ -202,7 +202,7 @@ export default function ChatPage() { ...@@ -202,7 +202,7 @@ export default function ChatPage() {
lockTask, lockTask,
unlockTask, unlockTask,
setTaskRef, setTaskRef,
setSubsectionRef, setTopicRef,
resetForNewChat, resetForNewChat,
} = useTutorSession(); } = useTutorSession();
...@@ -239,31 +239,33 @@ export default function ChatPage() { ...@@ -239,31 +239,33 @@ export default function ChatPage() {
.toLowerCase(); .toLowerCase();
const fileId = String(searchParams.get("file_id") || "").trim(); const fileId = String(searchParams.get("file_id") || "").trim();
const rawTaskId = String(searchParams.get("task_id") || "").trim(); const rawTaskId = String(searchParams.get("task_id") || "").trim();
const rawSubsectionKey = String(searchParams.get("subsection_key") || "").trim(); const rawTopicKey = String(
searchParams.get("topic_key") || searchParams.get("subsection_key") || ""
).trim();
const taskId = const taskId =
/^\d{1,2}$/.test(rawTaskId) && rawTaskId.length < 2 /^\d{1,2}$/.test(rawTaskId) && rawTaskId.length < 2
? rawTaskId.padStart(2, "0") ? rawTaskId.padStart(2, "0")
: rawTaskId; : rawTaskId;
const subsectionKey = rawSubsectionKey ? normalizeSubsectionKey(rawSubsectionKey) : ""; const topicKey = rawTopicKey ? normalizeTopicKey(rawTopicKey) : "";
const hasFileId = Boolean(fileId); const hasFileId = Boolean(fileId);
const hasTaskId = Boolean(taskId); const hasTaskId = Boolean(taskId);
const hasSubsectionKey = Boolean(subsectionKey); const hasTopicKey = Boolean(topicKey);
const isTaskOrchestrator = isTaskCoupledOrchestrator(orchestrator); const isTaskOrchestrator = isTaskCoupledOrchestrator(orchestrator);
const isSocratic = isSocraticOrchestrator(orchestrator as OrchestratorName); const isSocratic = isSocraticOrchestrator(orchestrator as OrchestratorName);
const hasAnyTaskParam = hasFileId || hasTaskId || hasSubsectionKey; const hasAnyTaskParam = hasFileId || hasTaskId || hasTopicKey;
const hasRequiredTaskParams = hasFileId && hasTaskId; const hasRequiredTaskParams = hasFileId && hasTaskId;
const hasRequiredSubsectionParams = hasSubsectionKey; const hasRequiredTopicParams = hasTopicKey;
const key = `${orchestrator}|${fileId}|${taskId}|${subsectionKey}`; const key = `${orchestrator}|${fileId}|${taskId}|${topicKey}`;
return { return {
fileId, fileId,
taskId, taskId,
subsectionKey, topicKey,
isTaskOrchestrator, isTaskOrchestrator,
isSocratic, isSocratic,
hasAnyTaskParam, hasAnyTaskParam,
hasRequiredTaskParams, hasRequiredTaskParams,
hasRequiredSubsectionParams, hasRequiredTopicParams,
key, key,
}; };
}, [searchParams]); }, [searchParams]);
...@@ -286,13 +288,13 @@ export default function ChatPage() { ...@@ -286,13 +288,13 @@ export default function ChatPage() {
if ( if (
(deepLinkTarget.isTaskOrchestrator && !deepLinkTarget.hasRequiredTaskParams) || (deepLinkTarget.isTaskOrchestrator && !deepLinkTarget.hasRequiredTaskParams) ||
(deepLinkTarget.isSocratic && !deepLinkTarget.hasRequiredSubsectionParams) (deepLinkTarget.isSocratic && !deepLinkTarget.hasRequiredTopicParams)
) { ) {
setDeepLinkError( setDeepLinkError(
deepLinkTarget.isSocratic ? t("deepLinkInvalidSubsection") : t("deepLinkInvalidTask") deepLinkTarget.isSocratic ? t("deepLinkInvalidSubsection") : t("deepLinkInvalidTask")
); );
setTaskRef(null); setTaskRef(null);
setSubsectionRef(null); setTopicRef(null);
unlockTask(); unlockTask();
navigate(targetRoute, { replace: true }); navigate(targetRoute, { replace: true });
return; return;
...@@ -301,13 +303,13 @@ export default function ChatPage() { ...@@ -301,13 +303,13 @@ export default function ChatPage() {
if (deepLinkTarget.isSocratic) { if (deepLinkTarget.isSocratic) {
setSelectedOrchestrator("socratic"); setSelectedOrchestrator("socratic");
setTaskRef(null); setTaskRef(null);
setSubsectionRef({ subsectionKey: deepLinkTarget.subsectionKey }); setTopicRef({ topicKey: deepLinkTarget.topicKey });
} else { } else {
const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId); const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId);
if (!selectedFile) { if (!selectedFile) {
setDeepLinkError(t("deepLinkInvalidTask")); setDeepLinkError(t("deepLinkInvalidTask"));
setTaskRef(null); setTaskRef(null);
setSubsectionRef(null); setTopicRef(null);
unlockTask(); unlockTask();
navigate(targetRoute, { replace: true }); navigate(targetRoute, { replace: true });
return; return;
...@@ -316,13 +318,13 @@ export default function ChatPage() { ...@@ -316,13 +318,13 @@ export default function ChatPage() {
if (!selectedTask) { if (!selectedTask) {
setDeepLinkError(t("deepLinkInvalidTask")); setDeepLinkError(t("deepLinkInvalidTask"));
setTaskRef(null); setTaskRef(null);
setSubsectionRef(null); setTopicRef(null);
unlockTask(); unlockTask();
navigate(targetRoute, { replace: true }); navigate(targetRoute, { replace: true });
return; return;
} }
setSelectedOrchestrator("task"); setSelectedOrchestrator("task");
setSubsectionRef(null); setTopicRef(null);
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id }); setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
} }
unlockTask(); unlockTask();
...@@ -331,9 +333,9 @@ export default function ChatPage() { ...@@ -331,9 +333,9 @@ export default function ChatPage() {
void (async () => { void (async () => {
try { try {
if (deepLinkTarget.isSocratic) { if (deepLinkTarget.isSocratic) {
await selectSubsection({ await selectTopic({
draft: chatSessionId, draft: chatSessionId,
subsectionKey: deepLinkTarget.subsectionKey, topicKey: deepLinkTarget.topicKey,
}); });
} else { } else {
const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId); const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId);
...@@ -356,7 +358,7 @@ export default function ChatPage() { ...@@ -356,7 +358,7 @@ export default function ChatPage() {
deepLinkTarget.isSocratic ? t("deepLinkInitFailedSubsection") : t("deepLinkInitFailed") deepLinkTarget.isSocratic ? t("deepLinkInitFailedSubsection") : t("deepLinkInitFailed")
); );
setTaskRef(null); setTaskRef(null);
setSubsectionRef(null); setTopicRef(null);
unlockTask(); unlockTask();
navigate(targetRoute, { replace: true }); navigate(targetRoute, { replace: true });
} }
...@@ -375,7 +377,7 @@ export default function ChatPage() { ...@@ -375,7 +377,7 @@ export default function ChatPage() {
searchParams, searchParams,
setSelectedOrchestrator, setSelectedOrchestrator,
setTaskRef, setTaskRef,
setSubsectionRef, setTopicRef,
taskFiles, taskFiles,
unlockTask, unlockTask,
]); ]);
...@@ -384,13 +386,13 @@ export default function ChatPage() { ...@@ -384,13 +386,13 @@ export default function ChatPage() {
if ( if (
!isTasksInitialized || !isTasksInitialized ||
selectedOrchestrator !== "socratic" || selectedOrchestrator !== "socratic" ||
!selectedSubsectionRef || !selectedTopicRef ||
messages.length > 0 messages.length > 0
) { ) {
return; return;
} }
const bootstrapKey = `${chatSessionId}|${selectedSubsectionRef.subsectionKey}`; const bootstrapKey = `${chatSessionId}|${selectedTopicRef.topicKey}`;
if (socraticBootstrapRef.current === bootstrapKey) { if (socraticBootstrapRef.current === bootstrapKey) {
return; return;
} }
...@@ -402,11 +404,11 @@ export default function ChatPage() { ...@@ -402,11 +404,11 @@ export default function ChatPage() {
const response = await fetch(`/api/chat/bootstrap-socratic`, { const response = await fetch(`/api/chat/bootstrap-socratic`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
draft: chatSessionId, draft: chatSessionId,
subsection_key: selectedSubsectionRef.subsectionKey, topic_key: selectedTopicRef.topicKey,
}), }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Bootstrap failed: ${response.status}`); throw new Error(`Bootstrap failed: ${response.status}`);
...@@ -464,7 +466,7 @@ export default function ChatPage() { ...@@ -464,7 +466,7 @@ export default function ChatPage() {
isTasksInitialized, isTasksInitialized,
messages.length, messages.length,
selectedOrchestrator, selectedOrchestrator,
selectedSubsectionRef, selectedTopicRef,
]); ]);
useEffect(() => { useEffect(() => {
...@@ -479,7 +481,7 @@ export default function ChatPage() { ...@@ -479,7 +481,7 @@ export default function ChatPage() {
return; return;
} }
if (selectedOrchestrator === "socratic") { if (selectedOrchestrator === "socratic") {
if (!selectedSubsectionRef) { if (!selectedTopicRef) {
navigate("/select-socratic", { replace: true }); navigate("/select-socratic", { replace: true });
} }
return; return;
...@@ -493,7 +495,7 @@ export default function ChatPage() { ...@@ -493,7 +495,7 @@ export default function ChatPage() {
isTasksInitialized, isTasksInitialized,
navigate, navigate,
selectedOrchestrator, selectedOrchestrator,
selectedSubsectionRef, selectedTopicRef,
selectedTaskRef, selectedTaskRef,
]); ]);
...@@ -674,7 +676,7 @@ export default function ChatPage() { ...@@ -674,7 +676,7 @@ export default function ChatPage() {
draft: string; draft: string;
orchestrator: OrchestratorName; orchestrator: OrchestratorName;
selected_task?: { file_id: string; task_id: string }; selected_task?: { file_id: string; task_id: string };
selected_subsection?: { subsection_key: string }; selected_topic?: { topic_key: string };
} = { } = {
messages: [...messages, userMessage].map((message) => ({ messages: [...messages, userMessage].map((message) => ({
role: message.role, role: message.role,
...@@ -684,9 +686,9 @@ export default function ChatPage() { ...@@ -684,9 +686,9 @@ export default function ChatPage() {
orchestrator: selectedOrchestrator, orchestrator: selectedOrchestrator,
}; };
if (isTaskModeEnabled) { if (isTaskModeEnabled) {
if (selectedOrchestrator === "socratic" && selectedSubsectionRef) { if (selectedOrchestrator === "socratic" && selectedTopicRef) {
chatPayload.selected_subsection = { chatPayload.selected_topic = {
subsection_key: selectedSubsectionRef.subsectionKey, topic_key: selectedTopicRef.topicKey,
}; };
} else if (selectedTaskRef) { } else if (selectedTaskRef) {
chatPayload.selected_task = { chatPayload.selected_task = {
...@@ -872,27 +874,27 @@ export default function ChatPage() { ...@@ -872,27 +874,27 @@ export default function ChatPage() {
if (nextOrchestrator === "socratic" || isTaskCoupledOrchestrator(nextOrchestrator)) { if (nextOrchestrator === "socratic" || isTaskCoupledOrchestrator(nextOrchestrator)) {
if (nextOrchestrator === "socratic") { if (nextOrchestrator === "socratic") {
const restoredSubsection = payload.selected_subsection; const restoredSubsection = payload.selected_topic;
if (restoredSubsection?.subsection_key) { if (restoredSubsection?.topic_key) {
setTaskRef(null); setTaskRef(null);
setSubsectionRef({ subsectionKey: restoredSubsection.subsection_key }); setTopicRef({ topicKey: restoredSubsection.topic_key });
lockTask(); lockTask();
try { try {
await selectSubsection({ await selectTopic({
draft: payload.chat_id, draft: payload.chat_id,
subsectionKey: restoredSubsection.subsection_key, topicKey: restoredSubsection.topic_key,
}); });
} catch (error) { } catch (error) {
void error; void error;
} }
} else { } else {
setTaskRef(null); setTaskRef(null);
setSubsectionRef(null); setTopicRef(null);
unlockTask(); unlockTask();
navigate("/select-socratic", { replace: true }); navigate("/select-socratic", { replace: true });
} }
} else { } else {
setSubsectionRef(null); setTopicRef(null);
const restoredTask = payload.selected_task; const restoredTask = payload.selected_task;
if (restoredTask?.file_id && restoredTask?.task_id) { if (restoredTask?.file_id && restoredTask?.task_id) {
setTaskRef({ setTaskRef({
...@@ -917,7 +919,7 @@ export default function ChatPage() { ...@@ -917,7 +919,7 @@ export default function ChatPage() {
} }
} else { } else {
setTaskRef(null); setTaskRef(null);
setSubsectionRef(null); setTopicRef(null);
unlockTask(); unlockTask();
navigate("/chat", { replace: true }); navigate("/chat", { replace: true });
} }
...@@ -1268,11 +1270,11 @@ export default function ChatPage() { ...@@ -1268,11 +1270,11 @@ export default function ChatPage() {
</section> </section>
<aside className={`retrieval-column ${isTaskModeEnabled ? "retrieval-column-task-mode" : ""}`}> <aside className={`retrieval-column ${isTaskModeEnabled ? "retrieval-column-task-mode" : ""}`}>
{selectedOrchestrator === "socratic" && selectedSubsection ? ( {selectedOrchestrator === "socratic" && selectedTopic ? (
<SocraticPanel <SocraticPanel
selectedSubsectionLabel={selectedSubsection.label} selectedSubsectionLabel={selectedTopic.label}
selectedSubsectionKey={selectedSubsection.subsectionKey} selectedSubsectionKey={selectedTopic.topicKey}
selectedSubsectionSummary={selectedSubsection.summary} selectedSubsectionSummary={selectedTopic.summary}
onChangeSelection={handleChangeTaskArea} onChangeSelection={handleChangeTaskArea}
/> />
) : null} ) : null}
......
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import type { OrchestratorName } from "../api/orchestratorApi"; import type { OrchestratorName } from "../api/orchestratorApi";
import { selectSubsection } from "../api/taskApi"; import { selectTopic } from "../api/taskApi";
import OrchestratorSelect from "../components/Orchestrator/OrchestratorSelect"; import OrchestratorSelect from "../components/Orchestrator/OrchestratorSelect";
import { t } from "../i18n"; import { t } from "../i18n";
import { useTutorSession } from "../state/tutorSession"; import { useTutorSession } from "../state/tutorSession";
...@@ -18,11 +18,11 @@ export default function SocraticSelectionPage() { ...@@ -18,11 +18,11 @@ export default function SocraticSelectionPage() {
switchOrchestrator, switchOrchestrator,
isOrchestratorSelectable, isOrchestratorSelectable,
orchestratorError, orchestratorError,
subsections, topics,
selectedSubsectionRef, selectedTopicRef,
selectedSubsection, selectedTopic,
tasksError, tasksError,
setSubsectionKey, setTopicKey,
lockTask, lockTask,
unlockTask, unlockTask,
isTasksInitialized, isTasksInitialized,
...@@ -31,11 +31,11 @@ export default function SocraticSelectionPage() { ...@@ -31,11 +31,11 @@ export default function SocraticSelectionPage() {
const subsectionMenuOptions = useMemo( const subsectionMenuOptions = useMemo(
() => () =>
subsections.map((option) => ({ topics.map((option) => ({
value: option.subsection_key, value: option.topic_key,
label: option.label || option.subsection_key, label: option.label || option.topic_key,
})), })),
[subsections] [topics]
); );
useEffect(() => { useEffect(() => {
...@@ -50,7 +50,7 @@ export default function SocraticSelectionPage() { ...@@ -50,7 +50,7 @@ export default function SocraticSelectionPage() {
}, [isTasksInitialized, navigate, selectedOrchestrator, unlockTask]); }, [isTasksInitialized, navigate, selectedOrchestrator, unlockTask]);
useEffect(() => { useEffect(() => {
if (!selectedSubsection?.label || !subsectionDisplayRef.current) { if (!selectedTopic?.label || !subsectionDisplayRef.current) {
return; return;
} }
const mathjax = window.MathJax; const mathjax = window.MathJax;
...@@ -58,30 +58,30 @@ export default function SocraticSelectionPage() { ...@@ -58,30 +58,30 @@ export default function SocraticSelectionPage() {
return; return;
} }
mathjax.typesetPromise([subsectionDisplayRef.current]).catch(() => undefined); mathjax.typesetPromise([subsectionDisplayRef.current]).catch(() => undefined);
}, [selectedSubsection?.label]); }, [selectedTopic?.label]);
useEffect(() => { useEffect(() => {
if (!subsectionMenuOptions.length) { if (!subsectionMenuOptions.length) {
return; return;
} }
if ( if (
selectedSubsectionRef && selectedTopicRef &&
subsectionMenuOptions.some((option) => option.value === selectedSubsectionRef.subsectionKey) subsectionMenuOptions.some((option) => option.value === selectedTopicRef.topicKey)
) { ) {
return; return;
} }
setSubsectionKey(subsectionMenuOptions[0].value); setTopicKey(subsectionMenuOptions[0].value);
}, [selectedSubsectionRef, setSubsectionKey, subsectionMenuOptions]); }, [selectedTopicRef, setTopicKey, subsectionMenuOptions]);
const handleStartSocratic = async () => { const handleStartSocratic = async () => {
if (!selectedSubsectionRef) { if (!selectedTopicRef) {
return; return;
} }
try { try {
await selectSubsection({ await selectTopic({
draft: chatSessionId, draft: chatSessionId,
subsectionKey: selectedSubsectionRef.subsectionKey, topicKey: selectedTopicRef.topicKey,
}); });
lockTask(); lockTask();
navigate("/chat"); navigate("/chat");
...@@ -137,8 +137,8 @@ export default function SocraticSelectionPage() { ...@@ -137,8 +137,8 @@ export default function SocraticSelectionPage() {
<select <select
id="socratic-subsection-select" id="socratic-subsection-select"
className="task-select" className="task-select"
value={selectedSubsectionRef?.subsectionKey || ""} value={selectedTopicRef?.topicKey || ""}
onChange={(event) => setSubsectionKey(event.target.value)} onChange={(event) => setTopicKey(event.target.value)}
disabled={!subsectionMenuOptions.length} disabled={!subsectionMenuOptions.length}
> >
{subsectionMenuOptions.length ? ( {subsectionMenuOptions.length ? (
...@@ -159,7 +159,7 @@ export default function SocraticSelectionPage() { ...@@ -159,7 +159,7 @@ export default function SocraticSelectionPage() {
type="button" type="button"
className="btn primary task-solve-btn" className="btn primary task-solve-btn"
onClick={handleStartSocratic} onClick={handleStartSocratic}
disabled={!selectedSubsectionRef} disabled={!selectedTopicRef}
> >
{t("startSocratic")} {t("startSocratic")}
</button> </button>
......
...@@ -17,11 +17,11 @@ import { ...@@ -17,11 +17,11 @@ import {
} from "../api/orchestratorApi"; } from "../api/orchestratorApi";
import { import {
fetchTaskDetails, fetchTaskDetails,
fetchSocraticSubsections, fetchSocraticTopics,
fetchTasks, fetchTasks,
type SelectedSubsectionRef, type SelectedTopicRef,
type SelectedTaskRef, type SelectedTaskRef,
type SubsectionOption, type TopicOption,
type TaskImage, type TaskImage,
type TaskDetailsResponse, type TaskDetailsResponse,
type TaskFile, type TaskFile,
...@@ -38,9 +38,9 @@ export type SelectedTask = SelectedTaskRef & { ...@@ -38,9 +38,9 @@ export type SelectedTask = SelectedTaskRef & {
images: TaskImage[]; images: TaskImage[];
}; };
export type SelectedSubsection = SelectedSubsectionRef & { export type SelectedTopic = SelectedTopicRef & {
label: string; label: string;
refs: [number, number, number][]; refs: [number, number, number, number][];
refsText: string; refsText: string;
summary: string; summary: string;
}; };
...@@ -52,10 +52,10 @@ export type TaskSelectionState = { ...@@ -52,10 +52,10 @@ export type TaskSelectionState = {
selectedTaskFile: TaskFile | null; selectedTaskFile: TaskFile | null;
taskFileOptions: SelectOption[]; taskFileOptions: SelectOption[];
taskOptions: SelectOption[]; taskOptions: SelectOption[];
selectedSubsectionRef: SelectedSubsectionRef | null; selectedTopicRef: SelectedTopicRef | null;
selectedSubsection: SelectedSubsection | null; selectedTopic: SelectedTopic | null;
subsections: SubsectionOption[]; topics: TopicOption[];
subsectionOptions: SelectOption[]; topicOptions: SelectOption[];
tasksError: string | null; tasksError: string | null;
isTaskModeEnabled: boolean; isTaskModeEnabled: boolean;
isTasksInitialized: boolean; isTasksInitialized: boolean;
...@@ -73,10 +73,10 @@ export type TutorSessionState = TaskSelectionState & { ...@@ -73,10 +73,10 @@ export type TutorSessionState = TaskSelectionState & {
isOrchestratorSelectable: boolean; isOrchestratorSelectable: boolean;
orchestratorError: string | null; orchestratorError: string | null;
setTaskRef: (value: SelectedTaskRef | null) => void; setTaskRef: (value: SelectedTaskRef | null) => void;
setSubsectionRef: (value: SelectedSubsectionRef | null) => void; setTopicRef: (value: SelectedTopicRef | null) => void;
setTaskFile: (fileId: string) => void; setTaskFile: (fileId: string) => void;
setTaskId: (taskId: string) => void; setTaskId: (taskId: string) => void;
setSubsectionKey: (subsectionKey: string) => void; setTopicKey: (topicKey: string) => void;
lockTask: () => void; lockTask: () => void;
unlockTask: () => void; unlockTask: () => void;
resetForNewChat: () => void; resetForNewChat: () => void;
...@@ -96,7 +96,7 @@ const isSelectableTaskFile = (file: TaskFile): boolean => ...@@ -96,7 +96,7 @@ const isSelectableTaskFile = (file: TaskFile): boolean =>
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean => const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "socratic"; value === "task" || value === "socratic";
const formatSubsectionRefs = (refs: [number, number, number][]): string => const formatTopicRefs = (refs: [number, number, number, number][]): string =>
refs.map((ref) => ref.join(":")).join(", "); refs.map((ref) => ref.join(":")).join(", ");
export function TutorSessionProvider({ children }: PropsWithChildren) { export function TutorSessionProvider({ children }: PropsWithChildren) {
...@@ -110,10 +110,10 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -110,10 +110,10 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const [orchestratorError, setOrchestratorError] = useState<string | null>(null); const [orchestratorError, setOrchestratorError] = useState<string | null>(null);
const [isTasksInitialized, setIsTasksInitialized] = useState(false); const [isTasksInitialized, setIsTasksInitialized] = useState(false);
const [taskFiles, setTaskFiles] = useState<TaskFile[]>([]); const [taskFiles, setTaskFiles] = useState<TaskFile[]>([]);
const [subsections, setSubsections] = useState<SubsectionOption[]>([]); const [topics, setTopics] = useState<TopicOption[]>([]);
const [selectedTaskRef, setSelectedTaskRef] = useState<SelectedTaskRef | null>(null); const [selectedTaskRef, setSelectedTaskRef] = useState<SelectedTaskRef | null>(null);
const [selectedSubsectionRef, setSelectedSubsectionRef] = const [selectedTopicRef, setSelectedTopicRef] =
useState<SelectedSubsectionRef | null>(null); useState<SelectedTopicRef | null>(null);
const [tasksError, setTasksError] = useState<string | null>(null); const [tasksError, setTasksError] = useState<string | null>(null);
const [taskLocked, setTaskLocked] = useState(false); const [taskLocked, setTaskLocked] = useState(false);
const [selectedTaskDetails, setSelectedTaskDetails] = useState<TaskDetailsResponse | null>(null); const [selectedTaskDetails, setSelectedTaskDetails] = useState<TaskDetailsResponse | null>(null);
...@@ -168,34 +168,34 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -168,34 +168,34 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
[selectedTaskFile] [selectedTaskFile]
); );
const subsectionOptions = useMemo( const topicOptions = useMemo(
() => () =>
subsections.map((option) => ({ topics.map((option) => ({
value: option.subsection_key, value: option.topic_key,
label: option.label || option.subsection_key, label: option.label || option.topic_key,
})), })),
[subsections] [topics]
); );
const selectedSubsection = useMemo<SelectedSubsection | null>(() => { const selectedTopic = useMemo<SelectedTopic | null>(() => {
if (!selectedSubsectionRef) { if (!selectedTopicRef) {
return null; return null;
} }
const option = subsections.find( const option = topics.find(
(item) => item.subsection_key === selectedSubsectionRef.subsectionKey (item) => item.topic_key === selectedTopicRef.topicKey
); );
if (!option) { if (!option) {
return null; return null;
} }
const refs = option.refs ?? []; const refs = option.refs ?? [];
return { return {
subsectionKey: option.subsection_key, topicKey: option.topic_key,
label: option.label, label: option.label,
refs, refs,
refsText: formatSubsectionRefs(refs), refsText: formatTopicRefs(refs),
summary: option.summary || "", summary: option.summary || "",
}; };
}, [selectedSubsectionRef, subsections]); }, [selectedTopicRef, topics]);
const loadTaskDetails = useCallback(async (taskRef: SelectedTaskRef | null) => { const loadTaskDetails = useCallback(async (taskRef: SelectedTaskRef | null) => {
if (!taskRef) { if (!taskRef) {
...@@ -228,11 +228,11 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -228,11 +228,11 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
try { try {
const isSocratic = orchestrator === "socratic"; const isSocratic = orchestrator === "socratic";
let files: TaskFile[] = []; let files: TaskFile[] = [];
let subsectionsPayload: SubsectionOption[] = []; let topicsPayload: TopicOption[] = [];
if (isSocratic) { if (isSocratic) {
const payload = await fetchSocraticSubsections(); const payload = await fetchSocraticTopics();
subsectionsPayload = payload.subsections || []; topicsPayload = payload.topics || [];
} else { } else {
const payload = await fetchTasks(); const payload = await fetchTasks();
files = payload.task_files || []; files = payload.task_files || [];
...@@ -240,10 +240,10 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -240,10 +240,10 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const selectableFiles = files.filter((file) => Array.isArray(file.tasks) && file.tasks.length > 0); const selectableFiles = files.filter((file) => Array.isArray(file.tasks) && file.tasks.length > 0);
let nextTaskRef: SelectedTaskRef | null = null; let nextTaskRef: SelectedTaskRef | null = null;
let nextSubsectionRef: SelectedSubsectionRef | null = null; let nextTopicRef: SelectedTopicRef | null = null;
setTaskFiles(files); setTaskFiles(files);
setSubsections(subsectionsPayload); setTopics(topicsPayload);
setSelectedTaskRef((prev) => { setSelectedTaskRef((prev) => {
if (isSocratic) { if (isSocratic) {
return null; return null;
...@@ -267,26 +267,26 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -267,26 +267,26 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
nextTaskRef = { fileId: firstFile.file_id, taskId: defaultTaskId }; nextTaskRef = { fileId: firstFile.file_id, taskId: defaultTaskId };
return nextTaskRef; return nextTaskRef;
}); });
setSelectedSubsectionRef((prev) => { setSelectedTopicRef((prev) => {
if (!isSocratic) { if (!isSocratic) {
return null; return null;
} }
if (prev) { if (prev) {
const option = subsectionsPayload.find( const option = topicsPayload.find(
(item) => item.subsection_key === prev.subsectionKey (item) => item.topic_key === prev.topicKey
); );
if (option) { if (option) {
nextSubsectionRef = prev; nextTopicRef = prev;
return prev; return prev;
} }
} }
const firstOption = subsectionsPayload[0]; const firstOption = topicsPayload[0];
if (!firstOption) { if (!firstOption) {
return null; return null;
} }
nextSubsectionRef = { subsectionKey: firstOption.subsection_key }; nextTopicRef = { topicKey: firstOption.topic_key };
return nextSubsectionRef; return nextTopicRef;
}); });
if (isSocratic) { if (isSocratic) {
taskDetailsRequestRef.current += 1; taskDetailsRequestRef.current += 1;
...@@ -299,13 +299,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -299,13 +299,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
void error; void error;
} }
} }
void nextSubsectionRef; void nextTopicRef;
} catch (error) { } catch (error) {
setTasksError(t("failedLoadTasks")); setTasksError(t("failedLoadTasks"));
setTaskFiles([]); setTaskFiles([]);
setSubsections([]); setTopics([]);
setSelectedTaskRef(null); setSelectedTaskRef(null);
setSelectedSubsectionRef(null); setSelectedTopicRef(null);
taskDetailsRequestRef.current += 1; taskDetailsRequestRef.current += 1;
setSelectedTaskDetails(null); setSelectedTaskDetails(null);
void error; void error;
...@@ -406,12 +406,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -406,12 +406,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
[loadTaskDetails, selectedTaskRef] [loadTaskDetails, selectedTaskRef]
); );
const setSubsectionKey = useCallback( const setTopicKey = useCallback(
(subsectionKey: string) => { (topicKey: string) => {
if (!subsectionKey) { if (!topicKey) {
return; return;
} }
setSelectedSubsectionRef({ subsectionKey }); setSelectedTopicRef({ topicKey });
}, },
[] []
); );
...@@ -434,7 +434,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -434,7 +434,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedTaskDetails(null); setSelectedTaskDetails(null);
} }
if (value !== "socratic") { if (value !== "socratic") {
setSelectedSubsectionRef(null); setSelectedTopicRef(null);
} }
}, []); }, []);
...@@ -449,7 +449,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -449,7 +449,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const resetForNewChat = useCallback(() => { const resetForNewChat = useCallback(() => {
setChatSessionId(createSessionId()); setChatSessionId(createSessionId());
setSelectedTaskRef(null); setSelectedTaskRef(null);
setSelectedSubsectionRef(null); setSelectedTopicRef(null);
setTaskLocked(false); setTaskLocked(false);
taskDetailsRequestRef.current += 1; taskDetailsRequestRef.current += 1;
setSelectedTaskDetails(null); setSelectedTaskDetails(null);
...@@ -466,20 +466,20 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -466,20 +466,20 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
isOrchestratorSelectable, isOrchestratorSelectable,
orchestratorError, orchestratorError,
taskFiles, taskFiles,
subsections, topics,
selectedTaskRef, selectedTaskRef,
selectedTask, selectedTask,
selectedTaskFile, selectedTaskFile,
taskFileOptions, taskFileOptions,
taskOptions, taskOptions,
selectedSubsectionRef, selectedTopicRef,
selectedSubsection, selectedTopic,
subsectionOptions, topicOptions,
setTaskRef, setTaskRef,
setSubsectionRef: setSelectedSubsectionRef, setTopicRef: setSelectedTopicRef,
setTaskFile, setTaskFile,
setTaskId, setTaskId,
setSubsectionKey, setTopicKey,
tasksError, tasksError,
isTaskModeEnabled, isTaskModeEnabled,
isTasksInitialized, isTasksInitialized,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment