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
9c79c5ca
Commit
9c79c5ca
authored
Feb 18, 2026
by
Kantz
Browse files
task panel exptrahiert
parent
0893fd21
Changes
4
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/deterministic_services/task_catalog.py
View file @
9c79c5ca
...
@@ -177,6 +177,13 @@ def select_task_for_context(
...
@@ -177,6 +177,13 @@ 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_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
]]
=
[]
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
task_file
in
catalog
:
for
task_file
in
catalog
:
file_id
=
str
(
task_file
.
get
(
"_file_id"
,
""
))
file_id
=
str
(
task_file
.
get
(
"_file_id"
,
""
))
...
@@ -196,6 +203,7 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
...
@@ -196,6 +203,7 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
"full_text"
:
"
\n
"
.
join
(
full_text_parts
),
"full_text"
:
"
\n
"
.
join
(
full_text_parts
),
}
}
)
)
tasks
.
sort
(
key
=
lambda
item
:
item
[
"task_id"
])
response
.
append
(
response
.
append
(
{
{
"file_id"
:
file_id
,
"file_id"
:
file_id
,
...
...
math-tutor/frontend/src/components/Task/TaskPanel.tsx
0 → 100644
View file @
9c79c5ca
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
{
t
}
from
"
../../i18n
"
;
type
SelectOption
=
{
value
:
string
;
label
:
string
;
};
type
TaskPanelProps
=
{
selectedFileId
:
string
;
fileOptions
:
SelectOption
[];
selectedTaskId
:
string
;
taskOptions
:
SelectOption
[];
selectedTaskText
:
string
;
tasksError
?:
string
|
null
;
onTaskFileChange
:
(
fileId
:
string
)
=>
void
;
onTaskChange
:
(
taskId
:
string
)
=>
void
;
};
export
default
function
TaskPanel
({
selectedFileId
,
fileOptions
,
selectedTaskId
,
taskOptions
,
selectedTaskText
,
tasksError
,
onTaskFileChange
,
onTaskChange
,
}:
TaskPanelProps
)
{
const
taskDisplayRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
);
useEffect
(()
=>
{
if
(
!
selectedTaskText
||
!
taskDisplayRef
.
current
)
{
return
;
}
const
mathjax
=
window
.
MathJax
;
if
(
!
mathjax
?.
typesetPromise
)
{
return
;
}
mathjax
.
typesetPromise
([
taskDisplayRef
.
current
]).
catch
(()
=>
undefined
);
},
[
selectedTaskText
]);
return
(
<
section
className
=
"task-panel"
>
<
div
className
=
"task-panel-header"
>
<
div
className
=
"task-panel-title"
>
{
t
(
"
task
"
)
}
</
div
>
<
div
className
=
"task-panel-controls"
>
<
select
className
=
"task-select"
value
=
{
selectedFileId
}
onChange
=
{
(
event
)
=>
onTaskFileChange
(
event
.
target
.
value
)
}
disabled
=
{
!
fileOptions
.
length
}
>
{
fileOptions
.
length
?
(
fileOptions
.
map
((
option
)
=>
(
<
option
key
=
{
option
.
value
}
value
=
{
option
.
value
}
>
{
option
.
label
}
</
option
>
))
)
:
(
<
option
value
=
""
>
{
t
(
"
noTasksAvailable
"
)
}
</
option
>
)
}
</
select
>
<
select
className
=
"task-select"
value
=
{
selectedTaskId
}
onChange
=
{
(
event
)
=>
onTaskChange
(
event
.
target
.
value
)
}
disabled
=
{
!
taskOptions
.
length
}
>
{
taskOptions
.
length
?
(
taskOptions
.
map
((
option
)
=>
(
<
option
key
=
{
option
.
value
}
value
=
{
option
.
value
}
>
{
option
.
label
}
</
option
>
))
)
:
(
<
option
value
=
""
>
{
t
(
"
noTasksAvailable
"
)
}
</
option
>
)
}
</
select
>
</
div
>
</
div
>
<
div
className
=
"task-panel-content"
ref
=
{
taskDisplayRef
}
>
{
selectedTaskText
||
t
(
"
noTaskSelected
"
)
}
</
div
>
{
tasksError
?
<
div
className
=
"task-panel-error"
>
{
tasksError
}
</
div
>
:
null
}
</
section
>
);
}
math-tutor/frontend/src/pages/App.tsx
View file @
9c79c5ca
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"
react
"
;
import
{
useEffect
,
useMemo
,
useState
}
from
"
react
"
;
import
"
../styles/theme.css
"
;
import
"
../styles/theme.css
"
;
import
ChatWindow
from
"
../components/Chat/ChatWindow
"
;
import
ChatWindow
from
"
../components/Chat/ChatWindow
"
;
import
CanvasDrawer
from
"
../components/Canvas/CanvasDrawer
"
;
import
CanvasDrawer
from
"
../components/Canvas/CanvasDrawer
"
;
import
DocPanel
from
"
../components/Retrieval/DocPanel
"
;
import
DocPanel
from
"
../components/Retrieval/DocPanel
"
;
import
TaskPanel
from
"
../components/Task/TaskPanel
"
;
import
type
{
ChatMessage
}
from
"
../components/Chat/MessageList
"
;
import
type
{
ChatMessage
}
from
"
../components/Chat/MessageList
"
;
import
type
{
RetrievedDoc
}
from
"
../components/Retrieval/DocPanel
"
;
import
type
{
RetrievedDoc
}
from
"
../components/Retrieval/DocPanel
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
t
}
from
"
../i18n
"
;
...
@@ -83,6 +84,9 @@ type SelectedTask = SelectedTaskRef & {
...
@@ -83,6 +84,9 @@ type SelectedTask = SelectedTaskRef & {
fullText
:
string
;
fullText
:
string
;
};
};
const
getDefaultTaskId
=
(
tasks
:
TaskItem
[]):
string
=>
tasks
.
find
((
task
)
=>
task
.
task_id
===
"
01
"
)?.
task_id
||
tasks
[
0
]?.
task_id
||
""
;
const
sourceIdToKey
=
(
sourceId
:
ContextSource
[
"
source_id
"
])
=>
{
const
sourceIdToKey
=
(
sourceId
:
ContextSource
[
"
source_id
"
])
=>
{
const
parts
=
[
const
parts
=
[
sourceId
.
chapter_title
??
""
,
sourceId
.
chapter_title
??
""
,
...
@@ -144,7 +148,6 @@ export default function App() {
...
@@ -144,7 +148,6 @@ export default function App() {
null
null
);
);
const
[
tasksError
,
setTasksError
]
=
useState
<
string
|
null
>
(
null
);
const
[
tasksError
,
setTasksError
]
=
useState
<
string
|
null
>
(
null
);
const
taskDisplayRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
);
const
docIndexes
=
useMemo
(()
=>
{
const
docIndexes
=
useMemo
(()
=>
{
const
bySourceKey
:
Record
<
string
,
RetrievedDoc
>
=
{};
const
bySourceKey
:
Record
<
string
,
RetrievedDoc
>
=
{};
...
@@ -209,17 +212,30 @@ export default function App() {
...
@@ -209,17 +212,30 @@ export default function App() {
};
};
},
[
selectedTaskRef
,
taskFiles
]);
},
[
selectedTaskRef
,
taskFiles
]);
const
taskOptions
=
useMemo
(
const
selectedTaskFile
=
useMemo
(
()
=>
taskFiles
.
find
((
file
)
=>
file
.
file_id
===
selectedTaskRef
?.
fileId
)
||
null
,
[
selectedTaskRef
?.
fileId
,
taskFiles
]
);
const
taskFileOptions
=
useMemo
(
()
=>
()
=>
taskFiles
.
flatMap
((
file
)
=>
taskFiles
.
map
((
file
)
=>
({
file
.
tasks
.
map
((
task
)
=>
({
value
:
file
.
file_id
,
value
:
`
${
file
.
file_id
}
::
${
task
.
task_id
}
`
,
label
:
file
.
title
||
file
.
file_id
,
label
:
`
${
file
.
title
}
-
${
task
.
task_id
}
`
,
})),
}))
),
[
taskFiles
]
[
taskFiles
]
);
);
const
taskOptions
=
useMemo
(
()
=>
(
selectedTaskFile
?.
tasks
||
[]).
map
((
task
)
=>
({
value
:
task
.
task_id
,
label
:
task
.
task_id
,
})),
[
selectedTaskFile
]
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
isActive
=
true
;
let
isActive
=
true
;
...
@@ -237,16 +253,7 @@ export default function App() {
...
@@ -237,16 +253,7 @@ export default function App() {
const
enabled
=
Boolean
(
payload
.
enabled
);
const
enabled
=
Boolean
(
payload
.
enabled
);
setIsTaskModeEnabled
(
enabled
);
setIsTaskModeEnabled
(
enabled
);
setTaskFiles
(
payload
.
task_files
||
[]);
setTaskFiles
(
payload
.
task_files
||
[]);
if
(
!
enabled
)
{
if
(
enabled
&&
payload
.
task_files
?.
length
)
{
const
firstFile
=
payload
.
task_files
[
0
];
const
firstTask
=
firstFile
.
tasks
?.[
0
];
if
(
firstTask
)
{
setSelectedTaskRef
((
prev
)
=>
prev
||
{
fileId
:
firstFile
.
file_id
,
taskId
:
firstTask
.
task_id
}
);
}
}
else
{
setSelectedTaskRef
(
null
);
setSelectedTaskRef
(
null
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -267,6 +274,39 @@ export default function App() {
...
@@ -267,6 +274,39 @@ export default function App() {
};
};
},
[]);
},
[]);
useEffect
(()
=>
{
if
(
!
isTaskModeEnabled
)
{
return
;
}
if
(
!
taskFiles
.
length
)
{
setSelectedTaskRef
(
null
);
return
;
}
setSelectedTaskRef
((
prev
)
=>
{
if
(
prev
)
{
const
file
=
taskFiles
.
find
((
item
)
=>
item
.
file_id
===
prev
.
fileId
);
if
(
file
)
{
const
hasTask
=
file
.
tasks
.
some
((
task
)
=>
task
.
task_id
===
prev
.
taskId
);
if
(
hasTask
)
{
return
prev
;
}
const
defaultTaskId
=
getDefaultTaskId
(
file
.
tasks
);
if
(
defaultTaskId
)
{
return
{
fileId
:
file
.
file_id
,
taskId
:
defaultTaskId
};
}
}
}
const
firstFile
=
taskFiles
[
0
];
const
defaultTaskId
=
getDefaultTaskId
(
firstFile
.
tasks
||
[]);
if
(
!
defaultTaskId
)
{
return
null
;
}
return
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
});
},
[
isTaskModeEnabled
,
taskFiles
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
isTaskModeEnabled
||
!
selectedTaskRef
)
{
if
(
!
isTaskModeEnabled
||
!
selectedTaskRef
)
{
return
;
return
;
...
@@ -291,17 +331,6 @@ export default function App() {
...
@@ -291,17 +331,6 @@ export default function App() {
void
syncTaskSelection
();
void
syncTaskSelection
();
},
[
chatSessionId
,
isTaskModeEnabled
,
selectedTaskRef
]);
},
[
chatSessionId
,
isTaskModeEnabled
,
selectedTaskRef
]);
useEffect
(()
=>
{
if
(
!
isTaskModeEnabled
||
!
selectedTask
?.
fullText
||
!
taskDisplayRef
.
current
)
{
return
;
}
const
mathjax
=
window
.
MathJax
;
if
(
!
mathjax
?.
typesetPromise
)
{
return
;
}
mathjax
.
typesetPromise
([
taskDisplayRef
.
current
]).
catch
(()
=>
undefined
);
},
[
isTaskModeEnabled
,
selectedTask
?.
fullText
]);
const
handleSend
=
async
()
=>
{
const
handleSend
=
async
()
=>
{
const
trimmed
=
draft
.
trim
();
const
trimmed
=
draft
.
trim
();
if
(
!
trimmed
)
{
if
(
!
trimmed
)
{
...
@@ -438,12 +467,26 @@ export default function App() {
...
@@ -438,12 +467,26 @@ export default function App() {
setIsCanvasVisible
((
prev
)
=>
!
prev
);
setIsCanvasVisible
((
prev
)
=>
!
prev
);
};
};
const
handleTaskChange
=
(
value
:
string
)
=>
{
const
handleTaskFileChange
=
(
fileId
:
string
)
=>
{
const
[
fileId
,
taskId
]
=
value
.
split
(
"
::
"
);
if
(
!
fileId
)
{
if
(
!
fileId
||
!
taskId
)
{
return
;
return
;
}
}
setSelectedTaskRef
({
fileId
,
taskId
});
const
file
=
taskFiles
.
find
((
item
)
=>
item
.
file_id
===
fileId
);
if
(
!
file
)
{
return
;
}
const
defaultTaskId
=
getDefaultTaskId
(
file
.
tasks
||
[]);
if
(
!
defaultTaskId
)
{
return
;
}
setSelectedTaskRef
({
fileId
:
file
.
file_id
,
taskId
:
defaultTaskId
});
};
const
handleTaskChange
=
(
taskId
:
string
)
=>
{
if
(
!
taskId
||
!
selectedTaskRef
?.
fileId
)
{
return
;
}
setSelectedTaskRef
({
fileId
:
selectedTaskRef
.
fileId
,
taskId
});
};
};
const
resetChatState
=
()
=>
{
const
resetChatState
=
()
=>
{
...
@@ -675,35 +718,16 @@ export default function App() {
...
@@ -675,35 +718,16 @@ export default function App() {
<
main
className
=
"app-main"
>
<
main
className
=
"app-main"
>
<
section
className
=
"chat-column"
>
<
section
className
=
"chat-column"
>
{
isTaskModeEnabled
?
(
{
isTaskModeEnabled
?
(
<
section
className
=
"task-panel"
>
<
TaskPanel
<
div
className
=
"task-panel-header"
>
selectedFileId
=
{
selectedTaskRef
?.
fileId
||
""
}
<
div
className
=
"task-panel-title"
>
{
t
(
"
task
"
)
}
</
div
>
fileOptions
=
{
taskFileOptions
}
<
select
selectedTaskId
=
{
selectedTaskRef
?.
taskId
||
""
}
className
=
"task-select"
taskOptions
=
{
taskOptions
}
value
=
{
selectedTaskText
=
{
selectedTask
?.
fullText
||
""
}
selectedTaskRef
tasksError
=
{
tasksError
}
?
`
${
selectedTaskRef
.
fileId
}
::
${
selectedTaskRef
.
taskId
}
`
onTaskFileChange
=
{
handleTaskFileChange
}
:
""
onTaskChange
=
{
handleTaskChange
}
}
/>
onChange
=
{
(
event
)
=>
handleTaskChange
(
event
.
target
.
value
)
}
disabled
=
{
!
taskOptions
.
length
}
>
{
taskOptions
.
length
?
(
taskOptions
.
map
((
option
)
=>
(
<
option
key
=
{
option
.
value
}
value
=
{
option
.
value
}
>
{
option
.
label
}
</
option
>
))
)
:
(
<
option
value
=
""
>
{
t
(
"
noTasksAvailable
"
)
}
</
option
>
)
}
</
select
>
</
div
>
<
div
className
=
"task-panel-content"
ref
=
{
taskDisplayRef
}
>
{
selectedTask
?.
fullText
||
t
(
"
noTaskSelected
"
)
}
</
div
>
{
tasksError
?
<
div
className
=
"task-panel-error"
>
{
tasksError
}
</
div
>
:
null
}
</
section
>
)
:
null
}
)
:
null
}
<
ChatWindow
<
ChatWindow
...
...
math-tutor/frontend/src/styles/theme.css
View file @
9c79c5ca
...
@@ -112,7 +112,6 @@ body {
...
@@ -112,7 +112,6 @@ body {
.task-panel-header
{
.task-panel-header
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
10px
;
gap
:
10px
;
flex-wrap
:
wrap
;
flex-wrap
:
wrap
;
}
}
...
@@ -121,6 +120,14 @@ body {
...
@@ -121,6 +120,14 @@ body {
font-weight
:
600
;
font-weight
:
600
;
}
}
.task-panel-controls
{
margin-left
:
auto
;
display
:
flex
;
gap
:
8px
;
flex-wrap
:
wrap
;
justify-content
:
flex-end
;
}
.task-select
{
.task-select
{
border
:
1px
solid
#d8d1c4
;
border
:
1px
solid
#d8d1c4
;
border-radius
:
10px
;
border-radius
:
10px
;
...
@@ -551,6 +558,12 @@ body {
...
@@ -551,6 +558,12 @@ body {
width
:
100%
;
width
:
100%
;
}
}
.task-panel-controls
{
width
:
100%
;
margin-left
:
0
;
justify-content
:
flex-start
;
}
.composer-row
.btn
{
.composer-row
.btn
{
flex
:
1
1
120px
;
flex
:
1
1
120px
;
}
}
...
...
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