Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
math_tutor_dev
public_math_tutor
Commits
d252c23d
Commit
d252c23d
authored
Apr 14, 2026
by
Kantz
Browse files
darstellung des Sokrats überarbeitet
parent
8e5eb489
Changes
8
Show whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/tasks.py
View file @
d252c23d
...
...
@@ -23,6 +23,7 @@ class SubsectionEntry(BaseModel):
subsection_key
:
str
label
:
str
refs
:
List
[
List
[
int
]]
summary
:
str
=
""
class
TaskFile
(
BaseModel
):
...
...
math-tutor/backend/app/deterministic_services/task_catalog.py
View file @
d252c23d
...
...
@@ -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
...
...
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
d252c23d
...
...
@@ -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
=
[
{
...
...
math-tutor/frontend/src/api/taskApi.ts
View file @
d252c23d
...
...
@@ -17,6 +17,7 @@ export type SubsectionOption = {
subsection_key
:
string
;
label
:
string
;
refs
:
[
number
,
number
,
number
][];
summary
:
string
;
};
export
type
TasksResponse
=
{
...
...
math-tutor/frontend/src/components/Task/SocraticPanel.tsx
View file @
d252c23d
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
;
selectedSubsection
RefsText
?:
string
;
selectedSubsection
Summary
?:
string
;
onChangeSelection
?:
()
=>
void
;
};
export
default
function
SocraticPanel
({
selectedSubsectionLabel
,
selectedSubsectionKey
,
selectedSubsection
RefsText
,
selectedSubsection
Summary
,
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
);
},
[
selectedSubsection
Label
,
selectedSubsectionRefsText
]);
},
[
selectedSubsection
Summary
]);
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
>
);
...
...
math-tutor/frontend/src/pages/ChatPage.tsx
View file @
d252c23d
...
...
@@ -1205,7 +1205,7 @@ export default function ChatPage() {
<
SocraticPanel
selectedSubsectionLabel
=
{
selectedSubsection
.
label
}
selectedSubsectionKey
=
{
selectedSubsection
.
subsectionKey
}
selectedSubsection
RefsText
=
{
selectedSubsection
.
refsText
}
selectedSubsection
Summary
=
{
selectedSubsection
.
summary
}
onChangeSelection
=
{
handleChangeTaskArea
}
/>
)
:
null
}
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
d252c23d
...
...
@@ -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
]);
...
...
math-tutor/frontend/src/styles/theme.css
View file @
d252c23d
...
...
@@ -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
;
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment