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" ...@@ -8,7 +8,6 @@ DAILY_LLM_TOKEN_LIMIT="500000"
FRONTEND_URL="http://frontend:3000" FRONTEND_URL="http://frontend:3000"
ORCHESTRATOR="task" # "tutor", "task" or "qa" ORCHESTRATOR="task" # "tutor", "task" or "qa"
RETRIEVAL_IMPL="child" # "child" or "subsection"
TASK_FOLDER="tasks" TASK_FOLDER="tasks"
LLM_PROVIDER="gwdg" # "openai", "gwdg", "mistral", or "ollama" LLM_PROVIDER="gwdg" # "openai", "gwdg", "mistral", or "ollama"
......
...@@ -6,7 +6,6 @@ from app.deterministic_services import llm_client ...@@ -6,7 +6,6 @@ from app.deterministic_services import llm_client
ParentRef = tuple[int, int, int, int] ParentRef = tuple[int, int, int, int]
SubsectionRef = tuple[int, int, int]
HINT_SYSTEM_PROMPT = r""" HINT_SYSTEM_PROMPT = r"""
...@@ -45,14 +44,11 @@ def _format_parent_refs(parent_refs: Iterable[ParentRef] | None) -> str: ...@@ -45,14 +44,11 @@ def _format_parent_refs(parent_refs: Iterable[ParentRef] | None) -> str:
def generate_dialog( def generate_dialog(
query: str | None, query: str | None,
subsection_refs: list[SubsectionRef] | None = None,
parent_refs: list[ParentRef] | None = None, parent_refs: list[ParentRef] | None = None,
history: list[dict] | None = None, history: list[dict] | None = None,
sources: str | None = None, sources: str | None = None,
) -> str: ) -> str:
effective_parent_refs = parent_refs 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 = [ context_parts = [
f"Parent-Referenzen:\n{_format_parent_refs(effective_parent_refs)}", f"Parent-Referenzen:\n{_format_parent_refs(effective_parent_refs)}",
......
...@@ -27,12 +27,6 @@ def get_orchestrator() -> str: ...@@ -27,12 +27,6 @@ def get_orchestrator() -> str:
return os.getenv("ORCHESTRATOR", "qa").lower() 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: def get_task_folder() -> Path:
value = os.getenv("TASK_FOLDER", "tasks").strip() value = os.getenv("TASK_FOLDER", "tasks").strip()
......
...@@ -2,13 +2,10 @@ from __future__ import annotations ...@@ -2,13 +2,10 @@ from __future__ import annotations
from typing import List from typing import List
import app.config as config from app.deterministic_services import vector_store
from app.deterministic_services import vector_store, vector_store_subsection
from app.deterministic_services.vector_store import EmbeddingLike, Source from app.deterministic_services.vector_store import EmbeddingLike, Source
def _use_subsection_retrieval() -> bool:
return config.get_retrieval_impl() == "subsection"
def retrieve( def retrieve(
...@@ -24,20 +21,6 @@ def retrieve( ...@@ -24,20 +21,6 @@ def retrieve(
expand_links: bool = True, expand_links: bool = True,
neighbor_expand: int = 0, neighbor_expand: int = 0,
) -> List[Source]: ) -> 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( return vector_store.retrieve(
pg_url=pg_url, pg_url=pg_url,
...@@ -58,7 +41,6 @@ def retrieve_with_subsections( ...@@ -58,7 +41,6 @@ def retrieve_with_subsections(
pg_url: str, pg_url: str,
embedder: EmbeddingLike, embedder: EmbeddingLike,
query: str, query: str,
subsection_refs: list[vector_store.SubsectionRef] | None = None,
k: int = 4, k: int = 4,
chapter_index: int | None = None, chapter_index: int | None = None,
section_index: int | None = None, section_index: int | None = None,
...@@ -68,27 +50,11 @@ def retrieve_with_subsections( ...@@ -68,27 +50,11 @@ def retrieve_with_subsections(
expand_links: bool = True, expand_links: bool = True,
neighbor_expand: int = 0, neighbor_expand: int = 0,
) -> List[Source]: ) -> 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( return vector_store.retrieve_with_subsections(
pg_url=pg_url, pg_url=pg_url,
embedder=embedder, embedder=embedder,
query=query, query=query,
subsection_refs=subsection_refs,
k=k, k=k,
chapter_index=chapter_index, chapter_index=chapter_index,
section_index=section_index, section_index=section_index,
...@@ -100,17 +66,6 @@ def retrieve_with_subsections( ...@@ -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( def retrieve_for_parent_refs(
pg_url: str, pg_url: str,
parent_refs: list[vector_store.ParentRef] | None = None, 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] ...@@ -75,7 +75,7 @@ def get_topic_entry(topic_key: str, path: Path = PROMPTS_PATH) -> dict[str, Any]
if not refs: if not refs:
raise ValueError(f"missing refs for topic '{normalized_key}'") 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 { return {
"topic_key": normalized_key, "topic_key": normalized_key,
"label": task_catalog._format_topic_label(label_source) or task_catalog._format_topic_label(normalized_key), "label": task_catalog._format_topic_label(label_source) or task_catalog._format_topic_label(normalized_key),
......
...@@ -856,12 +856,10 @@ def retrieve( ...@@ -856,12 +856,10 @@ def retrieve(
return sorted(sources, key=lambda source: source.score, reverse=True) 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] ParentRef = Tuple[int, int, int, int]
...@@ -892,58 +890,6 @@ def _parent_ref_scope(ref: ParentRef) -> DominantScope: ...@@ -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") 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( def load_sources_for_parent_refs(
pg_url: str, pg_url: str,
...@@ -1111,45 +1057,6 @@ def merge_sources(primary: List[Source], additional: List[Source]) -> List[Sourc ...@@ -1111,45 +1057,6 @@ def merge_sources(primary: List[Source], additional: List[Source]) -> List[Sourc
return merged 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 # 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]: ...@@ -40,15 +40,15 @@ def load_json_task(path: Path) -> dict[str, Any]:
return payload return payload
def _build_topic_entries(subsections: Any) -> list[dict[str, str]]: def _build_topic_entries(topics: Any) -> list[dict[str, str]]:
if not isinstance(subsections, list): if not isinstance(topics, list):
return [] return []
topics: list[dict[str, str]] = [] topic_entries: list[dict[str, str]] = []
for item in subsections: for item in topics:
topic_id = str(item).strip() topic_id = str(item).strip()
if topic_id: if topic_id:
topics.append({"topic-id": topic_id}) topic_entries.append({"topic-id": topic_id})
return topics return topic_entries
def _build_text_block(text: str) -> dict[str, str]: 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 ...@@ -105,7 +105,7 @@ def convert_task_payload(payload: dict[str, Any], source_file_id: str, image_pat
"titel": title, "titel": title,
"slug": slug, "slug": slug,
"description": intro, "description": intro,
"exercise_topic": _build_topic_entries(payload.get("subsections", [])), "exercise_topic": _build_topic_entries(payload.get("topics", [])),
"tasks": converted_tasks, "tasks": converted_tasks,
} }
......
...@@ -4,20 +4,20 @@ import { t } from "../../i18n"; ...@@ -4,20 +4,20 @@ import { t } from "../../i18n";
import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown"; import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown";
type SocraticPanelProps = { type SocraticPanelProps = {
selectedSubsectionLabel?: string; selectedTopicLabel?: string;
selectedSubsectionKey?: string; selectedTopicKey?: string;
selectedSubsectionSummary?: string; selectedTopicSummary?: string;
onChangeSelection?: () => void; onChangeSelection?: () => void;
}; };
export default function SocraticPanel({ export default function SocraticPanel({
selectedSubsectionLabel, selectedTopicLabel,
selectedSubsectionKey, selectedTopicKey,
selectedSubsectionSummary, selectedTopicSummary,
onChangeSelection, onChangeSelection,
}: SocraticPanelProps) { }: SocraticPanelProps) {
const contentRef = useRef<HTMLDivElement | null>(null); const contentRef = useRef<HTMLDivElement | null>(null);
const summaryMarkdown = selectedSubsectionSummary?.trim() || ""; const summaryMarkdown = selectedTopicSummary?.trim() || "";
const renderedSummary = useMemo( const renderedSummary = useMemo(
() => escapeAsterisksInsideMath(summaryMarkdown), () => escapeAsterisksInsideMath(summaryMarkdown),
[summaryMarkdown] [summaryMarkdown]
...@@ -32,7 +32,7 @@ export default function SocraticPanel({ ...@@ -32,7 +32,7 @@ export default function SocraticPanel({
return; return;
} }
mathjax.typesetPromise([contentRef.current]).catch(() => undefined); mathjax.typesetPromise([contentRef.current]).catch(() => undefined);
}, [selectedSubsectionSummary]); }, [selectedTopicSummary]);
return ( return (
<section className="task-panel"> <section className="task-panel">
...@@ -40,18 +40,18 @@ export default function SocraticPanel({ ...@@ -40,18 +40,18 @@ export default function SocraticPanel({
<div className="task-panel-title">{t("orchestratorModeSocraticLabel")}</div> <div className="task-panel-title">{t("orchestratorModeSocraticLabel")}</div>
{onChangeSelection ? ( {onChangeSelection ? (
<button type="button" className="btn task-panel-change-btn" onClick={onChangeSelection}> <button type="button" className="btn task-panel-change-btn" onClick={onChangeSelection}>
{t("changeSubsectionArea")} {t("changeTopicArea")}
</button> </button>
) : null} ) : null}
</div> </div>
<div className="task-panel-meta"> <div className="task-panel-meta">
<div>{selectedSubsectionLabel || ""}</div> <div>{selectedTopicLabel || ""}</div>
<div>{selectedSubsectionKey ? `${t("subsectionKey")}: ${selectedSubsectionKey}` : ""}</div> <div>{selectedTopicKey ? `${t("topicKey")}: ${selectedTopicKey}` : ""}</div>
</div> </div>
<div className="task-panel-content socratic-summary-content" ref={contentRef}> <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> </div>
</section> </section>
); );
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
orchestratorModeTutorDescription: "Help with your own questions", orchestratorModeTutorDescription: "Help with your own questions",
orchestratorModeTaskDescription: "Help with textbook exercises", orchestratorModeTaskDescription: "Help with textbook exercises",
orchestratorModeSocraticDescription: orchestratorModeSocraticDescription:
"Socratic guidance based on fixed subsection context for textbook exercises", "Socratic guidance based on fixed topic context for textbook exercises",
directChildren: "Direct children", directChildren: "Direct children",
taskChildren: "Task sources", taskChildren: "Task sources",
indirectChildren: "Indirect children", indirectChildren: "Indirect children",
...@@ -49,8 +49,8 @@ ...@@ -49,8 +49,8 @@
hideThinking: "Hide thinking", hideThinking: "Hide thinking",
noTasksAvailable: "No tasks available", noTasksAvailable: "No tasks available",
noTaskSelected: "No task selected.", noTaskSelected: "No task selected.",
noSubsectionsAvailable: "No subsections available", noTopicsAvailable: "No Topics available",
noSubsectionSelected: "No subsection selected.", noTopicSelected: "No topic selected.",
savedChats: "Saved Chats", savedChats: "Saved Chats",
saving: "Saving...", saving: "Saving...",
noSavedChatsYet: "No saved chats yet.", noSavedChatsYet: "No saved chats yet.",
...@@ -74,16 +74,16 @@ ...@@ -74,16 +74,16 @@
"Saving the canvas failed. Please check backend logs.", "Saving the canvas failed. Please check backend logs.",
taskSelectionTitle: "Select a Task", taskSelectionTitle: "Select a Task",
taskSelectionSubtitle: "Choose a task and start a tutor session", taskSelectionSubtitle: "Choose a task and start a tutor session",
socraticSelectionTitle: "Select a Subsection", socraticSelectionTitle: "Select a Topic",
socraticSelectionSubtitle: "Choose a subsection and start a socratic session", socraticSelectionSubtitle: "Choose a topic and start a socratic session",
subsection: "Subsection", topic: "Topic",
taskFile: "Task Set", taskFile: "Task Set",
taskId: "Task ID", taskId: "Task ID",
subsectionKey: "Subsection Key", topicKey: "Topic Key",
solveWithTutor: "Solve with Tutor", solveWithTutor: "Solve with Tutor",
startSocratic: "Start Socratic", startSocratic: "Start Socratic",
changeTaskArea: "Change Task Area", changeTaskArea: "Change Task Area",
changeSubsectionArea: "Change Subsection Area", changeTopicArea: "Change Topic Area",
previousTask: "Previous Task", previousTask: "Previous Task",
nextTask: "Next Task", nextTask: "Next Task",
backendChecking: "Checking backend availability...", backendChecking: "Checking backend availability...",
...@@ -94,9 +94,9 @@ ...@@ -94,9 +94,9 @@
lastCheckFailed: "Last check: {detail}", lastCheckFailed: "Last check: {detail}",
deepLinkInvalidTask: "Invalid task link. Please choose a task manually.", deepLinkInvalidTask: "Invalid task link. Please choose a task manually.",
deepLinkInitFailed: "Task link initialization failed. Please choose a task manually.", deepLinkInitFailed: "Task link initialization failed. Please choose a task manually.",
deepLinkInvalidSubsection: "Invalid subsection link. Please choose a subsection manually.", deepLinkInvalidTopic: "Invalid topic link. Please choose a topic manually.",
deepLinkInitFailedSubsection: deepLinkInitFailedTopic:
"Subsection link initialization failed. Please choose a subsection manually.", "Topic link initialization failed. Please choose a topic manually.",
}, },
de: { de: {
chats: "Chats", chats: "Chats",
...@@ -127,7 +127,7 @@ ...@@ -127,7 +127,7 @@
orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen", orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen",
orchestratorModeTaskDescription: "Hilfe bei Aufgaben aus dem Lehrwerk", orchestratorModeTaskDescription: "Hilfe bei Aufgaben aus dem Lehrwerk",
orchestratorModeSocraticDescription: 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", directChildren: "Direkte Quellen",
taskChildren: "Aufgaben-Quellen", taskChildren: "Aufgaben-Quellen",
indirectChildren: "Indirekte Quellen", indirectChildren: "Indirekte Quellen",
...@@ -149,8 +149,8 @@ ...@@ -149,8 +149,8 @@
hideThinking: "Thinking ausblenden", hideThinking: "Thinking ausblenden",
noTasksAvailable: "Keine Aufgaben verfügbar", noTasksAvailable: "Keine Aufgaben verfügbar",
noTaskSelected: "Keine Aufgabe ausgewählt.", noTaskSelected: "Keine Aufgabe ausgewählt.",
noSubsectionsAvailable: "Keine Unterabschnitte verfügbar", noTopicsAvailable: "Keine Unterabschnitte verfügbar",
noSubsectionSelected: "Kein Unterabschnitt ausgewählt.", noTopicSelected: "Kein Unterabschnitt ausgewählt.",
savedChats: "Gespeicherte Chats", savedChats: "Gespeicherte Chats",
saving: "Speichere...", saving: "Speichere...",
noSavedChatsYet: "Noch keine gespeicherten Chats.", noSavedChatsYet: "Noch keine gespeicherten Chats.",
...@@ -180,16 +180,16 @@ ...@@ -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.", "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", taskSelectionTitle: "Aufgabe auswählen",
taskSelectionSubtitle: "Wähle eine Aufgabe und starte den Tutor-Chat", taskSelectionSubtitle: "Wähle eine Aufgabe und starte den Tutor-Chat",
socraticSelectionTitle: "Unterabschnitt auswählen", socraticSelectionTitle: "Thema auswählen",
socraticSelectionSubtitle: "Wähle einen Unterabschnitt und starte den sokratischen Chat", socraticSelectionSubtitle: "Wähle ein Thema und starte den sokratischen Chat",
subsection: "Unterabschnitt", topic: "Thema",
taskFile: "Aufgabenset", taskFile: "Aufgabenset",
taskId: "Aufgaben-ID", taskId: "Aufgaben-ID",
subsectionKey: "Unterabschnitt-Schlüssel", topicKey: "Thema-Schlüssel",
solveWithTutor: "Mit Tutor lösen", solveWithTutor: "Mit Tutor lösen",
startSocratic: "Sokratischen Dialog führen", startSocratic: "Sokratischen Dialog führen",
changeTaskArea: "Aufgabengebiet ändern", changeTaskArea: "Aufgabengebiet ändern",
changeSubsectionArea: "Unterabschnitt ändern", changeTopicArea: "Thema ändern",
previousTask: "Vorherige Aufgabe", previousTask: "Vorherige Aufgabe",
nextTask: "Nächste Aufgabe", nextTask: "Nächste Aufgabe",
backendChecking: "Backend-Verbindung wird geprüft...", backendChecking: "Backend-Verbindung wird geprüft...",
...@@ -202,10 +202,10 @@ ...@@ -202,10 +202,10 @@
"Ungültiger Aufgaben-Link. Bitte wähle die Aufgabe manuell aus.", "Ungültiger Aufgaben-Link. Bitte wähle die Aufgabe manuell aus.",
deepLinkInitFailed: deepLinkInitFailed:
"Der Aufgaben-Link konnte nicht initialisiert werden. Bitte wähle die Aufgabe manuell aus.", "Der Aufgaben-Link konnte nicht initialisiert werden. Bitte wähle die Aufgabe manuell aus.",
deepLinkInvalidSubsection: deepLinkInvalidTopic:
"Ungültiger Unterabschnitt-Link. Bitte wähle den Unterabschnitt manuell aus.", "Ungültiger Themenseite-Link. Bitte wähle das Thema manuell aus.",
deepLinkInitFailedSubsection: deepLinkInitFailedTopic:
"Der Unterabschnitt-Link konnte nicht initialisiert werden. Bitte wähle den Unterabschnitt manuell aus.", "Der Themenseite-Link konnte nicht initialisiert werden. Bitte wähle das Thema manuell aus.",
}, },
} as const; } as const;
......
...@@ -14,7 +14,7 @@ import { selectTask, selectTopic } from "../api/taskApi"; ...@@ -14,7 +14,7 @@ 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 as normalizeTopicKey } from "../utils/subsectionKey"; import { normalizeTopicKey as normalizeTopicKey } from "../utils/topicKey";
import sumintLogo from "../../SuMINT-Logo.png"; import sumintLogo from "../../SuMINT-Logo.png";
const initialMessages: ChatMessage[] = []; const initialMessages: ChatMessage[] = [];
...@@ -240,7 +240,7 @@ export default function ChatPage() { ...@@ -240,7 +240,7 @@ export default function ChatPage() {
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 rawTopicKey = String( const rawTopicKey = String(
searchParams.get("topic_key") || searchParams.get("subsection_key") || "" searchParams.get("topic_key") || ""
).trim(); ).trim();
const taskId = const taskId =
/^\d{1,2}$/.test(rawTaskId) && rawTaskId.length < 2 /^\d{1,2}$/.test(rawTaskId) && rawTaskId.length < 2
...@@ -291,7 +291,7 @@ export default function ChatPage() { ...@@ -291,7 +291,7 @@ export default function ChatPage() {
(deepLinkTarget.isSocratic && !deepLinkTarget.hasRequiredTopicParams) (deepLinkTarget.isSocratic && !deepLinkTarget.hasRequiredTopicParams)
) { ) {
setDeepLinkError( setDeepLinkError(
deepLinkTarget.isSocratic ? t("deepLinkInvalidSubsection") : t("deepLinkInvalidTask") deepLinkTarget.isSocratic ? t("deepLinkInvalidTopic") : t("deepLinkInvalidTask")
); );
setTaskRef(null); setTaskRef(null);
setTopicRef(null); setTopicRef(null);
...@@ -355,7 +355,7 @@ export default function ChatPage() { ...@@ -355,7 +355,7 @@ export default function ChatPage() {
} catch (error) { } catch (error) {
if (!cancelled) { if (!cancelled) {
setDeepLinkError( setDeepLinkError(
deepLinkTarget.isSocratic ? t("deepLinkInitFailedSubsection") : t("deepLinkInitFailed") deepLinkTarget.isSocratic ? t("deepLinkInitFailedTopic") : t("deepLinkInitFailed")
); );
setTaskRef(null); setTaskRef(null);
setTopicRef(null); setTopicRef(null);
...@@ -1272,9 +1272,9 @@ export default function ChatPage() { ...@@ -1272,9 +1272,9 @@ export default function ChatPage() {
<aside className={`retrieval-column ${isTaskModeEnabled ? "retrieval-column-task-mode" : ""}`}> <aside className={`retrieval-column ${isTaskModeEnabled ? "retrieval-column-task-mode" : ""}`}>
{selectedOrchestrator === "socratic" && selectedTopic ? ( {selectedOrchestrator === "socratic" && selectedTopic ? (
<SocraticPanel <SocraticPanel
selectedSubsectionLabel={selectedTopic.label} selectedTopicLabel={selectedTopic.label}
selectedSubsectionKey={selectedTopic.topicKey} selectedTopicKey={selectedTopic.topicKey}
selectedSubsectionSummary={selectedTopic.summary} selectedTopicSummary={selectedTopic.summary}
onChangeSelection={handleChangeTaskArea} onChangeSelection={handleChangeTaskArea}
/> />
) : null} ) : null}
......
...@@ -27,9 +27,9 @@ export default function SocraticSelectionPage() { ...@@ -27,9 +27,9 @@ export default function SocraticSelectionPage() {
unlockTask, unlockTask,
isTasksInitialized, isTasksInitialized,
} = useTutorSession(); } = useTutorSession();
const subsectionDisplayRef = useRef<HTMLDivElement | null>(null); const topicDisplayRef = useRef<HTMLDivElement | null>(null);
const subsectionMenuOptions = useMemo( const topicMenuOptions = useMemo(
() => () =>
topics.map((option) => ({ topics.map((option) => ({
value: option.topic_key, value: option.topic_key,
...@@ -50,28 +50,28 @@ export default function SocraticSelectionPage() { ...@@ -50,28 +50,28 @@ export default function SocraticSelectionPage() {
}, [isTasksInitialized, navigate, selectedOrchestrator, unlockTask]); }, [isTasksInitialized, navigate, selectedOrchestrator, unlockTask]);
useEffect(() => { useEffect(() => {
if (!selectedTopic?.label || !subsectionDisplayRef.current) { if (!selectedTopic?.label || !topicDisplayRef.current) {
return; return;
} }
const mathjax = window.MathJax; const mathjax = window.MathJax;
if (!mathjax?.typesetPromise) { if (!mathjax?.typesetPromise) {
return; return;
} }
mathjax.typesetPromise([subsectionDisplayRef.current]).catch(() => undefined); mathjax.typesetPromise([topicDisplayRef.current]).catch(() => undefined);
}, [selectedTopic?.label]); }, [selectedTopic?.label]);
useEffect(() => { useEffect(() => {
if (!subsectionMenuOptions.length) { if (!topicMenuOptions.length) {
return; return;
} }
if ( if (
selectedTopicRef && selectedTopicRef &&
subsectionMenuOptions.some((option) => option.value === selectedTopicRef.topicKey) topicMenuOptions.some((option) => option.value === selectedTopicRef.topicKey)
) { ) {
return; return;
} }
setTopicKey(subsectionMenuOptions[0].value); setTopicKey(topicMenuOptions[0].value);
}, [selectedTopicRef, setTopicKey, subsectionMenuOptions]); }, [selectedTopicRef, setTopicKey, topicMenuOptions]);
const handleStartSocratic = async () => { const handleStartSocratic = async () => {
if (!selectedTopicRef) { if (!selectedTopicRef) {
...@@ -131,24 +131,24 @@ export default function SocraticSelectionPage() { ...@@ -131,24 +131,24 @@ export default function SocraticSelectionPage() {
</div> </div>
<div className="task-select-controls"> <div className="task-select-controls">
<label className="task-select-label" htmlFor="socratic-subsection-select"> <label className="task-select-label" htmlFor="socratic-topic-select">
{t("subsection")} {t("topic")}
</label> </label>
<select <select
id="socratic-subsection-select" id="socratic-topic-select"
className="task-select" className="task-select"
value={selectedTopicRef?.topicKey || ""} value={selectedTopicRef?.topicKey || ""}
onChange={(event) => setTopicKey(event.target.value)} onChange={(event) => setTopicKey(event.target.value)}
disabled={!subsectionMenuOptions.length} disabled={!topicMenuOptions.length}
> >
{subsectionMenuOptions.length ? ( {topicMenuOptions.length ? (
subsectionMenuOptions.map((option) => ( topicMenuOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
{option.label} {option.label}
</option> </option>
)) ))
) : ( ) : (
<option value="">{t("noSubsectionsAvailable")}</option> <option value="">{t("noTopicsAvailable")}</option>
)} )}
</select> </select>
</div> </div>
......
const collapseWhitespace = (value: string): string => value.replace(/\s+/g, " ").trim(); 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()); collapseWhitespace(String(value).replace(/[-_]+/g, " ").toLowerCase());
export const subsectionKeyToSlug = (value: string): string => export const topicKeyToSlug = (value: string): string =>
normalizeSubsectionKey(value).replace(/\s+/g, "-"); 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