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
5ce4995d
Commit
5ce4995d
authored
May 29, 2026
by
Kantz
Browse files
ladenzeiten von Tasks verschoben und verkürzt
parent
307b3f09
Changes
5
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/tasks.py
View file @
5ce4995d
...
@@ -21,16 +21,6 @@ class TaskImage(BaseModel):
...
@@ -21,16 +21,6 @@ class TaskImage(BaseModel):
class
TaskItem
(
BaseModel
):
class
TaskItem
(
BaseModel
):
task_id
:
str
task_id
:
str
statement
:
str
full_text
:
str
images
:
List
[
TaskImage
]
=
Field
(
default_factory
=
list
)
class
TopicEntry
(
BaseModel
):
topic_key
:
str
label
:
str
refs
:
List
[
List
[
int
]]
summary
:
str
=
""
class
SubsectionEntry
(
BaseModel
):
class
SubsectionEntry
(
BaseModel
):
...
@@ -44,17 +34,21 @@ class SubsectionEntry(BaseModel):
...
@@ -44,17 +34,21 @@ class SubsectionEntry(BaseModel):
class
TaskFile
(
BaseModel
):
class
TaskFile
(
BaseModel
):
file_id
:
str
file_id
:
str
title
:
str
title
:
str
intro
:
str
tasks
:
List
[
TaskItem
]
tasks
:
List
[
TaskItem
]
topics
:
List
[
str
]
=
Field
(
default_factory
=
list
)
topic_options
:
List
[
TopicEntry
]
=
Field
(
default_factory
=
list
)
class
TasksResponse
(
BaseModel
):
class
TasksResponse
(
BaseModel
):
orchestrator
:
str
orchestrator
:
str
enabled
:
bool
enabled
:
bool
task_files
:
List
[
TaskFile
]
task_files
:
List
[
TaskFile
]
topics
:
List
[
TopicEntry
]
=
Field
(
default_factory
=
list
)
class
TaskDetailsResponse
(
BaseModel
):
file_id
:
str
task_id
:
str
title
:
str
full_text
:
str
images
:
List
[
TaskImage
]
=
Field
(
default_factory
=
list
)
class
SocraticResponse
(
BaseModel
):
class
SocraticResponse
(
BaseModel
):
...
@@ -112,16 +106,22 @@ def get_task_config() -> dict[str, object]:
...
@@ -112,16 +106,22 @@ def get_task_config() -> dict[str, object]:
@
router
.
get
(
"/api/tasks"
,
response_model
=
TasksResponse
)
@
router
.
get
(
"/api/tasks"
,
response_model
=
TasksResponse
)
def
list_tasks
()
->
TasksResponse
:
def
list_tasks
()
->
TasksResponse
:
orchestrator
=
config
.
get_orchestrator
()
orchestrator
=
config
.
get_orchestrator
()
task_files
=
task_catalog
.
build_task_catalog
()
task_files
=
list
(
task_catalog
.
build_cached_task_metadata_catalog
())
topics
=
task_catalog
.
build_topic_catalog
()
return
TasksResponse
(
return
TasksResponse
(
orchestrator
=
orchestrator
,
orchestrator
=
orchestrator
,
enabled
=
orchestrator
in
TASK_ORCHESTRATORS
,
enabled
=
orchestrator
in
TASK_ORCHESTRATORS
,
task_files
=
task_files
,
task_files
=
task_files
,
topics
=
topics
,
)
)
@
router
.
get
(
"/api/tasks/{file_id}/{task_id}"
,
response_model
=
TaskDetailsResponse
)
def
get_task_details
(
file_id
:
str
,
task_id
:
str
)
->
TaskDetailsResponse
:
payload
=
task_catalog
.
find_task_details
(
file_id
=
file_id
,
task_id
=
task_id
)
if
payload
is
None
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"task not found"
)
return
TaskDetailsResponse
(
**
payload
)
@
router
.
get
(
"/api/tasks/socratic-subsections"
,
response_model
=
SocraticResponse
)
@
router
.
get
(
"/api/tasks/socratic-subsections"
,
response_model
=
SocraticResponse
)
def
list_socratic_subsections
()
->
SocraticResponse
:
def
list_socratic_subsections
()
->
SocraticResponse
:
orchestrator
=
config
.
get_orchestrator
()
orchestrator
=
config
.
get_orchestrator
()
...
...
math-tutor/backend/app/deterministic_services/task_catalog.py
View file @
5ce4995d
...
@@ -2,6 +2,7 @@ from __future__ import annotations
...
@@ -2,6 +2,7 @@ from __future__ import annotations
import
re
import
re
import
unicodedata
import
unicodedata
from
copy
import
deepcopy
from
functools
import
lru_cache
from
functools
import
lru_cache
from
pathlib
import
Path
from
pathlib
import
Path
from
typing
import
Any
from
typing
import
Any
...
@@ -542,6 +543,11 @@ def load_task_files(tasks_dir: Path = TASKS_DIR) -> list[dict[str, Any]]:
...
@@ -542,6 +543,11 @@ def load_task_files(tasks_dir: Path = TASKS_DIR) -> list[dict[str, Any]]:
return
loaded
return
loaded
@
lru_cache
(
maxsize
=
1
)
def
load_cached_task_files
()
->
list
[
dict
[
str
,
Any
]]:
return
load_task_files
()
def
_find_task_file
(
task_files
:
list
[
dict
[
str
,
Any
]],
file_id
:
str
)
->
dict
[
str
,
Any
]
|
None
:
def
_find_task_file
(
task_files
:
list
[
dict
[
str
,
Any
]],
file_id
:
str
)
->
dict
[
str
,
Any
]
|
None
:
for
task_file
in
task_files
:
for
task_file
in
task_files
:
if
str
(
task_file
.
get
(
"_file_id"
,
""
))
==
file_id
:
if
str
(
task_file
.
get
(
"_file_id"
,
""
))
==
file_id
:
...
@@ -558,6 +564,33 @@ def _find_task_entry(task_file: dict[str, Any], task_id: str) -> dict[str, Any]
...
@@ -558,6 +564,33 @@ def _find_task_entry(task_file: dict[str, Any], task_id: str) -> dict[str, Any]
return
None
return
None
def
find_task_details
(
file_id
:
str
,
task_id
:
str
,
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
)
->
dict
[
str
,
Any
]
|
None
:
catalog
=
task_files
if
task_files
is
not
None
else
load_cached_task_files
()
task_file
=
_find_task_file
(
catalog
,
file_id
)
if
not
task_file
:
return
None
task_entry
=
_find_task_entry
(
task_file
,
task_id
)
if
not
task_entry
:
return
None
title
=
str
(
task_file
.
get
(
"title"
,
""
)).
strip
()
intro
=
str
(
task_file
.
get
(
"intro"
,
""
)).
strip
()
statement
=
str
(
task_entry
.
get
(
"statement"
,
""
)).
strip
()
images
=
_normalize_task_images
(
task_entry
)
full_text_parts
=
[
part
for
part
in
[
title
,
intro
,
statement
]
if
part
]
return
{
"file_id"
:
str
(
task_file
.
get
(
"_file_id"
,
""
)),
"task_id"
:
str
(
task_entry
.
get
(
"id"
,
""
)).
zfill
(
2
),
"title"
:
title
,
"full_text"
:
"
\n
"
.
join
(
full_text_parts
),
"images"
:
images
,
}
def
_normalize_task_images
(
task_entry
:
dict
[
str
,
Any
])
->
list
[
dict
[
str
,
str
]]:
def
_normalize_task_images
(
task_entry
:
dict
[
str
,
Any
])
->
list
[
dict
[
str
,
str
]]:
images_raw
=
task_entry
.
get
(
"images"
,
[])
images_raw
=
task_entry
.
get
(
"images"
,
[])
if
not
isinstance
(
images_raw
,
list
):
if
not
isinstance
(
images_raw
,
list
):
...
@@ -644,7 +677,7 @@ def select_task_by_ids(
...
@@ -644,7 +677,7 @@ def select_task_by_ids(
task_id
:
str
,
task_id
:
str
,
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
)
->
bool
:
)
->
bool
:
catalog
=
task_files
if
task_files
is
not
None
else
load_task_files
()
catalog
=
task_files
if
task_files
is
not
None
else
load_
cached_
task_files
()
task_file
=
_find_task_file
(
catalog
,
file_id
)
task_file
=
_find_task_file
(
catalog
,
file_id
)
if
not
task_file
:
if
not
task_file
:
return
False
return
False
...
@@ -661,7 +694,7 @@ def select_subsection_by_ids(
...
@@ -661,7 +694,7 @@ def select_subsection_by_ids(
subsection_key
:
str
,
subsection_key
:
str
,
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
)
->
bool
:
)
->
bool
:
catalog
=
task_files
if
task_files
is
not
None
else
load_task_files
()
catalog
=
task_files
if
task_files
is
not
None
else
load_
cached_
task_files
()
task_file
=
_find_task_file
(
catalog
,
file_id
)
task_file
=
_find_task_file
(
catalog
,
file_id
)
if
not
task_file
:
if
not
task_file
:
return
False
return
False
...
@@ -776,7 +809,7 @@ def select_task_for_context(
...
@@ -776,7 +809,7 @@ def select_task_for_context(
history
:
list
[
dict
],
history
:
list
[
dict
],
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
)
->
tuple
[
dict
[
str
,
Any
],
dict
[
str
,
Any
]]
|
None
:
)
->
tuple
[
dict
[
str
,
Any
],
dict
[
str
,
Any
]]
|
None
:
catalog
=
task_files
if
task_files
is
not
None
else
load_task_files
()
catalog
=
task_files
if
task_files
is
not
None
else
load_
cached_
task_files
()
if
not
catalog
:
if
not
catalog
:
return
None
return
None
...
@@ -833,7 +866,7 @@ def select_task_for_context(
...
@@ -833,7 +866,7 @@ def select_task_for_context(
def
build_task_catalog
(
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
)
->
list
[
dict
[
str
,
Any
]]:
def
build_task_catalog
(
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
)
->
list
[
dict
[
str
,
Any
]]:
catalog
=
task_files
if
task_files
is
not
None
else
load_task_files
()
catalog
=
task_files
if
task_files
is
not
None
else
load_
cached_
task_files
()
catalog
=
sorted
(
catalog
=
sorted
(
catalog
,
catalog
,
key
=
lambda
item
:
(
key
=
lambda
item
:
(
...
@@ -880,3 +913,35 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
...
@@ -880,3 +913,35 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
}
}
)
)
return
response
return
response
def
build_task_metadata_catalog
(
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
)
->
list
[
dict
[
str
,
Any
]]:
catalog
=
task_files
if
task_files
is
not
None
else
load_cached_task_files
()
catalog
=
sorted
(
catalog
,
key
=
lambda
item
:
(
str
(
item
.
get
(
"title"
,
""
)).
strip
().
lower
(),
str
(
item
.
get
(
"_file_id"
,
""
)).
strip
().
lower
(),
),
)
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
task_file
in
catalog
:
tasks
:
list
[
dict
[
str
,
str
]]
=
[]
for
item
in
task_file
.
get
(
"tasks"
,
[]):
if
not
isinstance
(
item
,
dict
):
continue
tasks
.
append
({
"task_id"
:
str
(
item
.
get
(
"id"
,
""
)).
zfill
(
2
)})
tasks
.
sort
(
key
=
lambda
item
:
item
[
"task_id"
])
response
.
append
(
{
"file_id"
:
str
(
task_file
.
get
(
"_file_id"
,
""
)),
"title"
:
str
(
task_file
.
get
(
"title"
,
""
)).
strip
(),
"tasks"
:
tasks
,
}
)
return
response
@
lru_cache
(
maxsize
=
1
)
def
build_cached_task_metadata_catalog
()
->
tuple
[
dict
[
str
,
Any
],
...]:
return
tuple
(
deepcopy
(
build_task_metadata_catalog
()))
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
5ce4995d
...
@@ -20,6 +20,10 @@ from app.deterministic_services import session_store, socratic_oranisator, task_
...
@@ -20,6 +20,10 @@ from app.deterministic_services import session_store, socratic_oranisator, task_
class
TaskCatalogSocraticTest
(
unittest
.
TestCase
):
class
TaskCatalogSocraticTest
(
unittest
.
TestCase
):
def
tearDown
(
self
)
->
None
:
task_catalog
.
build_cached_task_metadata_catalog
.
cache_clear
()
task_catalog
.
load_cached_task_files
.
cache_clear
()
def
test_build_task_payload_appends_image_descriptions
(
self
)
->
None
:
def
test_build_task_payload_appends_image_descriptions
(
self
)
->
None
:
task_file
=
{
task_file
=
{
"title"
:
"Grundlagen von Funktionen"
,
"title"
:
"Grundlagen von Funktionen"
,
...
@@ -95,6 +99,50 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -95,6 +99,50 @@ class TaskCatalogSocraticTest(unittest.TestCase):
)
)
self
.
assertEqual
(
catalog
[
0
][
"topics"
],
[
"quadratische_gleichungen"
])
self
.
assertEqual
(
catalog
[
0
][
"topics"
],
[
"quadratische_gleichungen"
])
def
test_build_task_metadata_catalog_only_returns_titles_and_task_ids
(
self
)
->
None
:
task_files
=
[
{
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"subsections"
:
[
"quadratische_gleichungen"
],
"topic_refs"
:
[],
"tasks"
:
[
{
"id"
:
"01"
,
"statement"
:
"Bestimme f(x)."
,
"images"
:
[{
"src"
:
"analysis/01.png"
,
"description"
:
"Graph"
}],
}
],
}
]
catalog
=
task_catalog
.
build_task_metadata_catalog
(
task_files
)
self
.
assertEqual
(
catalog
,
[
{
"file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"tasks"
:
[{
"task_id"
:
"01"
}],
}
],
)
def
test_build_cached_task_metadata_catalog_reuses_cache
(
self
)
->
None
:
task_catalog
.
build_cached_task_metadata_catalog
.
cache_clear
()
with
patch
(
"app.deterministic_services.task_catalog.build_task_metadata_catalog"
,
return_value
=
[{
"file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"tasks"
:
[{
"task_id"
:
"01"
}]}],
)
as
build_mock
:
first
=
task_catalog
.
build_cached_task_metadata_catalog
()
second
=
task_catalog
.
build_cached_task_metadata_catalog
()
self
.
assertEqual
(
first
,
second
)
build_mock
.
assert_called_once
()
def
test_normalize_yaml_task_entry_excludes_images_from_hints
(
self
)
->
None
:
def
test_normalize_yaml_task_entry_excludes_images_from_hints
(
self
)
->
None
:
task_entry
=
{
task_entry
=
{
"aufgabe"
:
[
"aufgabe"
:
[
...
@@ -376,39 +424,47 @@ class TaskApiSocraticTest(unittest.TestCase):
...
@@ -376,39 +424,47 @@ class TaskApiSocraticTest(unittest.TestCase):
app
.
include_router
(
tasks
.
router
)
app
.
include_router
(
tasks
.
router
)
self
.
client
=
TestClient
(
app
)
self
.
client
=
TestClient
(
app
)
def
test_list_tasks_
includes_topic_options
(
self
)
->
None
:
def
test_list_tasks_
returns_lightweight_catalog
(
self
)
->
None
:
payload
=
[
payload
=
[
{
{
"file_id"
:
"analysis_1"
,
"file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"tasks"
:
[{
"task_id"
:
"01"
}],
"tasks"
:
[],
"topics"
:
[
"quadratische gleichungen"
],
"topic_options"
:
[],
}
]
topics
=
[
{
"topic_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"refs"
:
[[
1
,
3
,
3
,
0
]],
"level"
:
"subsection"
,
}
}
]
]
with
patch
(
"app.api.tasks.config.get_orchestrator"
,
return_value
=
"socratic"
),
patch
(
with
patch
(
"app.api.tasks.config.get_orchestrator"
,
return_value
=
"socratic"
),
patch
(
"app.api.tasks.task_catalog.build_ta
sk
_catalog"
,
"app.api.tasks.task_catalog.build_
cached_task_metada
ta_catalog"
,
return_value
=
payload
,
return_value
=
payload
,
),
patch
(
"app.api.tasks.task_catalog.build_topic_catalog"
,
return_value
=
topics
,
):
):
response
=
self
.
client
.
get
(
"/api/tasks"
)
response
=
self
.
client
.
get
(
"/api/tasks"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
body
=
response
.
json
()
body
=
response
.
json
()
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"topics"
][
0
][
"topic_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
body
[
"task_files"
],
payload
)
self
.
assertNotIn
(
"topics"
,
body
)
def
test_get_task_details_returns_task_payload
(
self
)
->
None
:
payload
=
{
"file_id"
:
"analysis_1"
,
"task_id"
:
"01"
,
"title"
:
"Analysis"
,
"full_text"
:
"Analysis
\n
Intro
\n
Bestimme f(x)."
,
"images"
:
[{
"src"
:
"/api/tasks/assets/analysis/01.png"
,
"description"
:
"Graph"
}],
}
with
patch
(
"app.api.tasks.task_catalog.find_task_details"
,
return_value
=
payload
):
response
=
self
.
client
.
get
(
"/api/tasks/analysis_1/01"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
json
(),
payload
)
def
test_get_task_details_returns_404_for_invalid_task
(
self
)
->
None
:
with
patch
(
"app.api.tasks.task_catalog.find_task_details"
,
return_value
=
None
):
response
=
self
.
client
.
get
(
"/api/tasks/analysis_1/99"
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_task_asset_endpoint_serves_files_from_task_image_dir
(
self
)
->
None
:
def
test_task_asset_endpoint_serves_files_from_task_image_dir
(
self
)
->
None
:
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
"task-assets-"
))
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
"task-assets-"
))
...
...
math-tutor/frontend/src/api/taskApi.ts
View file @
5ce4995d
...
@@ -3,27 +3,14 @@ export type TaskImage = {
...
@@ -3,27 +3,14 @@ export type TaskImage = {
description
:
string
;
description
:
string
;
};
};
export
type
TaskItem
=
{
export
type
Task
List
Item
=
{
task_id
:
string
;
task_id
:
string
;
statement
:
string
;
full_text
:
string
;
images
:
TaskImage
[];
};
};
export
type
TaskFile
=
{
export
type
TaskFile
=
{
file_id
:
string
;
file_id
:
string
;
title
:
string
;
title
:
string
;
intro
:
string
;
tasks
:
TaskListItem
[];
tasks
:
TaskItem
[];
topics
?:
string
[];
topic_options
?:
TopicOption
[];
};
export
type
TopicOption
=
{
topic_key
:
string
;
label
:
string
;
refs
:
[
number
,
number
,
number
,
number
][];
summary
:
string
;
};
};
export
type
SubsectionOption
=
{
export
type
SubsectionOption
=
{
...
@@ -37,7 +24,14 @@ export type TasksResponse = {
...
@@ -37,7 +24,14 @@ export type TasksResponse = {
orchestrator
:
string
;
orchestrator
:
string
;
enabled
:
boolean
;
enabled
:
boolean
;
task_files
:
TaskFile
[];
task_files
:
TaskFile
[];
topics
:
TopicOption
[];
};
export
type
TaskDetailsResponse
=
{
file_id
:
string
;
task_id
:
string
;
title
:
string
;
full_text
:
string
;
images
:
TaskImage
[];
};
};
export
type
SocraticResponse
=
{
export
type
SocraticResponse
=
{
...
@@ -74,6 +68,19 @@ export async function fetchTasks(): Promise<TasksResponse> {
...
@@ -74,6 +68,19 @@ export async function fetchTasks(): Promise<TasksResponse> {
return
response
.
json
();
return
response
.
json
();
}
}
export
async
function
fetchTaskDetails
(
input
:
{
fileId
:
string
;
taskId
:
string
;
}):
Promise
<
TaskDetailsResponse
>
{
const
response
=
await
fetch
(
`/api/tasks/
${
encodeURIComponent
(
input
.
fileId
)}
/
${
encodeURIComponent
(
input
.
taskId
)}
`
);
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Task details failed:
${
response
.
status
}
`
);
}
return
response
.
json
();
}
export
async
function
fetchSocraticSubsections
():
Promise
<
SocraticResponse
>
{
export
async
function
fetchSocraticSubsections
():
Promise
<
SocraticResponse
>
{
const
response
=
await
fetch
(
"
/api/tasks/socratic-subsections
"
);
const
response
=
await
fetch
(
"
/api/tasks/socratic-subsections
"
);
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
5ce4995d
...
@@ -5,6 +5,7 @@ import {
...
@@ -5,6 +5,7 @@ import {
useContext
,
useContext
,
useEffect
,
useEffect
,
useMemo
,
useMemo
,
useRef
,
useState
,
useState
,
type
PropsWithChildren
,
type
PropsWithChildren
,
}
from
"
react
"
;
}
from
"
react
"
;
...
@@ -15,13 +16,14 @@ import {
...
@@ -15,13 +16,14 @@ import {
type
OrchestratorName
,
type
OrchestratorName
,
}
from
"
../api/orchestratorApi
"
;
}
from
"
../api/orchestratorApi
"
;
import
{
import
{
fetchTaskDetails
,
fetchSocraticSubsections
,
fetchSocraticSubsections
,
fetchTasks
,
fetchTasks
,
type
SelectedSubsectionRef
,
type
SelectedSubsectionRef
,
type
SelectedTaskRef
,
type
SelectedTaskRef
,
type
SubsectionOption
,
type
SubsectionOption
,
type
TopicOption
,
type
TaskImage
,
type
TaskImage
,
type
TaskDetailsResponse
,
type
TaskFile
,
type
TaskFile
,
}
from
"
../api/taskApi
"
;
}
from
"
../api/taskApi
"
;
...
@@ -45,7 +47,6 @@ export type SelectedSubsection = SelectedSubsectionRef & {
...
@@ -45,7 +47,6 @@ export type SelectedSubsection = SelectedSubsectionRef & {
export
type
TaskSelectionState
=
{
export
type
TaskSelectionState
=
{
taskFiles
:
TaskFile
[];
taskFiles
:
TaskFile
[];
topics
:
TopicOption
[];
selectedTaskRef
:
SelectedTaskRef
|
null
;
selectedTaskRef
:
SelectedTaskRef
|
null
;
selectedTask
:
SelectedTask
|
null
;
selectedTask
:
SelectedTask
|
null
;
selectedTaskFile
:
TaskFile
|
null
;
selectedTaskFile
:
TaskFile
|
null
;
...
@@ -90,8 +91,7 @@ export const getDefaultTaskId = (tasks: Array<{ task_id: string }>): string =>
...
@@ -90,8 +91,7 @@ export const getDefaultTaskId = (tasks: Array<{ task_id: string }>): string =>
tasks
.
find
((
task
)
=>
task
.
task_id
===
"
01
"
)?.
task_id
||
tasks
[
0
]?.
task_id
||
""
;
tasks
.
find
((
task
)
=>
task
.
task_id
===
"
01
"
)?.
task_id
||
tasks
[
0
]?.
task_id
||
""
;
const
isSelectableTaskFile
=
(
file
:
TaskFile
):
boolean
=>
const
isSelectableTaskFile
=
(
file
:
TaskFile
):
boolean
=>
(
Array
.
isArray
(
file
.
topics
)
&&
file
.
topics
.
length
>
0
)
||
Array
.
isArray
(
file
.
tasks
)
&&
file
.
tasks
.
length
>
0
;
(
Array
.
isArray
(
file
.
topic_options
)
&&
file
.
topic_options
.
length
>
0
);
const
isTaskCoupledOrchestrator
=
(
value
:
OrchestratorName
):
boolean
=>
const
isTaskCoupledOrchestrator
=
(
value
:
OrchestratorName
):
boolean
=>
value
===
"
task
"
||
value
===
"
socratic
"
;
value
===
"
task
"
||
value
===
"
socratic
"
;
...
@@ -110,36 +110,36 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -110,36 +110,36 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
[
orchestratorError
,
setOrchestratorError
]
=
useState
<
string
|
null
>
(
null
);
const
[
orchestratorError
,
setOrchestratorError
]
=
useState
<
string
|
null
>
(
null
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
const
[
topics
,
setTopics
]
=
useState
<
TopicOption
[]
>
([]);
const
[
subsections
,
setSubsections
]
=
useState
<
SubsectionOption
[]
>
([]);
const
[
subsections
,
setSubsections
]
=
useState
<
SubsectionOption
[]
>
([]);
const
[
selectedTaskRef
,
setSelectedTaskRef
]
=
useState
<
SelectedTaskRef
|
null
>
(
null
);
const
[
selectedTaskRef
,
setSelectedTaskRef
]
=
useState
<
SelectedTaskRef
|
null
>
(
null
);
const
[
selectedSubsectionRef
,
setSelectedSubsectionRef
]
=
const
[
selectedSubsectionRef
,
setSelectedSubsectionRef
]
=
useState
<
SelectedSubsectionRef
|
null
>
(
null
);
useState
<
SelectedSubsectionRef
|
null
>
(
null
);
const
[
tasksError
,
setTasksError
]
=
useState
<
string
|
null
>
(
null
);
const
[
tasksError
,
setTasksError
]
=
useState
<
string
|
null
>
(
null
);
const
[
taskLocked
,
setTaskLocked
]
=
useState
(
false
);
const
[
taskLocked
,
setTaskLocked
]
=
useState
(
false
);
const
[
selectedTaskDetails
,
setSelectedTaskDetails
]
=
useState
<
TaskDetailsResponse
|
null
>
(
null
);
const
taskDetailsRequestRef
=
useRef
(
0
);
const
didRunPostInitReloadRef
=
useRef
(
false
);
const
isTaskModeEnabled
=
isTaskCoupledOrchestrator
(
selectedOrchestrator
);
const
isTaskModeEnabled
=
isTaskCoupledOrchestrator
(
selectedOrchestrator
);
const
selectedTask
=
useMemo
<
SelectedTask
|
null
>
(()
=>
{
const
selectedTask
=
useMemo
<
SelectedTask
|
null
>
(()
=>
{
if
(
!
selectedTaskRef
)
{
if
(
!
selectedTaskRef
||
!
selectedTaskDetails
)
{
return
null
;
return
null
;
}
}
const
file
=
taskFiles
.
find
((
item
)
=>
item
.
file_id
===
selectedTaskRef
.
fileId
);
if
(
if
(
!
file
)
{
selectedTaskDetails
.
file_id
!==
selectedTaskRef
.
fileId
||
return
null
;
selectedTaskDetails
.
task_id
!==
selectedTaskRef
.
taskId
}
)
{
const
task
=
file
.
tasks
.
find
((
item
)
=>
item
.
task_id
===
selectedTaskRef
.
taskId
);
if
(
!
task
)
{
return
null
;
return
null
;
}
}
return
{
return
{
fileId
:
f
il
e
.
file_id
,
fileId
:
selectedTaskDeta
il
s
.
file_id
,
taskId
:
task
.
task_id
,
taskId
:
selectedTaskDetails
.
task_id
,
title
:
f
il
e
.
title
,
title
:
selectedTaskDeta
il
s
.
title
,
fullText
:
task
.
full_text
,
fullText
:
selectedTaskDetails
.
full_text
,
images
:
task
.
images
||
[],
images
:
selectedTaskDetails
.
images
||
[],
};
};
},
[
selectedTask
Ref
,
taskFiles
]);
},
[
selectedTask
Details
,
selectedTaskRef
]);
const
selectedTaskFile
=
useMemo
(
const
selectedTaskFile
=
useMemo
(
()
=>
taskFiles
.
find
((
file
)
=>
file
.
file_id
===
selectedTaskRef
?.
fileId
)
||
null
,
()
=>
taskFiles
.
find
((
file
)
=>
file
.
file_id
===
selectedTaskRef
?.
fileId
)
||
null
,
...
@@ -198,12 +198,37 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -198,12 +198,37 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
};
};
},
[
selectedSubsectionRef
,
subsections
]);
},
[
selectedSubsectionRef
,
subsections
]);
const
loadTaskDetails
=
useCallback
(
async
(
taskRef
:
SelectedTaskRef
|
null
)
=>
{
if
(
!
taskRef
)
{
setSelectedTaskDetails
(
null
);
return
;
}
const
requestId
=
++
taskDetailsRequestRef
.
current
;
try
{
const
payload
=
await
fetchTaskDetails
({
fileId
:
taskRef
.
fileId
,
taskId
:
taskRef
.
taskId
,
});
if
(
taskDetailsRequestRef
.
current
!==
requestId
)
{
return
;
}
setSelectedTaskDetails
(
payload
);
}
catch
(
error
)
{
if
(
taskDetailsRequestRef
.
current
!==
requestId
)
{
return
;
}
setSelectedTaskDetails
(
null
);
setTasksError
(
t
(
"
failedLoadTasks
"
));
void
error
;
}
},
[]);
const
loadSelectionData
=
useCallback
(
async
(
orchestrator
:
OrchestratorName
)
=>
{
const
loadSelectionData
=
useCallback
(
async
(
orchestrator
:
OrchestratorName
)
=>
{
setTasksError
(
null
);
setTasksError
(
null
);
try
{
try
{
const
isSocratic
=
orchestrator
===
"
socratic
"
;
const
isSocratic
=
orchestrator
===
"
socratic
"
;
let
files
:
TaskFile
[]
=
[];
let
files
:
TaskFile
[]
=
[];
let
topicsPayload
:
TopicOption
[]
=
[];
let
subsectionsPayload
:
SubsectionOption
[]
=
[];
let
subsectionsPayload
:
SubsectionOption
[]
=
[];
if
(
isSocratic
)
{
if
(
isSocratic
)
{
...
@@ -212,13 +237,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -212,13 +237,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
}
else
{
}
else
{
const
payload
=
await
fetchTasks
();
const
payload
=
await
fetchTasks
();
files
=
payload
.
task_files
||
[];
files
=
payload
.
task_files
||
[];
topicsPayload
=
payload
.
topics
||
[];
}
}
const
selectableFiles
=
files
.
filter
((
file
)
=>
isSelectableTaskFile
(
file
));
const
selectableFiles
=
files
.
filter
((
file
)
=>
Array
.
isArray
(
file
.
tasks
)
&&
file
.
tasks
.
length
>
0
);
let
nextTaskRef
:
SelectedTaskRef
|
null
=
null
;
let
nextSubsectionRef
:
SelectedSubsectionRef
|
null
=
null
;
setTaskFiles
(
files
);
setTaskFiles
(
files
);
setTopics
(
topicsPayload
);
setSubsections
(
subsectionsPayload
);
setSubsections
(
subsectionsPayload
);
setSelectedTaskRef
((
prev
)
=>
{
setSelectedTaskRef
((
prev
)
=>
{
if
(
isSocratic
)
{
if
(
isSocratic
)
{
...
@@ -227,6 +252,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -227,6 +252,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
if
(
prev
)
{
if
(
prev
)
{
const
file
=
selectableFiles
.
find
((
item
)
=>
item
.
file_id
===
prev
.
fileId
);
const
file
=
selectableFiles
.
find
((
item
)
=>
item
.
file_id
===
prev
.
fileId
);
if
(
file
&&
file
.
tasks
.
some
((
task
)
=>
task
.
task_id
===
prev
.
taskId
))
{
if
(
file
&&
file
.
tasks
.
some
((
task
)
=>
task
.
task_id
===
prev
.
taskId
))
{
nextTaskRef
=
prev
;
return
prev
;
return
prev
;
}
}
}
}
...
@@ -239,7 +265,8 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -239,7 +265,8 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
if
(
!
defaultTaskId
)
{
if
(
!
defaultTaskId
)
{
return
null
;
return
null
;
}
}
return
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
nextTaskRef
=
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
return
nextTaskRef
;
});
});
setSelectedSubsectionRef
((
prev
)
=>
{
setSelectedSubsectionRef
((
prev
)
=>
{
if
(
!
isSocratic
)
{
if
(
!
isSocratic
)
{
...
@@ -250,6 +277,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -250,6 +277,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
(
item
)
=>
item
.
subsection_key
===
prev
.
subsectionKey
(
item
)
=>
item
.
subsection_key
===
prev
.
subsectionKey
);
);
if
(
option
)
{
if
(
option
)
{
nextSubsectionRef
=
prev
;
return
prev
;
return
prev
;
}
}
}
}
...
@@ -258,18 +286,27 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -258,18 +286,27 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
if
(
!
firstOption
)
{
if
(
!
firstOption
)
{
return
null
;
return
null
;
}
}
return
{
subsectionKey
:
firstOption
.
subsection_key
};
nextSubsectionRef
=
{
subsectionKey
:
firstOption
.
subsection_key
};
return
nextSubsectionRef
;
});
});
if
(
isSocratic
)
{
taskDetailsRequestRef
.
current
+=
1
;
setSelectedTaskDetails
(
null
);
}
else
{
await
loadTaskDetails
(
nextTaskRef
);
}
void
nextSubsectionRef
;
}
catch
(
error
)
{
}
catch
(
error
)
{
setTasksError
(
t
(
"
failedLoadTasks
"
));
setTasksError
(
t
(
"
failedLoadTasks
"
));
setTaskFiles
([]);
setTaskFiles
([]);
setTopics
([]);
setSubsections
([]);
setSubsections
([]);
setSelectedTaskRef
(
null
);
setSelectedTaskRef
(
null
);
setSelectedSubsectionRef
(
null
);
setSelectedSubsectionRef
(
null
);
taskDetailsRequestRef
.
current
+=
1
;
setSelectedTaskDetails
(
null
);
void
error
;
void
error
;
}
}
},
[]);
},
[
loadTaskDetails
]);
const
initTasks
=
useCallback
(
async
()
=>
{
const
initTasks
=
useCallback
(
async
()
=>
{
setOrchestratorError
(
null
);
setOrchestratorError
(
null
);
...
@@ -302,12 +339,52 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -302,12 +339,52 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
void
initTasks
();
void
initTasks
();
},
[
initTasks
]);
},
[
initTasks
]);
useEffect
(()
=>
{
if
(
!
isTasksInitialized
)
{
return
;
}
if
(
!
didRunPostInitReloadRef
.
current
)
{
didRunPostInitReloadRef
.
current
=
true
;
return
;
}
void
loadSelectionData
(
selectedOrchestrator
);
},
[
isTasksInitialized
,
loadSelectionData
,
selectedOrchestrator
]);
useEffect
(()
=>
{
if
(
!
isTasksInitialized
||
selectedOrchestrator
!==
"
task
"
||
!
selectedTaskRef
)
{
return
;
}
if
(
selectedTaskDetails
?.
file_id
===
selectedTaskRef
.
fileId
&&
selectedTaskDetails
?.
task_id
===
selectedTaskRef
.
taskId
)
{
return
;
}
void
loadTaskDetails
(
selectedTaskRef
);
},
[
isTasksInitialized
,
loadTaskDetails
,
selectedOrchestrator
,
selectedTaskDetails
?.
file_id
,
selectedTaskDetails
?.
task_id
,
selectedTaskRef
,
]);
const
setTaskRef
=
useCallback
(
(
value
:
SelectedTaskRef
|
null
)
=>
{
setSelectedTaskRef
(
value
);
setTasksError
(
null
);
void
loadTaskDetails
(
value
);
},
[
loadTaskDetails
]
);
const
setTaskFile
=
useCallback
(
const
setTaskFile
=
useCallback
(
(
fileId
:
string
)
=>
{
(
fileId
:
string
)
=>
{
if
(
!
fileId
)
{
if
(
!
fileId
)
{
return
;
return
;
}
}
const
file
=
taskFiles
.
find
((
item
)
=>
item
.
file_id
===
fileId
&&
i
sSelectableTaskFile
(
item
)
);
const
file
=
taskFiles
.
find
((
item
)
=>
item
.
file_id
===
fileId
&&
i
tem
.
tasks
.
length
>
0
);
if
(
!
file
)
{
if
(
!
file
)
{
return
;
return
;
}
}
...
@@ -315,9 +392,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -315,9 +392,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
if
(
!
defaultTaskId
)
{
if
(
!
defaultTaskId
)
{
return
;
return
;
}
}
setSelectedTaskRef
({
fileId
:
file
.
file_id
,
taskId
:
defaultTaskId
});
setTasksError
(
null
);
const
nextRef
=
{
fileId
:
file
.
file_id
,
taskId
:
defaultTaskId
};
setSelectedTaskRef
(
nextRef
);
void
loadTaskDetails
(
nextRef
);
},
},
[
taskFiles
]
[
loadTaskDetails
,
taskFiles
]
);
);
const
setTaskId
=
useCallback
(
const
setTaskId
=
useCallback
(
...
@@ -325,9 +405,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -325,9 +405,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
if
(
!
taskId
||
!
selectedTaskRef
?.
fileId
)
{
if
(
!
taskId
||
!
selectedTaskRef
?.
fileId
)
{
return
;
return
;
}
}
setSelectedTaskRef
({
fileId
:
selectedTaskRef
.
fileId
,
taskId
});
setTasksError
(
null
);
const
nextRef
=
{
fileId
:
selectedTaskRef
.
fileId
,
taskId
};
setSelectedTaskRef
(
nextRef
);
void
loadTaskDetails
(
nextRef
);
},
},
[
selectedTaskRef
]
[
loadTaskDetails
,
selectedTaskRef
]
);
);
const
setSubsectionKey
=
useCallback
(
const
setSubsectionKey
=
useCallback
(
...
@@ -350,8 +433,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -350,8 +433,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setTaskLocked
(
false
);
setTaskLocked
(
false
);
if
(
value
===
"
socratic
"
)
{
if
(
value
===
"
socratic
"
)
{
setSelectedTaskRef
(
null
);
setSelectedTaskRef
(
null
);
taskDetailsRequestRef
.
current
+=
1
;
setSelectedTaskDetails
(
null
);
}
else
if
(
!
isTaskCoupledOrchestrator
(
value
))
{
}
else
if
(
!
isTaskCoupledOrchestrator
(
value
))
{
setSelectedTaskRef
(
null
);
setSelectedTaskRef
(
null
);
taskDetailsRequestRef
.
current
+=
1
;
setSelectedTaskDetails
(
null
);
}
}
if
(
value
!==
"
socratic
"
)
{
if
(
value
!==
"
socratic
"
)
{
setSelectedSubsectionRef
(
null
);
setSelectedSubsectionRef
(
null
);
...
@@ -371,6 +458,8 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -371,6 +458,8 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedTaskRef
(
null
);
setSelectedTaskRef
(
null
);
setSelectedSubsectionRef
(
null
);
setSelectedSubsectionRef
(
null
);
setTaskLocked
(
false
);
setTaskLocked
(
false
);
taskDetailsRequestRef
.
current
+=
1
;
setSelectedTaskDetails
(
null
);
},
[]);
},
[]);
const
value
:
TutorSessionState
=
{
const
value
:
TutorSessionState
=
{
...
@@ -384,7 +473,6 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -384,7 +473,6 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
isOrchestratorSelectable
,
isOrchestratorSelectable
,
orchestratorError
,
orchestratorError
,
taskFiles
,
taskFiles
,
topics
,
subsections
,
subsections
,
selectedTaskRef
,
selectedTaskRef
,
selectedTask
,
selectedTask
,
...
@@ -394,7 +482,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -394,7 +482,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
selectedSubsectionRef
,
selectedSubsectionRef
,
selectedSubsection
,
selectedSubsection
,
subsectionOptions
,
subsectionOptions
,
setTaskRef
:
setSelectedTaskRef
,
setTaskRef
,
setSubsectionRef
:
setSelectedSubsectionRef
,
setSubsectionRef
:
setSelectedSubsectionRef
,
setTaskFile
,
setTaskFile
,
setTaskId
,
setTaskId
,
...
...
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