Commit 4091f2a9 authored by Kantz's avatar Kantz
Browse files

Merge branch 'dev' into 'main'

ladenzeiten von Tasks verschoben und verkürzt

See merge request kantz/tutor_react!26
parents 307b3f09 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()
......
...@@ -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()))
...@@ -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_task_catalog", "app.api.tasks.task_catalog.build_cached_task_metadata_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\nIntro\nBestimme 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-"))
......
...@@ -3,27 +3,14 @@ export type TaskImage = { ...@@ -3,27 +3,14 @@ export type TaskImage = {
description: string; description: string;
}; };
export type TaskItem = { export type TaskListItem = {
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) {
......
...@@ -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: file.file_id, fileId: selectedTaskDetails.file_id,
taskId: task.task_id, taskId: selectedTaskDetails.task_id,
title: file.title, title: selectedTaskDetails.title,
fullText: task.full_text, fullText: selectedTaskDetails.full_text,
images: task.images || [], images: selectedTaskDetails.images || [],
}; };
}, [selectedTaskRef, taskFiles]); }, [selectedTaskDetails, 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 && isSelectableTaskFile(item)); const file = taskFiles.find((item) => item.file_id === fileId && item.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,
......
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