Commit 7a683fea authored by Kantz's avatar Kantz
Browse files

weitere entfernung von subsection elementen

parent 134a9c5c
......@@ -8,7 +8,6 @@ DAILY_LLM_TOKEN_LIMIT="500000"
FRONTEND_URL="http://frontend:3000"
ORCHESTRATOR="task" # "tutor", "task" or "qa"
RETRIEVAL_IMPL="child" # "child" or "subsection"
TASK_FOLDER="tasks"
LLM_PROVIDER="gwdg" # "openai", "gwdg", "mistral", or "ollama"
......
......@@ -6,7 +6,6 @@ from app.deterministic_services import llm_client
ParentRef = tuple[int, int, int, int]
SubsectionRef = tuple[int, int, int]
HINT_SYSTEM_PROMPT = r"""
......@@ -45,14 +44,11 @@ def _format_parent_refs(parent_refs: Iterable[ParentRef] | None) -> str:
def generate_dialog(
query: str | None,
subsection_refs: list[SubsectionRef] | None = None,
parent_refs: list[ParentRef] | None = None,
history: list[dict] | None = None,
sources: str | None = None,
) -> str:
effective_parent_refs = parent_refs
if effective_parent_refs is None and subsection_refs is not None:
effective_parent_refs = [(chap, sec, sub, 0) for chap, sec, sub in subsection_refs]
context_parts = [
f"Parent-Referenzen:\n{_format_parent_refs(effective_parent_refs)}",
......
......@@ -27,12 +27,6 @@ def get_orchestrator() -> str:
return os.getenv("ORCHESTRATOR", "qa").lower()
def get_retrieval_impl() -> str:
value = os.getenv("RETRIEVAL_IMPL", "child").strip().lower()
if value in {"child", "subsection"}:
return value
return "child"
def get_task_folder() -> Path:
value = os.getenv("TASK_FOLDER", "tasks").strip()
......
......@@ -2,13 +2,10 @@ from __future__ import annotations
from typing import List
import app.config as config
from app.deterministic_services import vector_store, vector_store_subsection
from app.deterministic_services import vector_store
from app.deterministic_services.vector_store import EmbeddingLike, Source
def _use_subsection_retrieval() -> bool:
return config.get_retrieval_impl() == "subsection"
def retrieve(
......@@ -24,20 +21,6 @@ def retrieve(
expand_links: bool = True,
neighbor_expand: int = 0,
) -> List[Source]:
if _use_subsection_retrieval():
return vector_store_subsection.retrieve(
pg_url=pg_url,
embedder=embedder,
query=query,
k=k,
chapter_index=chapter_index,
section_index=section_index,
subsection_index=subsection_index,
subsubsection_index=subsubsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
return vector_store.retrieve(
pg_url=pg_url,
......@@ -58,7 +41,6 @@ def retrieve_with_subsections(
pg_url: str,
embedder: EmbeddingLike,
query: str,
subsection_refs: list[vector_store.SubsectionRef] | None = None,
k: int = 4,
chapter_index: int | None = None,
section_index: int | None = None,
......@@ -68,27 +50,11 @@ def retrieve_with_subsections(
expand_links: bool = True,
neighbor_expand: int = 0,
) -> List[Source]:
if _use_subsection_retrieval():
return vector_store_subsection.retrieve_with_subsections(
pg_url=pg_url,
embedder=embedder,
query=query,
subsection_refs=subsection_refs,
k=k,
chapter_index=chapter_index,
section_index=section_index,
subsection_index=subsection_index,
subsubsection_index=subsubsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
return vector_store.retrieve_with_subsections(
pg_url=pg_url,
embedder=embedder,
query=query,
subsection_refs=subsection_refs,
k=k,
chapter_index=chapter_index,
section_index=section_index,
......@@ -100,17 +66,6 @@ def retrieve_with_subsections(
)
def retrieve_for_subsections(
pg_url: str,
subsection_refs: list[vector_store.SubsectionRef] | None = None,
) -> List[Source]:
# Deprecated: subsection-ref retrieval is kept only for legacy task/socratic flows.
return vector_store.load_children_for_subsections(
pg_url=pg_url,
subsection_refs=subsection_refs,
)
def retrieve_for_parent_refs(
pg_url: str,
parent_refs: list[vector_store.ParentRef] | None = None,
......
......@@ -75,7 +75,7 @@ def get_topic_entry(topic_key: str, path: Path = PROMPTS_PATH) -> dict[str, Any]
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)
label_source = str(item.get("label") or item.get("topic") or normalized_key)
return {
"topic_key": normalized_key,
"label": task_catalog._format_topic_label(label_source) or task_catalog._format_topic_label(normalized_key),
......
......@@ -856,12 +856,10 @@ def retrieve(
return sorted(sources, key=lambda source: source.score, reverse=True)
# --------------------------------------------------------------------------------------------------------------------
# Retrival mit Subsection Referenzen
# Retrival mit Parent Referenzen
# --------------------------------------------------------------------------------------------------------------------
# Deprecated: subsection refs only represent the legacy subsection-centric task/socratic flow.
SubsectionRef = Tuple[int, int, int]
ParentRef = Tuple[int, int, int, int]
......@@ -892,58 +890,6 @@ def _parent_ref_scope(ref: ParentRef) -> DominantScope:
return DominantScope(chapter_index=chap, section_index=sec, subsection_index=sub, subsubsection_index=subsub, level="subsubsection")
def _normalize_subsection_refs(
subsection_refs: Optional[List[SubsectionRef]],
) -> List[SubsectionRef]:
# Deprecated compatibility helper for subsection-only retrieval.
if not subsection_refs:
return []
normalized = {
(int(chap), int(sec), int(sub)) for chap, sec, sub in subsection_refs
}
return sorted(normalized)
def load_children_for_subsections(
pg_url: str,
subsection_refs: Optional[List[SubsectionRef]],
) -> List[Source]:
# Deprecated compatibility path for subsection-only task/socratic retrieval.
refs = _normalize_subsection_refs(subsection_refs)
if not refs:
return []
chap_arr = [chap for chap, _, _ in refs]
sec_arr = [sec for _, sec, _ in refs]
sub_arr = [sub for _, _, sub in refs]
sql = """
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.subsubsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.subsubsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
JOIN unnest(%(chap_arr)s::int[], %(sec_arr)s::int[], %(sub_arr)s::int[]) AS u(chap, sec, sub)
ON d.chapter_index = u.chap AND d.section_index = u.sec AND d.subsection_index = u.sub
WHERE d.doc_type = 'child'
ORDER BY d.chapter_index, d.section_index, d.subsection_index, d.child_index
"""
with psycopg.connect(pg_url, row_factory=dict_row) as conn:
with conn.cursor() as cur:
cur.execute(
sql,
{
"chap_arr": chap_arr,
"sec_arr": sec_arr,
"sub_arr": sub_arr,
},
)
rows = cur.fetchall()
children = [_row_to_retrieved(row) for row in rows]
return _retrivla_to_sources({"task_childs": children})
def load_sources_for_parent_refs(
pg_url: str,
......@@ -1111,45 +1057,6 @@ def merge_sources(primary: List[Source], additional: List[Source]) -> List[Sourc
return merged
def retrieve_with_subsections(
pg_url: str,
embedder: EmbeddingLike,
query: str,
subsection_refs: Optional[List[SubsectionRef]] = None,
k: int = 4,
chapter_index: Optional[int] = None,
section_index: Optional[int] = None,
subsection_index: Optional[int] = None,
subsubsection_index: Optional[int] = None,
source_type_filter: Optional[List[str]] = None,
expand_links: bool = True,
neighbor_expand: int = 0,
) -> List[Source]:
# Deprecated compatibility path for subsection-only retrieval composition.
# Oversampling improves recall with ivfflat when additional filters exclude
# close hits. We trim back to k after retrieval.
vector_k = max(k * 4, k + 16)
vector_sources = retrieve(
pg_url=pg_url,
embedder=embedder,
query=query,
k=vector_k,
chapter_index=chapter_index,
section_index=section_index,
subsection_index=subsection_index,
subsubsection_index=subsubsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
vector_sources = vector_sources[:k]
subsection_children = load_children_for_subsections(
pg_url=pg_url,
subsection_refs=subsection_refs,
)
return merge_sources(vector_sources, subsection_children)
# --------------------------------------------------------------------------------------------------------------------
# Retrival in Sources umwandeln
# --------------------------------------------------------------------------------------------------------------------
......
from __future__ import annotations
from typing import Any, Dict, List, Optional
import psycopg
from pgvector import Vector
from pgvector.psycopg import register_vector
from psycopg.rows import dict_row
from app.deterministic_services.vector_store import (
EmbeddingLike,
Source,
SubsectionRef,
_retrivla_to_sources,
_row_to_retrieved,
embed_query,
load_children_for_subsections,
merge_sources,
)
# Deprecated: this module keeps the legacy subsection-centric retrieval path for compatibility.
def retrieve(
pg_url: str,
embedder: EmbeddingLike,
query: str,
k: int = 4,
chapter_index: Optional[int] = None,
section_index: Optional[int] = None,
subsection_index: Optional[int] = None,
subsubsection_index: Optional[int] = None,
source_type_filter: Optional[List[str]] = None,
expand_links: bool = False,
neighbor_expand: int = 0,
) -> List[Source]:
# Deprecated compatibility path. Parameters kept for drop-in compatibility with child-level retrieve.
_ = expand_links
_ = neighbor_expand
_ = subsubsection_index
qvec = Vector(embed_query(embedder, query))
where = ["doc_type = ANY(%(sub_doc_types)s)"]
params: Dict[str, Any] = {
"qvec": qvec,
"k": k,
"sub_doc_types": ["subsection", "chapter"],
}
if chapter_index is not None:
where.append("chapter_index = %(chapter_index)s")
params["chapter_index"] = chapter_index
if section_index is not None:
where.append("section_index = %(section_index)s")
params["section_index"] = section_index
if subsection_index is not None:
where.append("subsection_index = %(subsection_index)s")
params["subsection_index"] = subsection_index
if source_type_filter:
where.append("source_type = ANY(%(source_type_filter)s)")
params["source_type_filter"] = source_type_filter
where_sql = " AND ".join(where)
sql = f"""
SELECT
uid, doc_type,
chapter_index, section_index, subsection_index, subsubsection_index, child_index,
chapter_title, section_title, subsection_title, subsubsection_title, title, source_type,
path, markdown,
1 - (embedding <=> %(qvec)s) AS score
FROM docs
WHERE {where_sql}
ORDER BY embedding <=> %(qvec)s
LIMIT %(k)s;
"""
with psycopg.connect(pg_url, row_factory=dict_row) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute(sql, params)
rows = cur.fetchall()
subsections = [_row_to_retrieved(
row, source_type="subsection") for row in rows]
return _retrivla_to_sources({"subsections_direct": subsections})
def retrieve_with_subsections(
pg_url: str,
embedder: EmbeddingLike,
query: str,
subsection_refs: Optional[List[SubsectionRef]] = None,
k: int = 4,
chapter_index: Optional[int] = None,
section_index: Optional[int] = None,
subsection_index: Optional[int] = None,
subsubsection_index: Optional[int] = None,
source_type_filter: Optional[List[str]] = None,
expand_links: bool = False,
neighbor_expand: int = 0,
) -> List[Source]:
# Deprecated compatibility path for subsection-only retrieval composition.
vector_k = max(k * 4, k + 16)
vector_sources = retrieve(
pg_url=pg_url,
embedder=embedder,
query=query,
k=vector_k,
chapter_index=chapter_index,
section_index=section_index,
subsection_index=subsection_index,
subsubsection_index=subsubsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
vector_sources = vector_sources[:k]
subsection_children = load_children_for_subsections(
pg_url=pg_url,
subsection_refs=subsection_refs,
)
return merge_sources(vector_sources, subsection_children)
......@@ -40,15 +40,15 @@ def load_json_task(path: Path) -> dict[str, Any]:
return payload
def _build_topic_entries(subsections: Any) -> list[dict[str, str]]:
if not isinstance(subsections, list):
def _build_topic_entries(topics: Any) -> list[dict[str, str]]:
if not isinstance(topics, list):
return []
topics: list[dict[str, str]] = []
for item in subsections:
topic_entries: list[dict[str, str]] = []
for item in topics:
topic_id = str(item).strip()
if topic_id:
topics.append({"topic-id": topic_id})
return topics
topic_entries.append({"topic-id": topic_id})
return topic_entries
def _build_text_block(text: str) -> dict[str, str]:
......@@ -105,7 +105,7 @@ def convert_task_payload(payload: dict[str, Any], source_file_id: str, image_pat
"titel": title,
"slug": slug,
"description": intro,
"exercise_topic": _build_topic_entries(payload.get("subsections", [])),
"exercise_topic": _build_topic_entries(payload.get("topics", [])),
"tasks": converted_tasks,
}
......
......@@ -4,20 +4,20 @@ import { t } from "../../i18n";
import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown";
type SocraticPanelProps = {
selectedSubsectionLabel?: string;
selectedSubsectionKey?: string;
selectedSubsectionSummary?: string;
selectedTopicLabel?: string;
selectedTopicKey?: string;
selectedTopicSummary?: string;
onChangeSelection?: () => void;
};
export default function SocraticPanel({
selectedSubsectionLabel,
selectedSubsectionKey,
selectedSubsectionSummary,
selectedTopicLabel,
selectedTopicKey,
selectedTopicSummary,
onChangeSelection,
}: SocraticPanelProps) {
const contentRef = useRef<HTMLDivElement | null>(null);
const summaryMarkdown = selectedSubsectionSummary?.trim() || "";
const summaryMarkdown = selectedTopicSummary?.trim() || "";
const renderedSummary = useMemo(
() => escapeAsterisksInsideMath(summaryMarkdown),
[summaryMarkdown]
......@@ -32,7 +32,7 @@ export default function SocraticPanel({
return;
}
mathjax.typesetPromise([contentRef.current]).catch(() => undefined);
}, [selectedSubsectionSummary]);
}, [selectedTopicSummary]);
return (
<section className="task-panel">
......@@ -40,18 +40,18 @@ export default function SocraticPanel({
<div className="task-panel-title">{t("orchestratorModeSocraticLabel")}</div>
{onChangeSelection ? (
<button type="button" className="btn task-panel-change-btn" onClick={onChangeSelection}>
{t("changeSubsectionArea")}
{t("changeTopicArea")}
</button>
) : null}
</div>
<div className="task-panel-meta">
<div>{selectedSubsectionLabel || ""}</div>
<div>{selectedSubsectionKey ? `${t("subsectionKey")}: ${selectedSubsectionKey}` : ""}</div>
<div>{selectedTopicLabel || ""}</div>
<div>{selectedTopicKey ? `${t("topicKey")}: ${selectedTopicKey}` : ""}</div>
</div>
<div className="task-panel-content socratic-summary-content" ref={contentRef}>
{renderedSummary ? <ReactMarkdown>{renderedSummary}</ReactMarkdown> : selectedSubsectionLabel || t("noSubsectionSelected")}
{renderedSummary ? <ReactMarkdown>{renderedSummary}</ReactMarkdown> : selectedTopicLabel || t("noTopicSelected")}
</div>
</section>
);
......
......@@ -27,7 +27,7 @@
orchestratorModeTutorDescription: "Help with your own questions",
orchestratorModeTaskDescription: "Help with textbook exercises",
orchestratorModeSocraticDescription:
"Socratic guidance based on fixed subsection context for textbook exercises",
"Socratic guidance based on fixed topic context for textbook exercises",
directChildren: "Direct children",
taskChildren: "Task sources",
indirectChildren: "Indirect children",
......@@ -49,8 +49,8 @@
hideThinking: "Hide thinking",
noTasksAvailable: "No tasks available",
noTaskSelected: "No task selected.",
noSubsectionsAvailable: "No subsections available",
noSubsectionSelected: "No subsection selected.",
noTopicsAvailable: "No Topics available",
noTopicSelected: "No topic selected.",
savedChats: "Saved Chats",
saving: "Saving...",
noSavedChatsYet: "No saved chats yet.",
......@@ -74,16 +74,16 @@
"Saving the canvas failed. Please check backend logs.",
taskSelectionTitle: "Select a Task",
taskSelectionSubtitle: "Choose a task and start a tutor session",
socraticSelectionTitle: "Select a Subsection",
socraticSelectionSubtitle: "Choose a subsection and start a socratic session",
subsection: "Subsection",
socraticSelectionTitle: "Select a Topic",
socraticSelectionSubtitle: "Choose a topic and start a socratic session",
topic: "Topic",
taskFile: "Task Set",
taskId: "Task ID",
subsectionKey: "Subsection Key",
topicKey: "Topic Key",
solveWithTutor: "Solve with Tutor",
startSocratic: "Start Socratic",
changeTaskArea: "Change Task Area",
changeSubsectionArea: "Change Subsection Area",
changeTopicArea: "Change Topic Area",
previousTask: "Previous Task",
nextTask: "Next Task",
backendChecking: "Checking backend availability...",
......@@ -94,9 +94,9 @@
lastCheckFailed: "Last check: {detail}",
deepLinkInvalidTask: "Invalid task link. Please choose a task manually.",
deepLinkInitFailed: "Task link initialization failed. Please choose a task manually.",
deepLinkInvalidSubsection: "Invalid subsection link. Please choose a subsection manually.",
deepLinkInitFailedSubsection:
"Subsection link initialization failed. Please choose a subsection manually.",
deepLinkInvalidTopic: "Invalid topic link. Please choose a topic manually.",
deepLinkInitFailedTopic:
"Topic link initialization failed. Please choose a topic manually.",
},
de: {
chats: "Chats",
......@@ -127,7 +127,7 @@
orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen",
orchestratorModeTaskDescription: "Hilfe bei Aufgaben aus dem Lehrwerk",
orchestratorModeSocraticDescription:
"Sokratische Anleitung auf Basis fester Subsection-Kontexte für Aufgaben aus dem Lehrwerk",
"Sokratische Anleitung auf Basis fester Topic-Kontexte für Aufgaben aus dem Lehrwerk",
directChildren: "Direkte Quellen",
taskChildren: "Aufgaben-Quellen",
indirectChildren: "Indirekte Quellen",
......@@ -149,8 +149,8 @@
hideThinking: "Thinking ausblenden",
noTasksAvailable: "Keine Aufgaben verfügbar",
noTaskSelected: "Keine Aufgabe ausgewählt.",
noSubsectionsAvailable: "Keine Unterabschnitte verfügbar",
noSubsectionSelected: "Kein Unterabschnitt ausgewählt.",
noTopicsAvailable: "Keine Unterabschnitte verfügbar",
noTopicSelected: "Kein Unterabschnitt ausgewählt.",
savedChats: "Gespeicherte Chats",
saving: "Speichere...",
noSavedChatsYet: "Noch keine gespeicherten Chats.",
......@@ -180,16 +180,16 @@
"Da ist wohl das Speichern des Canvas fehlgeschlagen. Gib gerne deinem Dozenten bescheid. In vielen Fällen hilft es die Seite neu zu laden.",
taskSelectionTitle: "Aufgabe auswählen",
taskSelectionSubtitle: "Wähle eine Aufgabe und starte den Tutor-Chat",
socraticSelectionTitle: "Unterabschnitt auswählen",
socraticSelectionSubtitle: "Wähle einen Unterabschnitt und starte den sokratischen Chat",
subsection: "Unterabschnitt",
socraticSelectionTitle: "Thema auswählen",
socraticSelectionSubtitle: "Wähle ein Thema und starte den sokratischen Chat",
topic: "Thema",
taskFile: "Aufgabenset",
taskId: "Aufgaben-ID",
subsectionKey: "Unterabschnitt-Schlüssel",
topicKey: "Thema-Schlüssel",
solveWithTutor: "Mit Tutor lösen",
startSocratic: "Sokratischen Dialog führen",
changeTaskArea: "Aufgabengebiet ändern",
changeSubsectionArea: "Unterabschnitt ändern",
changeTopicArea: "Thema ändern",
previousTask: "Vorherige Aufgabe",
nextTask: "Nächste Aufgabe",
backendChecking: "Backend-Verbindung wird geprüft...",
......@@ -202,10 +202,10 @@
"Ungültiger Aufgaben-Link. Bitte wähle die Aufgabe manuell aus.",
deepLinkInitFailed:
"Der Aufgaben-Link konnte nicht initialisiert werden. Bitte wähle die Aufgabe manuell aus.",
deepLinkInvalidSubsection:
"Ungültiger Unterabschnitt-Link. Bitte wähle den Unterabschnitt manuell aus.",
deepLinkInitFailedSubsection:
"Der Unterabschnitt-Link konnte nicht initialisiert werden. Bitte wähle den Unterabschnitt manuell aus.",
deepLinkInvalidTopic:
"Ungültiger Themenseite-Link. Bitte wähle das Thema manuell aus.",
deepLinkInitFailedTopic:
"Der Themenseite-Link konnte nicht initialisiert werden. Bitte wähle das Thema manuell aus.",
},
} as const;
......
......@@ -14,7 +14,7 @@ import { selectTask, selectTopic } from "../api/taskApi";
import { t } from "../i18n";
import { createSessionId, useTutorSession } from "../state/tutorSession";
import { getSelectionRouteForOrchestrator, isSocraticOrchestrator } from "../utils/orchestratorRoutes";
import { normalizeSubsectionKey as normalizeTopicKey } from "../utils/subsectionKey";
import { normalizeTopicKey as normalizeTopicKey } from "../utils/topicKey";
import sumintLogo from "../../SuMINT-Logo.png";
const initialMessages: ChatMessage[] = [];
......@@ -240,7 +240,7 @@ export default function ChatPage() {
const fileId = String(searchParams.get("file_id") || "").trim();
const rawTaskId = String(searchParams.get("task_id") || "").trim();
const rawTopicKey = String(
searchParams.get("topic_key") || searchParams.get("subsection_key") || ""
searchParams.get("topic_key") || ""
).trim();
const taskId =
/^\d{1,2}$/.test(rawTaskId) && rawTaskId.length < 2
......@@ -291,7 +291,7 @@ export default function ChatPage() {
(deepLinkTarget.isSocratic && !deepLinkTarget.hasRequiredTopicParams)
) {
setDeepLinkError(
deepLinkTarget.isSocratic ? t("deepLinkInvalidSubsection") : t("deepLinkInvalidTask")
deepLinkTarget.isSocratic ? t("deepLinkInvalidTopic") : t("deepLinkInvalidTask")
);
setTaskRef(null);
setTopicRef(null);
......@@ -355,7 +355,7 @@ export default function ChatPage() {
} catch (error) {
if (!cancelled) {
setDeepLinkError(
deepLinkTarget.isSocratic ? t("deepLinkInitFailedSubsection") : t("deepLinkInitFailed")
deepLinkTarget.isSocratic ? t("deepLinkInitFailedTopic") : t("deepLinkInitFailed")
);
setTaskRef(null);
setTopicRef(null);
......@@ -1272,9 +1272,9 @@ export default function ChatPage() {
<aside className={`retrieval-column ${isTaskModeEnabled ? "retrieval-column-task-mode" : ""}`}>
{selectedOrchestrator === "socratic" && selectedTopic ? (
<SocraticPanel
selectedSubsectionLabel={selectedTopic.label}
selectedSubsectionKey={selectedTopic.topicKey}
selectedSubsectionSummary={selectedTopic.summary}
selectedTopicLabel={selectedTopic.label}
selectedTopicKey={selectedTopic.topicKey}
selectedTopicSummary={selectedTopic.summary}
onChangeSelection={handleChangeTaskArea}
/>
) : null}
......
......@@ -27,9 +27,9 @@ export default function SocraticSelectionPage() {
unlockTask,
isTasksInitialized,
} = useTutorSession();
const subsectionDisplayRef = useRef<HTMLDivElement | null>(null);
const topicDisplayRef = useRef<HTMLDivElement | null>(null);
const subsectionMenuOptions = useMemo(
const topicMenuOptions = useMemo(
() =>
topics.map((option) => ({
value: option.topic_key,
......@@ -50,28 +50,28 @@ export default function SocraticSelectionPage() {
}, [isTasksInitialized, navigate, selectedOrchestrator, unlockTask]);
useEffect(() => {
if (!selectedTopic?.label || !subsectionDisplayRef.current) {
if (!selectedTopic?.label || !topicDisplayRef.current) {
return;
}
const mathjax = window.MathJax;
if (!mathjax?.typesetPromise) {
return;
}
mathjax.typesetPromise([subsectionDisplayRef.current]).catch(() => undefined);
mathjax.typesetPromise([topicDisplayRef.current]).catch(() => undefined);
}, [selectedTopic?.label]);
useEffect(() => {
if (!subsectionMenuOptions.length) {
if (!topicMenuOptions.length) {
return;
}
if (
selectedTopicRef &&
subsectionMenuOptions.some((option) => option.value === selectedTopicRef.topicKey)
topicMenuOptions.some((option) => option.value === selectedTopicRef.topicKey)
) {
return;
}
setTopicKey(subsectionMenuOptions[0].value);
}, [selectedTopicRef, setTopicKey, subsectionMenuOptions]);
setTopicKey(topicMenuOptions[0].value);
}, [selectedTopicRef, setTopicKey, topicMenuOptions]);
const handleStartSocratic = async () => {
if (!selectedTopicRef) {
......@@ -131,24 +131,24 @@ export default function SocraticSelectionPage() {
</div>
<div className="task-select-controls">
<label className="task-select-label" htmlFor="socratic-subsection-select">
{t("subsection")}
<label className="task-select-label" htmlFor="socratic-topic-select">
{t("topic")}
</label>
<select
id="socratic-subsection-select"
id="socratic-topic-select"
className="task-select"
value={selectedTopicRef?.topicKey || ""}
onChange={(event) => setTopicKey(event.target.value)}
disabled={!subsectionMenuOptions.length}
disabled={!topicMenuOptions.length}
>
{subsectionMenuOptions.length ? (
subsectionMenuOptions.map((option) => (
{topicMenuOptions.length ? (
topicMenuOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))
) : (
<option value="">{t("noSubsectionsAvailable")}</option>
<option value="">{t("noTopicsAvailable")}</option>
)}
</select>
</div>
......
const collapseWhitespace = (value: string): string => value.replace(/\s+/g, " ").trim();
export const normalizeSubsectionKey = (value: string): string =>
export const normalizeTopicKey = (value: string): string =>
collapseWhitespace(String(value).replace(/[-_]+/g, " ").toLowerCase());
export const subsectionKeyToSlug = (value: string): string =>
normalizeSubsectionKey(value).replace(/\s+/g, "-");
export const topicKeyToSlug = (value: string): string =>
normalizeTopicKey(value).replace(/\s+/g, "-");
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