Commit d252c23d authored by Kantz's avatar Kantz
Browse files

darstellung des Sokrats überarbeitet

parent 8e5eb489
......@@ -23,6 +23,7 @@ class SubsectionEntry(BaseModel):
subsection_key: str
label: str
refs: List[List[int]]
summary: str = ""
class TaskFile(BaseModel):
......
......@@ -2,15 +2,18 @@ from __future__ import annotations
import json
import re
import unicodedata
from functools import lru_cache
from pathlib import Path
from typing import Any
from app.deterministic_services import context_store
from app.deterministic_services.vector_store import parse_markdown_with_frontmatter
TASKS_DIR = Path(__file__).resolve().parents[2] / "sources" / "tasks"
SOURCES_DIR = Path(__file__).resolve().parents[2] / "sources"
SUBSECTION_MAP_PATH = TASKS_DIR / "_subsection_map.json"
def _normalize_text(value: str) -> str:
return re.sub(r"\s+", " ", value.strip().lower())
......@@ -20,8 +23,11 @@ def _tokenize(value: str) -> set[str]:
def _normalize_subsection_key(value: str) -> str:
collapsed = re.sub(r"[-_]+", " ", value.strip().lower())
return re.sub(r"\s+", " ", collapsed)
normalized = unicodedata.normalize("NFKD", str(value))
ascii_value = normalized.encode("ascii", "ignore").decode("ascii")
collapsed = re.sub(r"[-_]+", " ", ascii_value.strip().lower())
collapsed = re.sub(r"[^a-z0-9]+", " ", collapsed)
return re.sub(r"\s+", " ", collapsed).strip()
def _parse_subsection_ref(value: str) -> tuple[int, int, int] | None:
......@@ -51,6 +57,47 @@ def load_subsection_map(path: Path = SUBSECTION_MAP_PATH) -> dict[str, tuple[int
return mapped
def _extract_subsection_summary(text: str) -> str:
body = text.replace("\r\n", "\n").strip()
if not body:
return ""
header_match = re.search(r"(?im)^#\s+AI-Generierte Zusammenfassung\s*$", body)
if header_match:
body = body[header_match.end():].lstrip()
child_chunks_match = re.search(r"(?im)^#\s+Child-Chunks\s*$", body)
if child_chunks_match:
body = body[:child_chunks_match.start()].rstrip()
return body.strip()
@lru_cache(maxsize=1)
def load_subsection_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
summaries: dict[str, str] = {}
for folder in (
base_dir / "with_chapters" / "subsections",
base_dir / "child_lvl" / "subsections",
base_dir / "subsection_lvl" / "subsections",
):
if not folder.exists():
continue
for path in sorted(folder.glob("*.md")):
try:
text = path.read_text(encoding="utf-8")
except Exception:
continue
meta, body = parse_markdown_with_frontmatter(text)
title = str(meta.get("title") or meta.get("subsection_title") or meta.get("section_title") or "").strip()
key = _normalize_subsection_key(title)
summary = _extract_subsection_summary(body)
if not key or not summary:
continue
summaries.setdefault(key, summary)
return summaries
def _resolve_task_subsection_refs(
task_file: dict[str, Any],
subsection_map: dict[str, tuple[int, int, int]] | None = None,
......@@ -81,6 +128,7 @@ def _format_subsection_label(value: str) -> str:
def build_subsection_catalog(path: Path = SUBSECTION_MAP_PATH) -> list[dict[str, Any]]:
subsection_map = load_subsection_map(path)
subsection_summaries = load_subsection_summaries()
response: list[dict[str, Any]] = []
for key, ref in sorted(subsection_map.items(), key=lambda item: (item[0], item[1])):
response.append(
......@@ -88,6 +136,7 @@ def build_subsection_catalog(path: Path = SUBSECTION_MAP_PATH) -> list[dict[str,
"subsection_key": key,
"label": _format_subsection_label(key),
"refs": [[int(ref[0]), int(ref[1]), int(ref[2])]],
"summary": subsection_summaries.get(key, ""),
}
)
return response
......
......@@ -18,13 +18,19 @@ from app.deterministic_services import session_store, task_catalog
class TaskCatalogSocraticTest(unittest.TestCase):
def test_build_subsection_catalog_uses_only_map(self) -> None:
def test_build_subsection_catalog_includes_summary(self) -> None:
with patch(
"app.deterministic_services.task_catalog.load_subsection_map",
return_value={
"quadratische gleichungen": (1, 3, 3),
"mengen": (1, 1, 1),
},
), patch(
"app.deterministic_services.task_catalog.load_subsection_summaries",
return_value={
"mengen": "Mengen summary text",
"quadratische gleichungen": "Quadratische summary text",
},
):
catalog = task_catalog.build_subsection_catalog()
......@@ -35,15 +41,60 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"subsection_key": "mengen",
"label": "Mengen",
"refs": [[1, 1, 1]],
"summary": "Mengen summary text",
},
{
"subsection_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen",
"refs": [[1, 3, 3]],
"summary": "Quadratische summary text",
},
],
)
def test_load_subsection_summaries_extracts_body(self) -> None:
temp_dir = Path(__file__).resolve().parent / "_tmp_subsection_summaries"
summary_root = temp_dir / "child_lvl" / "subsections"
summary_root.mkdir(parents=True, exist_ok=True)
md_path = summary_root / "s001-ss001-mengen.md"
md_path.write_text(
"""---
section_index: 1
subsection_index: 1
section_title: "Mengen"
title: "Mengen"
---
# AI-Generierte Zusammenfassung
### **Zusammenfassung: Mengen**
Erste Zeile der Zusammenfassung.
Zweite Zeile.
# Child-Chunks
- `childs/s001-ss001-c001-mengen.md`
""",
encoding="utf-8",
)
try:
summaries = task_catalog.load_subsection_summaries(temp_dir)
finally:
if md_path.exists():
md_path.unlink()
if summary_root.exists():
summary_root.rmdir()
child_lvl_dir = temp_dir / "child_lvl"
if child_lvl_dir.exists():
child_lvl_dir.rmdir()
if temp_dir.exists():
temp_dir.rmdir()
self.assertIn("mengen", summaries)
self.assertTrue(summaries["mengen"].startswith("### **Zusammenfassung: Mengen**"))
self.assertNotIn("Child-Chunks", summaries["mengen"])
def test_select_subsection_by_ids_sets_sheet_fields(self) -> None:
task_files = [
{
......
......@@ -17,6 +17,7 @@ export type SubsectionOption = {
subsection_key: string;
label: string;
refs: [number, number, number][];
summary: string;
};
export type TasksResponse = {
......
import { useEffect, useRef } from "react";
import { useEffect, useMemo, useRef } from "react";
import ReactMarkdown from "react-markdown";
import { t } from "../../i18n";
import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown";
type SocraticPanelProps = {
selectedSubsectionLabel?: string;
selectedSubsectionKey?: string;
selectedSubsectionRefsText?: string;
selectedSubsectionSummary?: string;
onChangeSelection?: () => void;
};
export default function SocraticPanel({
selectedSubsectionLabel,
selectedSubsectionKey,
selectedSubsectionRefsText,
selectedSubsectionSummary,
onChangeSelection,
}: SocraticPanelProps) {
const contentRef = useRef<HTMLDivElement | null>(null);
const summaryMarkdown = selectedSubsectionSummary?.trim() || "";
const renderedSummary = useMemo(
() => escapeAsterisksInsideMath(summaryMarkdown),
[summaryMarkdown]
);
useEffect(() => {
if (!contentRef.current) {
......@@ -25,7 +32,7 @@ export default function SocraticPanel({
return;
}
mathjax.typesetPromise([contentRef.current]).catch(() => undefined);
}, [selectedSubsectionLabel, selectedSubsectionRefsText]);
}, [selectedSubsectionSummary]);
return (
<section className="task-panel">
......@@ -39,12 +46,12 @@ export default function SocraticPanel({
</div>
<div className="task-panel-meta">
<div>{selectedSubsectionLabel || ""}</div>
<div>{selectedSubsectionKey ? `${t("subsectionKey")}: ${selectedSubsectionKey}` : ""}</div>
</div>
<div className="task-panel-content" ref={contentRef}>
{selectedSubsectionLabel || t("noSubsectionSelected")}
{selectedSubsectionRefsText ? <div>{selectedSubsectionRefsText}</div> : null}
<div className="task-panel-content socratic-summary-content" ref={contentRef}>
{renderedSummary ? <ReactMarkdown>{renderedSummary}</ReactMarkdown> : selectedSubsectionLabel || t("noSubsectionSelected")}
</div>
</section>
);
......
......@@ -1205,7 +1205,7 @@ export default function ChatPage() {
<SocraticPanel
selectedSubsectionLabel={selectedSubsection.label}
selectedSubsectionKey={selectedSubsection.subsectionKey}
selectedSubsectionRefsText={selectedSubsection.refsText}
selectedSubsectionSummary={selectedSubsection.summary}
onChangeSelection={handleChangeTaskArea}
/>
) : null}
......
......@@ -36,6 +36,7 @@ export type SelectedSubsection = SelectedSubsectionRef & {
label: string;
refs: [number, number, number][];
refsText: string;
summary: string;
};
export type TaskSelectionState = {
......@@ -185,6 +186,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
label: option.label,
refs,
refsText: formatSubsectionRefs(refs),
summary: option.summary || "",
};
}, [selectedSubsectionRef, subsections]);
......
......@@ -260,6 +260,12 @@ body {
overflow-x: auto;
}
.socratic-summary-content {
max-height: 30vh;
overflow-y: auto;
white-space: normal;
}
.task-panel-error {
padding: 8px 10px;
border-radius: 10px;
......
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