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
0d98206f
Commit
0d98206f
authored
May 08, 2026
by
Kantz
Browse files
Bilder in Übungsaufgaben anzeigbar gemacht
parent
d63d0e46
Changes
10
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/tasks.py
View file @
0d98206f
...
@@ -3,6 +3,7 @@ from __future__ import annotations
...
@@ -3,6 +3,7 @@ from __future__ import annotations
from
typing
import
List
from
typing
import
List
from
fastapi
import
APIRouter
,
HTTPException
from
fastapi
import
APIRouter
,
HTTPException
from
fastapi.responses
import
FileResponse
from
pydantic
import
BaseModel
,
Field
from
pydantic
import
BaseModel
,
Field
import
app.config
as
config
import
app.config
as
config
...
@@ -13,10 +14,16 @@ router = APIRouter()
...
@@ -13,10 +14,16 @@ router = APIRouter()
TASK_ORCHESTRATORS
=
{
"task"
,
"socratic"
}
TASK_ORCHESTRATORS
=
{
"task"
,
"socratic"
}
class
TaskImage
(
BaseModel
):
src
:
str
description
:
str
class
TaskItem
(
BaseModel
):
class
TaskItem
(
BaseModel
):
task_id
:
str
task_id
:
str
statement
:
str
statement
:
str
full_text
:
str
full_text
:
str
images
:
List
[
TaskImage
]
=
Field
(
default_factory
=
list
)
class
SubsectionEntry
(
BaseModel
):
class
SubsectionEntry
(
BaseModel
):
...
@@ -68,6 +75,25 @@ class SelectSubsectionResponse(BaseModel):
...
@@ -68,6 +75,25 @@ class SelectSubsectionResponse(BaseModel):
subsection_key
:
str
subsection_key
:
str
@
router
.
get
(
"/api/tasks/assets/{asset_path:path}"
)
def
get_task_asset
(
asset_path
:
str
)
->
FileResponse
:
requested_path
=
str
(
asset_path
).
strip
().
replace
(
"
\\
"
,
"/"
)
if
not
requested_path
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"asset not found"
)
candidate
=
(
task_catalog
.
TASK_IMAGES_DIR
/
requested_path
).
resolve
()
base_dir
=
task_catalog
.
TASK_IMAGES_DIR
.
resolve
()
try
:
candidate
.
relative_to
(
base_dir
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"asset not found"
)
from
exc
if
not
candidate
.
is_file
():
raise
HTTPException
(
status_code
=
404
,
detail
=
"asset not found"
)
return
FileResponse
(
candidate
)
@
router
.
get
(
"/api/tasks/config"
)
@
router
.
get
(
"/api/tasks/config"
)
def
get_task_config
()
->
dict
[
str
,
object
]:
def
get_task_config
()
->
dict
[
str
,
object
]:
orchestrator
=
config
.
get_orchestrator
()
orchestrator
=
config
.
get_orchestrator
()
...
...
math-tutor/backend/app/deterministic_services/task_catalog.py
View file @
0d98206f
...
@@ -5,14 +5,17 @@ import re
...
@@ -5,14 +5,17 @@ import re
import
unicodedata
import
unicodedata
from
functools
import
lru_cache
from
functools
import
lru_cache
from
pathlib
import
Path
from
pathlib
import
Path
from
urllib.parse
import
quote
from
typing
import
Any
from
typing
import
Any
from
app.deterministic_services
import
context_store
from
app.deterministic_services
import
context_store
from
app.deterministic_services.vector_store
import
parse_markdown_with_frontmatter
from
app.deterministic_services.vector_store
import
parse_markdown_with_frontmatter
TASKS_DIR
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
/
"tasks"
TASKS_DIR
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
/
"tasks"
TASK_IMAGES_DIR
=
TASKS_DIR
/
"images"
SOURCES_DIR
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
SOURCES_DIR
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
SUBSECTION_MAP_PATH
=
TASKS_DIR
/
"_subsection_map.json"
SUBSECTION_MAP_PATH
=
TASKS_DIR
/
"_subsection_map.json"
TASK_ASSET_URL_PREFIX
=
"/api/tasks/assets"
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
())
...
@@ -219,17 +222,49 @@ def _find_task_entry(task_file: dict[str, Any], task_id: str) -> dict[str, Any]
...
@@ -219,17 +222,49 @@ def _find_task_entry(task_file: dict[str, Any], task_id: str) -> dict[str, Any]
return
None
return
None
def
_build_task_payload
(
task_file
:
dict
[
str
,
Any
],
task_entry
:
dict
[
str
,
Any
])
->
tuple
[
str
,
list
[
str
],
str
]:
def
_normalize_task_images
(
task_entry
:
dict
[
str
,
Any
])
->
list
[
dict
[
str
,
str
]]:
images_raw
=
task_entry
.
get
(
"images"
,
[])
if
not
isinstance
(
images_raw
,
list
):
return
[]
normalized_images
:
list
[
dict
[
str
,
str
]]
=
[]
for
item
in
images_raw
:
if
not
isinstance
(
item
,
dict
):
continue
src
=
str
(
item
.
get
(
"src"
,
""
)).
strip
().
replace
(
"
\\
"
,
"/"
)
description
=
str
(
item
.
get
(
"description"
,
""
)).
strip
()
if
not
src
or
not
description
:
continue
if
src
.
startswith
(
"/"
)
or
Path
(
src
).
is_absolute
():
continue
normalized_path
=
Path
(
src
)
if
any
(
part
in
{
""
,
"."
,
".."
}
for
part
in
normalized_path
.
parts
):
continue
encoded_src
=
"/"
.
join
(
quote
(
part
)
for
part
in
normalized_path
.
parts
)
normalized_images
.
append
(
{
"src"
:
f
"
{
TASK_ASSET_URL_PREFIX
}
/
{
encoded_src
}
"
,
"description"
:
description
,
}
)
return
normalized_images
def
_build_task_payload
(
task_file
:
dict
[
str
,
Any
],
task_entry
:
dict
[
str
,
Any
]
)
->
tuple
[
str
,
list
[
str
],
str
,
list
[
dict
[
str
,
str
]]]:
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
()
statement
=
str
(
task_entry
.
get
(
"statement"
,
""
)).
strip
()
statement
=
str
(
task_entry
.
get
(
"statement"
,
""
)).
strip
()
hints_raw
=
task_entry
.
get
(
"hints"
,
[])
hints_raw
=
task_entry
.
get
(
"hints"
,
[])
hints
=
[
str
(
item
).
strip
()
for
item
in
hints_raw
if
str
(
item
).
strip
()]
if
isinstance
(
hints_raw
,
list
)
else
[]
hints
=
[
str
(
item
).
strip
()
for
item
in
hints_raw
if
str
(
item
).
strip
()]
if
isinstance
(
hints_raw
,
list
)
else
[]
solution
=
str
(
task_entry
.
get
(
"solution"
,
""
)).
strip
()
solution
=
str
(
task_entry
.
get
(
"solution"
,
""
)).
strip
()
images
=
_normalize_task_images
(
task_entry
)
task_parts
=
[
part
for
part
in
[
title
,
intro
,
statement
]
if
part
]
task_parts
=
[
part
for
part
in
[
title
,
intro
,
statement
]
if
part
]
task_parts
.
extend
([
f
"Bildbeschreibung:
{
image
[
'description'
]
}
"
for
image
in
images
])
task_text
=
"
\n
"
.
join
(
task_parts
)
task_text
=
"
\n
"
.
join
(
task_parts
)
return
task_text
,
hints
,
solution
return
task_text
,
hints
,
solution
,
images
def
set_selected_task
(
def
set_selected_task
(
...
@@ -237,7 +272,7 @@ def set_selected_task(
...
@@ -237,7 +272,7 @@ def set_selected_task(
task_file
:
dict
[
str
,
Any
],
task_file
:
dict
[
str
,
Any
],
task_entry
:
dict
[
str
,
Any
],
task_entry
:
dict
[
str
,
Any
],
)
->
None
:
)
->
None
:
task_text
,
hints
,
solution
=
_build_task_payload
(
task_file
,
task_entry
)
task_text
,
hints
,
solution
,
_
=
_build_task_payload
(
task_file
,
task_entry
)
store_new
=
context_store
.
context_store_new
store_new
=
context_store
.
context_store_new
store_new
.
set_task
(
sheet
,
task_text
)
store_new
.
set_task
(
sheet
,
task_text
)
store_new
.
set_hints
(
sheet
,
hints
)
store_new
.
set_hints
(
sheet
,
hints
)
...
@@ -471,12 +506,14 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
...
@@ -471,12 +506,14 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
continue
continue
task_id
=
str
(
item
.
get
(
"id"
,
""
)).
zfill
(
2
)
task_id
=
str
(
item
.
get
(
"id"
,
""
)).
zfill
(
2
)
statement
=
str
(
item
.
get
(
"statement"
,
""
)).
strip
()
statement
=
str
(
item
.
get
(
"statement"
,
""
)).
strip
()
images
=
_normalize_task_images
(
item
)
full_text_parts
=
[
part
for
part
in
[
title
,
intro
,
statement
]
if
part
]
full_text_parts
=
[
part
for
part
in
[
title
,
intro
,
statement
]
if
part
]
tasks
.
append
(
tasks
.
append
(
{
{
"task_id"
:
task_id
,
"task_id"
:
task_id
,
"statement"
:
statement
,
"statement"
:
statement
,
"full_text"
:
"
\n
"
.
join
(
full_text_parts
),
"full_text"
:
"
\n
"
.
join
(
full_text_parts
),
"images"
:
images
,
}
}
)
)
tasks
.
sort
(
key
=
lambda
item
:
item
[
"task_id"
])
tasks
.
sort
(
key
=
lambda
item
:
item
[
"task_id"
])
...
...
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
0d98206f
...
@@ -2,6 +2,8 @@ from __future__ import annotations
...
@@ -2,6 +2,8 @@ from __future__ import annotations
import
json
import
json
import
os
import
os
import
shutil
import
tempfile
import
unittest
import
unittest
from
pathlib
import
Path
from
pathlib
import
Path
from
unittest.mock
import
patch
from
unittest.mock
import
patch
...
@@ -18,6 +20,79 @@ from app.deterministic_services import session_store, socratic_oranisator, task_
...
@@ -18,6 +20,79 @@ from app.deterministic_services import session_store, socratic_oranisator, task_
class
TaskCatalogSocraticTest
(
unittest
.
TestCase
):
class
TaskCatalogSocraticTest
(
unittest
.
TestCase
):
def
test_build_task_payload_appends_image_descriptions
(
self
)
->
None
:
task_file
=
{
"title"
:
"Grundlagen von Funktionen"
,
"intro"
:
"Ordnen Sie zu."
,
}
task_entry
=
{
"id"
:
"01"
,
"statement"
:
""
,
"images"
:
[
{
"src"
:
"grundlagen-von-funktionen/01.svg"
,
"description"
:
"Pfeildiagramm mit einer fehlenden Abbildung."
,
}
],
"hints"
:
[
"Prüfe jedes Element."
],
"solution"
:
"Keine Funktion."
,
}
task_text
,
hints
,
solution
,
images
=
task_catalog
.
_build_task_payload
(
task_file
,
task_entry
)
self
.
assertIn
(
"Grundlagen von Funktionen"
,
task_text
)
self
.
assertIn
(
"Ordnen Sie zu."
,
task_text
)
self
.
assertIn
(
"Bildbeschreibung: Pfeildiagramm mit einer fehlenden Abbildung."
,
task_text
)
self
.
assertEqual
(
hints
,
[
"Prüfe jedes Element."
])
self
.
assertEqual
(
solution
,
"Keine Funktion."
)
self
.
assertEqual
(
images
,
[
{
"src"
:
"/api/tasks/assets/grundlagen-von-funktionen/01.svg"
,
"description"
:
"Pfeildiagramm mit einer fehlenden Abbildung."
,
}
],
)
def
test_build_task_catalog_includes_normalized_images
(
self
)
->
None
:
task_files
=
[
{
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"subsections"
:
[
"quadratische_gleichungen"
],
"tasks"
:
[
{
"id"
:
"01"
,
"statement"
:
""
,
"images"
:
[
{
"src"
:
"analysis/01.png"
,
"description"
:
"Graph einer Parabel."
,
},
{
"src"
:
"../invalid.png"
,
"description"
:
"Invalid"
,
},
],
}
],
}
]
catalog
=
task_catalog
.
build_task_catalog
(
task_files
)
self
.
assertEqual
(
catalog
[
0
][
"tasks"
][
0
][
"images"
],
[
{
"src"
:
"/api/tasks/assets/analysis/01.png"
,
"description"
:
"Graph einer Parabel."
,
}
],
)
def
test_socratic_build_subsection_catalog_returns_response_shape
(
self
)
->
None
:
def
test_socratic_build_subsection_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"
,
...
@@ -212,6 +287,36 @@ class TaskApiSocraticTest(unittest.TestCase):
...
@@ -212,6 +287,36 @@ class TaskApiSocraticTest(unittest.TestCase):
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"subsections"
][
0
][
"subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
body
[
"subsections"
][
0
][
"subsection_key"
],
"quadratische gleichungen"
)
def
test_task_asset_endpoint_serves_files_from_task_image_dir
(
self
)
->
None
:
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
"task-assets-"
))
image_root
=
temp_dir
/
"images"
image_root
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
image_path
=
image_root
/
"functions"
/
"01.svg"
image_path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
image_path
.
write_text
(
"<svg xmlns=
\"
http://www.w3.org/2000/svg
\"
></svg>"
,
encoding
=
"utf-8"
)
try
:
with
patch
.
object
(
task_catalog
,
"TASK_IMAGES_DIR"
,
image_root
):
response
=
self
.
client
.
get
(
"/api/tasks/assets/functions/01.svg"
)
finally
:
shutil
.
rmtree
(
temp_dir
,
ignore_errors
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertIn
(
"<svg"
,
response
.
text
)
def
test_task_asset_endpoint_rejects_path_traversal
(
self
)
->
None
:
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
"task-assets-"
))
image_root
=
temp_dir
/
"images"
image_root
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
try
:
with
patch
.
object
(
task_catalog
,
"TASK_IMAGES_DIR"
,
image_root
):
response
=
self
.
client
.
get
(
"/api/tasks/assets/%2E%2E/secret.txt"
)
finally
:
shutil
.
rmtree
(
temp_dir
,
ignore_errors
=
True
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_list_socratic_subsections_returns_catalog
(
self
)
->
None
:
def
test_list_socratic_subsections_returns_catalog
(
self
)
->
None
:
subsections
=
[
subsections
=
[
{
{
...
@@ -293,7 +398,7 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
...
@@ -293,7 +398,7 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
"app.api.chat.retrieval_store.retrieve_for_subsections"
,
"app.api.chat.retrieval_store.retrieve_for_subsections"
,
return_value
=
[],
return_value
=
[],
)
as
retrieve_mock
,
patch
(
)
as
retrieve_mock
,
patch
(
"app.api.chat.socratic_
LLM.generate_dialog
"
,
"app.api.chat.socratic_
oranisator.get_initial_message
"
,
return_value
=
"Was sind die Themen dieses Abschnitts?"
,
return_value
=
"Was sind die Themen dieses Abschnitts?"
,
):
):
response
=
self
.
client
.
post
(
response
=
self
.
client
.
post
(
...
...
math-tutor/frontend/src/api/taskApi.ts
View file @
0d98206f
export
type
TaskItem
=
{
export
type
TaskImage
=
{
src
:
string
;
description
:
string
;
};
export
type
TaskItem
=
{
task_id
:
string
;
task_id
:
string
;
statement
:
string
;
statement
:
string
;
full_text
:
string
;
full_text
:
string
;
images
:
TaskImage
[];
};
};
export
type
TaskFile
=
{
export
type
TaskFile
=
{
...
...
math-tutor/frontend/src/components/Task/TaskContent.tsx
0 → 100644
View file @
0d98206f
import
type
{
RefObject
}
from
"
react
"
;
import
type
{
TaskImage
}
from
"
../../api/taskApi
"
;
type
TaskContentProps
=
{
text
:
string
;
images
?:
TaskImage
[];
textRef
?:
RefObject
<
HTMLDivElement
|
null
>
;
};
export
default
function
TaskContent
({
text
,
images
=
[],
textRef
}:
TaskContentProps
)
{
return
(
<
div
className
=
"task-content-stack"
>
<
div
className
=
"task-panel-content"
ref
=
{
textRef
}
>
{
text
}
</
div
>
{
images
.
length
?
(
<
div
className
=
"task-image-list"
>
{
images
.
map
((
image
)
=>
(
<
div
className
=
"task-image-wrap"
key
=
{
`
${
image
.
src
}
|
${
image
.
description
}
`
}
>
<
img
className
=
"task-image"
src
=
{
image
.
src
}
alt
=
{
image
.
description
}
/>
</
div
>
))
}
</
div
>
)
:
null
}
</
div
>
);
}
math-tutor/frontend/src/components/Task/TaskPanel.tsx
View file @
0d98206f
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
type
{
TaskImage
}
from
"
../../api/taskApi
"
;
import
{
t
}
from
"
../../i18n
"
;
import
{
t
}
from
"
../../i18n
"
;
import
TaskContent
from
"
./TaskContent
"
;
type
SelectOption
=
{
type
SelectOption
=
{
value
:
string
;
value
:
string
;
...
@@ -14,6 +16,7 @@ type TaskPanelProps = {
...
@@ -14,6 +16,7 @@ type TaskPanelProps = {
selectedTaskId
?:
string
;
selectedTaskId
?:
string
;
taskOptions
?:
SelectOption
[];
taskOptions
?:
SelectOption
[];
selectedTaskText
:
string
;
selectedTaskText
:
string
;
selectedTaskImages
?:
TaskImage
[];
tasksError
?:
string
|
null
;
tasksError
?:
string
|
null
;
onTaskFileChange
?:
(
fileId
:
string
)
=>
void
;
onTaskFileChange
?:
(
fileId
:
string
)
=>
void
;
onTaskChange
?:
(
taskId
:
string
)
=>
void
;
onTaskChange
?:
(
taskId
:
string
)
=>
void
;
...
@@ -32,6 +35,7 @@ export default function TaskPanel({
...
@@ -32,6 +35,7 @@ export default function TaskPanel({
selectedTaskId
=
""
,
selectedTaskId
=
""
,
taskOptions
=
[],
taskOptions
=
[],
selectedTaskText
,
selectedTaskText
,
selectedTaskImages
=
[],
tasksError
,
tasksError
,
onTaskFileChange
,
onTaskFileChange
,
onTaskChange
,
onTaskChange
,
...
@@ -111,9 +115,11 @@ export default function TaskPanel({
...
@@ -111,9 +115,11 @@ export default function TaskPanel({
</
div
>
</
div
>
)
:
null
}
)
:
null
}
<
div
className
=
"task-panel-content"
ref
=
{
taskDisplayRef
}
>
<
TaskContent
{
selectedTaskText
||
t
(
"
noTaskSelected
"
)
}
text
=
{
selectedTaskText
||
t
(
"
noTaskSelected
"
)
}
</
div
>
images
=
{
selectedTaskImages
}
textRef
=
{
taskDisplayRef
}
/>
{
readOnly
?
(
{
readOnly
?
(
<
div
className
=
"task-panel-nav"
>
<
div
className
=
"task-panel-nav"
>
...
...
math-tutor/frontend/src/pages/ChatPage.tsx
View file @
0d98206f
...
@@ -1265,6 +1265,7 @@ export default function ChatPage() {
...
@@ -1265,6 +1265,7 @@ export default function ChatPage() {
<
TaskPanel
<
TaskPanel
readOnly
readOnly
selectedTaskText
=
{
selectedTask
.
fullText
}
selectedTaskText
=
{
selectedTask
.
fullText
}
selectedTaskImages
=
{
selectedTask
.
images
}
selectedFileLabel
=
{
selectedTask
.
title
||
selectedTask
.
fileId
}
selectedFileLabel
=
{
selectedTask
.
title
||
selectedTask
.
fileId
}
selectedTaskId
=
{
selectedTask
.
taskId
}
selectedTaskId
=
{
selectedTask
.
taskId
}
onChangeTaskArea
=
{
handleChangeTaskArea
}
onChangeTaskArea
=
{
handleChangeTaskArea
}
...
...
math-tutor/frontend/src/pages/TaskSelectionPage.tsx
View file @
0d98206f
...
@@ -2,6 +2,7 @@ import { useEffect, useRef } from "react";
...
@@ -2,6 +2,7 @@ import { useEffect, 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
OrchestratorSelect
from
"
../components/Orchestrator/OrchestratorSelect
"
;
import
OrchestratorSelect
from
"
../components/Orchestrator/OrchestratorSelect
"
;
import
TaskContent
from
"
../components/Task/TaskContent
"
;
import
{
selectTask
}
from
"
../api/taskApi
"
;
import
{
selectTask
}
from
"
../api/taskApi
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
useTutorSession
}
from
"
../state/tutorSession
"
;
import
{
useTutorSession
}
from
"
../state/tutorSession
"
;
...
@@ -170,9 +171,11 @@ export default function TaskSelectionPage() {
...
@@ -170,9 +171,11 @@ export default function TaskSelectionPage() {
</
select
>
</
select
>
</
div
>
</
div
>
<
div
className
=
"task-panel-content"
ref
=
{
taskDisplayRef
}
>
<
TaskContent
{
selectedTask
?.
fullText
||
t
(
"
noTaskSelected
"
)
}
text
=
{
selectedTask
?.
fullText
||
t
(
"
noTaskSelected
"
)
}
</
div
>
images
=
{
selectedTask
?.
images
||
[]
}
textRef
=
{
taskDisplayRef
}
/>
{
tasksError
?
<
div
className
=
"task-panel-error"
>
{
tasksError
}
</
div
>
:
null
}
{
tasksError
?
<
div
className
=
"task-panel-error"
>
{
tasksError
}
</
div
>
:
null
}
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
0d98206f
...
@@ -20,6 +20,7 @@ import {
...
@@ -20,6 +20,7 @@ import {
type
SelectedSubsectionRef
,
type
SelectedSubsectionRef
,
type
SelectedTaskRef
,
type
SelectedTaskRef
,
type
SubsectionOption
,
type
SubsectionOption
,
type
TaskImage
,
type
TaskFile
,
type
TaskFile
,
}
from
"
../api/taskApi
"
;
}
from
"
../api/taskApi
"
;
...
@@ -31,6 +32,7 @@ export type SelectOption = {
...
@@ -31,6 +32,7 @@ export type SelectOption = {
export
type
SelectedTask
=
SelectedTaskRef
&
{
export
type
SelectedTask
=
SelectedTaskRef
&
{
title
:
string
;
title
:
string
;
fullText
:
string
;
fullText
:
string
;
images
:
TaskImage
[];
};
};
export
type
SelectedSubsection
=
SelectedSubsectionRef
&
{
export
type
SelectedSubsection
=
SelectedSubsectionRef
&
{
...
@@ -131,6 +133,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -131,6 +133,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
taskId
:
task
.
task_id
,
taskId
:
task
.
task_id
,
title
:
file
.
title
,
title
:
file
.
title
,
fullText
:
task
.
full_text
,
fullText
:
task
.
full_text
,
images
:
task
.
images
||
[],
};
};
},
[
selectedTaskRef
,
taskFiles
]);
},
[
selectedTaskRef
,
taskFiles
]);
...
...
math-tutor/frontend/src/styles/theme.css
View file @
0d98206f
...
@@ -260,6 +260,34 @@ body {
...
@@ -260,6 +260,34 @@ body {
overflow-x
:
auto
;
overflow-x
:
auto
;
}
}
.task-content-stack
{
display
:
flex
;
flex-direction
:
column
;
gap
:
12px
;
}
.task-image-list
{
display
:
flex
;
flex-direction
:
column
;
gap
:
12px
;
}
.task-image-wrap
{
background
:
#fff
;
border
:
1px
solid
#e2ded5
;
border-radius
:
12px
;
padding
:
10px
;
}
.task-image
{
display
:
block
;
width
:
100%
;
max-width
:
100%
;
height
:
auto
;
border-radius
:
8px
;
object-fit
:
contain
;
}
.socratic-summary-content
{
.socratic-summary-content
{
max-height
:
30vh
;
max-height
:
30vh
;
overflow-y
:
auto
;
overflow-y
:
auto
;
...
...
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