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
ff6c3d7f
Commit
ff6c3d7f
authored
Mar 04, 2026
by
Kantz
Browse files
implementierung des Modus wechsels
parent
92ce484a
Changes
12
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/chat.py
View file @
ff6c3d7f
...
...
@@ -3,20 +3,15 @@ from __future__ import annotations
import
logging
from
typing
import
List
,
Optional
import
app.config
as
config
from
app.deterministic_services
import
session_store
from
app.deterministic_services.orchestrators.registry
import
(
get_default_orchestrator
,
is_valid_orchestrator
,
resolve_orchestrator
,
)
from
fastapi
import
APIRouter
,
HTTPException
,
Path
,
Query
from
pydantic
import
BaseModel
,
Field
if
config
.
get_orchestrator
()
==
"tutor"
:
from
app.deterministic_services.orchestrators
import
(
orchestrator_tutor
as
orchestrator
,
)
elif
config
.
get_orchestrator
()
==
"task"
:
from
app.deterministic_services.orchestrators
import
orchestrator_task
as
orchestrator
else
:
from
app.deterministic_services.orchestrators
import
orchestrator_qa
as
orchestrator
router
=
APIRouter
()
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -31,6 +26,7 @@ class ChatRequest(BaseModel):
messages
:
List
[
ChatMessage
]
draft
:
Optional
[
str
]
=
None
selected_task
:
Optional
[
dict
[
str
,
str
]]
=
None
orchestrator
:
Optional
[
str
]
=
None
class
ChatResponse
(
BaseModel
):
...
...
@@ -48,6 +44,7 @@ class ChatArchiveSummary(BaseModel):
saved_at
:
str
message_count
:
int
preview
:
str
orchestrator
:
Optional
[
str
]
=
None
class
SelectedTaskRef
(
BaseModel
):
...
...
@@ -60,6 +57,7 @@ class ChatArchiveDetail(BaseModel):
saved_at
:
str
history
:
List
[
ChatMessage
]
selected_task
:
Optional
[
SelectedTaskRef
]
=
None
orchestrator
:
str
@
router
.
post
(
"/api/chat"
,
response_model
=
ChatResponse
)
...
...
@@ -67,16 +65,24 @@ def chat(request: ChatRequest) -> ChatResponse:
if
not
request
.
messages
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"messages required"
)
requested_orchestrator
=
str
(
request
.
orchestrator
or
""
).
strip
().
lower
()
if
requested_orchestrator
and
not
is_valid_orchestrator
(
requested_orchestrator
):
raise
HTTPException
(
status_code
=
422
,
detail
=
"unsupported orchestrator"
)
orchestrator_name
,
orchestrator_impl
=
resolve_orchestrator
(
requested_orchestrator
or
None
)
try
:
payload_messages
=
[{
"role"
:
m
.
role
,
"content"
:
m
.
text
}
for
m
in
request
.
messages
]
if
config
.
get_
orchestrator
()
==
"task"
:
result
=
orchestrator
.
run_chat
(
if
orchestrator
_name
==
"task"
:
result
=
orchestrator
_impl
.
run_chat
(
payload_messages
,
draft
=
request
.
draft
,
selected_task
=
request
.
selected_task
,
)
else
:
result
=
orchestrator
.
run_chat
(
result
=
orchestrator
_impl
.
run_chat
(
payload_messages
,
draft
=
request
.
draft
,
)
...
...
@@ -124,6 +130,7 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail:
saved_at
=
record
.
get
(
"saved_at"
,
""
),
history
=
[
ChatMessage
(
role
=
item
[
"role"
],
text
=
item
[
"text"
])
for
item
in
record
[
"history"
]],
selected_task
=
selected_task
,
orchestrator
=
record
.
get
(
"orchestrator"
)
or
get_default_orchestrator
(),
)
...
...
@@ -132,10 +139,16 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse:
if
not
request
.
messages
:
return
ChatArchiveResponse
(
status
=
"skipped"
,
chat_id
=
"unknown"
)
requested_orchestrator
=
str
(
request
.
orchestrator
or
""
).
strip
().
lower
()
if
requested_orchestrator
and
not
is_valid_orchestrator
(
requested_orchestrator
):
raise
HTTPException
(
status_code
=
422
,
detail
=
"unsupported orchestrator"
)
archive_orchestrator
=
requested_orchestrator
or
get_default_orchestrator
()
try
:
chat_id
=
session_store
.
archive_chat
(
[{
"role"
:
m
.
role
,
"content"
:
m
.
text
}
for
m
in
request
.
messages
],
draft
=
request
.
draft
,
orchestrator
=
archive_orchestrator
,
)
except
Exception
as
exc
:
logger
.
exception
(
"Chat archive failed"
)
...
...
math-tutor/backend/app/api/orchestrator.py
0 → 100644
View file @
ff6c3d7f
from
__future__
import
annotations
from
fastapi
import
APIRouter
from
pydantic
import
BaseModel
from
app.deterministic_services.orchestrators.registry
import
(
AVAILABLE_ORCHESTRATORS
,
get_default_orchestrator
,
)
router
=
APIRouter
()
class
OrchestratorConfigResponse
(
BaseModel
):
default_orchestrator
:
str
available_orchestrators
:
list
[
str
]
@
router
.
get
(
"/api/orchestrator/config"
,
response_model
=
OrchestratorConfigResponse
)
def
get_orchestrator_config
()
->
OrchestratorConfigResponse
:
return
OrchestratorConfigResponse
(
default_orchestrator
=
get_default_orchestrator
(),
available_orchestrators
=
list
(
AVAILABLE_ORCHESTRATORS
),
)
math-tutor/backend/app/deterministic_services/orchestrators/registry.py
0 → 100644
View file @
ff6c3d7f
from
__future__
import
annotations
from
typing
import
Any
import
app.config
as
config
from
app.deterministic_services.orchestrators
import
(
orchestrator_qa
,
orchestrator_task
,
orchestrator_tutor
,
)
AVAILABLE_ORCHESTRATORS
:
tuple
[
str
,
...]
=
(
"qa"
,
"tutor"
,
"task"
)
_ORCHESTRATOR_MODULES
:
dict
[
str
,
Any
]
=
{
"qa"
:
orchestrator_qa
,
"tutor"
:
orchestrator_tutor
,
"task"
:
orchestrator_task
,
}
def
get_default_orchestrator
()
->
str
:
configured
=
str
(
config
.
get_orchestrator
()
or
""
).
strip
().
lower
()
if
configured
in
_ORCHESTRATOR_MODULES
:
return
configured
return
"qa"
def
is_valid_orchestrator
(
value
:
str
)
->
bool
:
return
value
in
_ORCHESTRATOR_MODULES
def
resolve_orchestrator
(
value
:
str
|
None
=
None
)
->
tuple
[
str
,
Any
]:
if
value
:
normalized
=
value
.
strip
().
lower
()
if
normalized
in
_ORCHESTRATOR_MODULES
:
return
normalized
,
_ORCHESTRATOR_MODULES
[
normalized
]
default_name
=
get_default_orchestrator
()
return
default_name
,
_ORCHESTRATOR_MODULES
[
default_name
]
math-tutor/backend/app/deterministic_services/session_store.py
View file @
ff6c3d7f
...
...
@@ -26,7 +26,11 @@ def _extract_selected_task(sheet: dict[str, Any]) -> dict[str, str] | None:
return
{
"file_id"
:
file_id
,
"task_id"
:
task_id
}
def
archive_chat
(
messages
:
list
[
dict
[
str
,
Any
]],
draft
:
str
|
None
=
None
)
->
str
:
def
archive_chat
(
messages
:
list
[
dict
[
str
,
Any
]],
draft
:
str
|
None
=
None
,
orchestrator
:
str
|
None
=
None
,
)
->
str
:
chat_id
=
context_store
.
get_chat_id
(
messages
,
draft
=
draft
)
sheet
=
context_store
.
load_sheet
(
chat_id
)
if
not
sheet
:
...
...
@@ -38,6 +42,7 @@ def archive_chat(messages: list[dict[str, Any]], draft: str | None = None) -> st
record
=
{
"chat_id"
:
chat_id
,
"saved_at"
:
_utc_now
(),
"orchestrator"
:
str
(
orchestrator
or
""
).
strip
().
lower
()
or
None
,
"history"
:
sheet
.
get
(
"history"
,
[]),
"context_sheet"
:
context_store
.
format_sheet
(
sheet
),
"retrieval_contexts"
:
sheet
.
get
(
"retrieval_contexts"
,
[]),
...
...
@@ -65,6 +70,7 @@ def _summarize_record(record: dict[str, Any]) -> dict[str, Any]:
return
{
"chat_id"
:
record
.
get
(
"chat_id"
,
"unknown"
),
"saved_at"
:
record
.
get
(
"saved_at"
,
""
),
"orchestrator"
:
record
.
get
(
"orchestrator"
),
"message_count"
:
len
(
history
),
"preview"
:
preview
,
}
...
...
@@ -123,6 +129,7 @@ def load_archive(chat_id: str) -> dict[str, Any] | None:
return
{
"chat_id"
:
record
.
get
(
"chat_id"
,
chat_id
),
"saved_at"
:
record
.
get
(
"saved_at"
,
""
),
"orchestrator"
:
record
.
get
(
"orchestrator"
),
"history"
:
history
,
"selected_task"
:
selected_task
,
}
...
...
math-tutor/backend/app/main.py
View file @
ff6c3d7f
...
...
@@ -3,7 +3,7 @@ import logging
from
fastapi
import
FastAPI
from
fastapi.middleware.cors
import
CORSMiddleware
from
app.api
import
canvas
,
chat
,
health
,
context
,
tasks
from
app.api
import
canvas
,
chat
,
context
,
health
,
orchestrator
,
tasks
from
app.config
import
get_frontend_url
from
app.deterministic_services
import
embedding_provider
...
...
@@ -36,4 +36,5 @@ app.include_router(chat.router)
app
.
include_router
(
canvas
.
router
)
app
.
include_router
(
context
.
router
)
app
.
include_router
(
health
.
router
)
app
.
include_router
(
orchestrator
.
router
)
app
.
include_router
(
tasks
.
router
)
math-tutor/frontend/src/api/orchestratorApi.ts
0 → 100644
View file @
ff6c3d7f
export
type
OrchestratorName
=
"
qa
"
|
"
tutor
"
|
"
task
"
;
export
type
OrchestratorConfigResponse
=
{
default_orchestrator
:
OrchestratorName
;
available_orchestrators
:
OrchestratorName
[];
};
const
FALLBACK_ORCHESTRATORS
:
OrchestratorName
[]
=
[
"
qa
"
,
"
tutor
"
,
"
task
"
];
const
normalizeOrchestrator
=
(
value
:
string
):
OrchestratorName
|
null
=>
{
if
(
value
===
"
qa
"
||
value
===
"
tutor
"
||
value
===
"
task
"
)
{
return
value
;
}
return
null
;
};
export
const
getFallbackOrchestrators
=
():
OrchestratorName
[]
=>
[...
FALLBACK_ORCHESTRATORS
];
export
async
function
fetchOrchestratorConfig
():
Promise
<
OrchestratorConfigResponse
>
{
const
response
=
await
fetch
(
"
/api/orchestrator/config
"
);
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Orchestrator config failed:
${
response
.
status
}
`
);
}
const
payload
=
await
response
.
json
();
const
defaultOrchestrator
=
normalizeOrchestrator
(
String
(
payload
?.
default_orchestrator
||
""
))
||
"
qa
"
;
const
availableRaw
=
Array
.
isArray
(
payload
?.
available_orchestrators
)
?
payload
.
available_orchestrators
:
[];
const
available
=
availableRaw
.
map
((
item
:
unknown
)
=>
normalizeOrchestrator
(
String
(
item
||
""
)))
.
filter
((
item
:
OrchestratorName
|
null
):
item
is
OrchestratorName
=>
Boolean
(
item
));
return
{
default_orchestrator
:
defaultOrchestrator
,
available_orchestrators
:
available
.
length
?
available
:
getFallbackOrchestrators
(),
};
}
math-tutor/frontend/src/components/Orchestrator/OrchestratorSelect.tsx
0 → 100644
View file @
ff6c3d7f
import
type
{
OrchestratorName
}
from
"
../../api/orchestratorApi
"
;
import
{
t
}
from
"
../../i18n
"
;
type
OrchestratorSelectProps
=
{
value
:
OrchestratorName
;
options
:
OrchestratorName
[];
onChange
:
(
value
:
OrchestratorName
)
=>
void
;
disabled
?:
boolean
;
};
const
labelMap
:
Record
<
OrchestratorName
,
string
>
=
{
qa
:
"
QA
"
,
tutor
:
"
Tutor
"
,
task
:
"
Task
"
,
};
export
default
function
OrchestratorSelect
({
value
,
options
,
onChange
,
disabled
=
false
,
}:
OrchestratorSelectProps
)
{
return
(
<
label
className
=
"orchestrator-select-wrap"
htmlFor
=
"orchestrator-select"
>
<
span
className
=
"orchestrator-select-label"
>
{
t
(
"
orchestratorMode
"
)
}
</
span
>
<
select
id
=
"orchestrator-select"
className
=
"task-select orchestrator-select"
value
=
{
value
}
onChange
=
{
(
event
)
=>
onChange
(
event
.
target
.
value
as
OrchestratorName
)
}
disabled
=
{
disabled
}
>
{
options
.
map
((
option
)
=>
(
<
option
key
=
{
option
}
value
=
{
option
}
>
{
labelMap
[
option
]
}
</
option
>
))
}
</
select
>
</
label
>
);
}
math-tutor/frontend/src/i18n.ts
View file @
ff6c3d7f
...
...
@@ -16,6 +16,7 @@
show
:
"
Show
"
,
newChat
:
"
New Chat
"
,
loading
:
"
Loading...
"
,
orchestratorMode
:
"
Mode
"
,
directChildren
:
"
Direct children
"
,
taskChildren
:
"
Task sources
"
,
indirectChildren
:
"
Indirect children
"
,
...
...
@@ -35,6 +36,7 @@
noSavedChatsYet
:
"
No saved chats yet.
"
,
canvasHidden
:
"
Canvas hidden
"
,
failedLoadTasks
:
"
Could not load tasks.
"
,
failedLoadOrchestratorConfig
:
"
Could not load orchestrator config. Fallback mode active.
"
,
chatRequestFailed
:
"
The chat request failed. Please check backend logs.
"
,
retrievalFailed
:
"
Source retrieval failed. Please check backend logs.
"
,
failedLoadSavedChats
:
"
Could not load saved chats.
"
,
...
...
@@ -72,6 +74,7 @@
show
:
"
anzeigen
"
,
newChat
:
"
neuer Chat
"
,
loading
:
"
Lade...
"
,
orchestratorMode
:
"
Modus
"
,
directChildren
:
"
Direkte Quellen
"
,
taskChildren
:
"
Aufgaben-Quellen
"
,
indirectChildren
:
"
Indirekte Quellen
"
,
...
...
@@ -91,6 +94,8 @@
noSavedChatsYet
:
"
Noch keine gespeicherten Chats.
"
,
canvasHidden
:
"
Canvas ausgeblendet
"
,
failedLoadTasks
:
"
Aufgaben konnten nicht geladen werden.
"
,
failedLoadOrchestratorConfig
:
"
Orchestrator-Konfiguration konnte nicht geladen werden. Fallback-Modus aktiv.
"
,
chatRequestFailed
:
"
Chat-Anfrage ist fehlgeschlagen. Bitte pruefe die Backend-Logs.
"
,
retrievalFailed
:
...
...
math-tutor/frontend/src/pages/ChatPage.tsx
View file @
ff6c3d7f
import
{
useEffect
,
useMemo
,
useState
}
from
"
react
"
;
import
{
useEffect
,
useMemo
,
useState
}
from
"
react
"
;
import
{
useNavigate
}
from
"
react-router-dom
"
;
import
"
../styles/theme.css
"
;
import
ChatWindow
from
"
../components/Chat/ChatWindow
"
;
import
CanvasDrawer
from
"
../components/Canvas/CanvasDrawer
"
;
import
OrchestratorSelect
from
"
../components/Orchestrator/OrchestratorSelect
"
;
import
DocPanel
from
"
../components/Retrieval/DocPanel
"
;
import
TaskPanel
from
"
../components/Task/TaskPanel
"
;
import
type
{
ChatMessage
}
from
"
../components/Chat/MessageList
"
;
import
type
{
RetrievedDoc
}
from
"
../components/Retrieval/DocPanel
"
;
import
{
t
}
from
"
../i18n
"
;
import
type
{
OrchestratorName
}
from
"
../api/orchestratorApi
"
;
import
{
selectTask
}
from
"
../api/taskApi
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
createSessionId
,
useTutorSession
}
from
"
../state/tutorSession
"
;
const
initialMessages
:
ChatMessage
[]
=
[];
const
normalizeOrchestrator
=
(
value
:
string
|
null
|
undefined
):
OrchestratorName
|
null
=>
{
if
(
value
===
"
qa
"
||
value
===
"
tutor
"
||
value
===
"
task
"
)
{
return
value
;
}
return
null
;
};
type
ArchivedChatSummary
=
{
chat_id
:
string
;
saved_at
:
string
;
message_count
:
number
;
preview
:
string
;
orchestrator
?:
string
|
null
;
};
type
ArchivedChatDetail
=
{
chat_id
:
string
;
saved_at
:
string
;
history
:
ChatMessage
[];
orchestrator
?:
string
|
null
;
selected_task
?:
{
file_id
:
string
;
task_id
:
string
;
...
...
@@ -91,9 +102,7 @@ const toRetrievedDoc = (source: ContextSource, index: number): RetrievedDoc => (
});
export
default
function
ChatPage
()
{
const
showChatsButton
=
String
(
import
.
meta
.
env
.
VITE_SHOW_CHATS_BUTTON
??
"
true
"
)
const
showChatsButton
=
String
(
import
.
meta
.
env
.
VITE_SHOW_CHATS_BUTTON
??
"
true
"
)
.
trim
()
.
toLowerCase
()
!==
"
false
"
;
const
navigate
=
useNavigate
();
...
...
@@ -105,6 +114,12 @@ export default function ChatPage() {
selectedTaskRef
,
selectedTask
,
selectedTaskFile
,
selectedOrchestrator
,
availableOrchestrators
,
setSelectedOrchestrator
,
switchOrchestrator
,
isOrchestratorSelectable
,
orchestratorError
,
taskLocked
,
lockTask
,
unlockTask
,
...
...
@@ -139,13 +154,7 @@ export default function ChatPage() {
if
(
!
selectedTaskRef
||
!
taskLocked
)
{
navigate
(
"
/select-task
"
,
{
replace
:
true
});
}
},
[
isTaskModeEnabled
,
isTasksInitialized
,
navigate
,
selectedTaskRef
,
taskLocked
,
]);
},
[
isTaskModeEnabled
,
isTasksInitialized
,
navigate
,
selectedTaskRef
,
taskLocked
]);
const
docIndexes
=
useMemo
(()
=>
{
const
bySourceKey
:
Record
<
string
,
RetrievedDoc
>
=
{};
...
...
@@ -243,6 +252,7 @@ export default function ChatPage() {
const
chatPayload
:
{
messages
:
Array
<
{
role
:
"
user
"
|
"
assistant
"
;
text
:
string
}
>
;
draft
:
string
;
orchestrator
:
OrchestratorName
;
selected_task
?:
{
file_id
:
string
;
task_id
:
string
};
}
=
{
messages
:
[...
messages
,
userMessage
].
map
((
message
)
=>
({
...
...
@@ -250,6 +260,7 @@ export default function ChatPage() {
text
:
message
.
text
,
})),
draft
:
chatSessionId
,
orchestrator
:
selectedOrchestrator
,
};
if
(
isTaskModeEnabled
&&
selectedTaskRef
)
{
chatPayload
.
selected_task
=
{
...
...
@@ -410,7 +421,14 @@ export default function ChatPage() {
setMessages
(
payload
.
history
||
[]);
setChatSessionId
(
payload
.
chat_id
);
if
(
isTaskModeEnabled
)
{
const
restoredOrchestrator
=
normalizeOrchestrator
(
payload
.
orchestrator
);
const
nextOrchestrator
=
restoredOrchestrator
&&
availableOrchestrators
.
includes
(
restoredOrchestrator
)
?
restoredOrchestrator
:
selectedOrchestrator
;
setSelectedOrchestrator
(
nextOrchestrator
);
if
(
nextOrchestrator
===
"
task
"
)
{
const
restoredTask
=
payload
.
selected_task
;
if
(
restoredTask
?.
file_id
&&
restoredTask
?.
task_id
)
{
setTaskRef
({
...
...
@@ -432,6 +450,10 @@ export default function ChatPage() {
unlockTask
();
navigate
(
"
/select-task
"
,
{
replace
:
true
});
}
}
else
{
setTaskRef
(
null
);
unlockTask
();
navigate
(
"
/chat
"
,
{
replace
:
true
});
}
}
catch
(
error
)
{
setArchiveError
(
t
(
"
failedLoadSelectedChat
"
));
...
...
@@ -452,6 +474,7 @@ export default function ChatPage() {
text
:
message
.
text
,
})),
draft
:
chatSessionId
,
orchestrator
:
selectedOrchestrator
,
}),
});
}
catch
(
error
)
{
...
...
@@ -484,6 +507,7 @@ export default function ChatPage() {
text
:
message
.
text
,
})),
draft
:
chatSessionId
,
orchestrator
:
selectedOrchestrator
,
}),
});
}
catch
(
error
)
{
...
...
@@ -533,6 +557,15 @@ export default function ChatPage() {
void
loadArchives
();
};
const
handleSwitchOrchestrator
=
(
next
:
OrchestratorName
)
=>
{
if
(
next
===
selectedOrchestrator
)
{
return
;
}
resetChatState
();
switchOrchestrator
(
next
);
navigate
(
next
===
"
task
"
?
"
/select-task
"
:
"
/chat
"
);
};
const
handleCanvasSave
=
async
(
dataUrl
:
string
)
=>
{
setCanvasStatus
({
kind
:
"
info
"
,
...
...
@@ -655,11 +688,7 @@ export default function ChatPage() {
<
div
className
=
"app-shell"
>
<
header
className
=
"app-header"
>
{
showChatsButton
?
(
<
button
type
=
"button"
className
=
"btn sidebar-toggle"
onClick
=
{
handleOpenSidebar
}
>
<
button
type
=
"button"
className
=
"btn sidebar-toggle"
onClick
=
{
handleOpenSidebar
}
>
{
t
(
"
chats
"
)
}
</
button
>
)
:
null
}
...
...
@@ -671,11 +700,18 @@ export default function ChatPage() {
</
div
>
</
div
>
<
div
className
=
"header-meta"
>
<
OrchestratorSelect
value
=
{
selectedOrchestrator
}
options
=
{
availableOrchestrators
}
onChange
=
{
handleSwitchOrchestrator
}
disabled
=
{
!
isOrchestratorSelectable
}
/>
<
span
className
=
"pill"
>
Postgres + pgvector
</
span
>
<
span
className
=
"pill"
>
Python FastAPI
</
span
>
<
span
className
=
"pill"
>
Mathpix
</
span
>
</
div
>
</
header
>
{
orchestratorError
?
<
div
className
=
"chat-archive-error"
>
{
orchestratorError
}
</
div
>
:
null
}
<
main
className
=
"app-main"
>
<
section
className
=
"chat-column"
>
...
...
@@ -701,11 +737,7 @@ export default function ChatPage() {
)
:
null
}
</
section
>
<
aside
className
=
{
`retrieval-column
${
isTaskModeEnabled
?
"
retrieval-column-task-mode
"
:
""
}
`
}
>
<
aside
className
=
{
`retrieval-column
${
isTaskModeEnabled
?
"
retrieval-column-task-mode
"
:
""
}
`
}
>
{
isTaskModeEnabled
&&
selectedTask
?
(
<
TaskPanel
readOnly
...
...
@@ -736,23 +768,14 @@ export default function ChatPage() {
<
div
className
=
{
`sidebar
${
isSidebarOpen
?
"
active
"
:
""
}
`
}
>
<
div
className
=
"sd-header"
>
<
h4
className
=
"sd-title"
>
{
t
(
"
savedChats
"
)
}
</
h4
>
<
button
type
=
"button"
className
=
"sidebar-button"
onClick
=
{
()
=>
setIsSidebarOpen
(
false
)
}
>
<
button
type
=
"button"
className
=
"sidebar-button"
onClick
=
{
()
=>
setIsSidebarOpen
(
false
)
}
>
X
</
button
>
</
div
>
<
div
className
=
"sd-body"
>
<
ul
className
=
"sd-list"
>
<
li
>
<
button
type
=
"button"
className
=
"sd-link"
onClick
=
{
handleNewChat
}
disabled
=
{
isArchiving
}
>
<
button
type
=
"button"
className
=
"sd-link"
onClick
=
{
handleNewChat
}
disabled
=
{
isArchiving
}
>
{
isArchiving
?
t
(
"
saving
"
)
:
t
(
"
newChat
"
)
}
</
button
>
</
li
>
...
...
@@ -764,9 +787,7 @@ export default function ChatPage() {
<
li
key
=
{
item
.
chat_id
}
>
<
button
type
=
"button"
className
=
{
`sd-link
${
selectedArchiveId
===
item
.
chat_id
?
"
active
"
:
""
}
`
}
className
=
{
`sd-link
${
selectedArchiveId
===
item
.
chat_id
?
"
active
"
:
""
}
`
}
onClick
=
{
()
=>
{
setSelectedArchiveId
(
item
.
chat_id
);
void
handleLoadArchive
(
item
.
chat_id
);
...
...
@@ -782,9 +803,7 @@ export default function ChatPage() {
</
li
>
)
}
</
ul
>
{
archiveError
?
(
<
div
className
=
"chat-archive-error"
>
{
archiveError
}
</
div
>
)
:
null
}
{
archiveError
?
<
div
className
=
"chat-archive-error"
>
{
archiveError
}
</
div
>
:
null
}
</
div
>
</
div
>
<
div
...
...
math-tutor/frontend/src/pages/TaskSelectionPage.tsx
View file @
ff6c3d7f
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
{
useNavigate
}
from
"
react-router-dom
"
;
import
"
../styles/theme.css
"
;
import
{
t
}
from
"
../i18n
"
;
import
type
{
OrchestratorName
}
from
"
../api/orchestratorApi
"
;
import
OrchestratorSelect
from
"
../components/Orchestrator/OrchestratorSelect
"
;
import
{
selectTask
}
from
"
../api/taskApi
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
useTutorSession
}
from
"
../state/tutorSession
"
;
import
"
../styles/theme.css
"
;
export
default
function
TaskSelectionPage
()
{
const
navigate
=
useNavigate
();
...
...
@@ -11,6 +13,11 @@ export default function TaskSelectionPage() {
chatSessionId
,
isTaskModeEnabled
,
isTasksInitialized
,
selectedOrchestrator
,
availableOrchestrators
,
switchOrchestrator
,
isOrchestratorSelectable
,
orchestratorError
,
selectedTaskRef
,
selectedTask
,
taskFileOptions
,
...
...
@@ -63,6 +70,14 @@ export default function TaskSelectionPage() {
}
};
const
handleSwitchOrchestrator
=
(
next
:
OrchestratorName
)
=>
{
if
(
next
===
selectedOrchestrator
)
{
return
;
}
switchOrchestrator
(
next
);
navigate
(
next
===
"
task
"
?
"
/select-task
"
:
"
/chat
"
,
{
replace
:
true
});
};
if
(
!
isTasksInitialized
)
{
return
<
div
className
=
"app-loading"
>
{
t
(
"
loading
"
)
}
</
div
>;
}
...
...
@@ -77,7 +92,16 @@ export default function TaskSelectionPage() {
<
div
className
=
"brand-subtitle"
>
{
t
(
"
taskSelectionSubtitle
"
)
}
</
div
>
</
div
>
</
div
>
<
div
className
=
"header-meta"
>
<
OrchestratorSelect
value
=
{
selectedOrchestrator
}
options
=
{
availableOrchestrators
}
onChange
=
{
handleSwitchOrchestrator
}
disabled
=
{
!
isOrchestratorSelectable
}
/>
</
div
>
</
header
>
{
orchestratorError
?
<
div
className
=
"chat-archive-error"
>
{
orchestratorError
}
</
div
>
:
null
}
<
main
className
=
"task-select-main"
>
<
section
className
=
"task-select-card"
>
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
ff6c3d7f
...
...
@@ -9,6 +9,11 @@ import {
type
PropsWithChildren
,
}
from
"
react
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
fetchOrchestratorConfig
,
getFallbackOrchestrators
,
type
OrchestratorName
,
}
from
"
../api/orchestratorApi
"
;
import
{
fetchTasks
,
type
SelectedTaskRef
,
type
TaskFile
}
from
"
../api/taskApi
"
;
export
type
SelectOption
=
{
...
...
@@ -38,6 +43,12 @@ export type TutorSessionState = TaskSelectionState & {
chatSessionId
:
string
;
setChatSessionId
:
(
value
:
string
)
=>
void
;
initTasks
:
()
=>
Promise
<
void
>
;
selectedOrchestrator
:
OrchestratorName
;
availableOrchestrators
:
OrchestratorName
[];
setSelectedOrchestrator
:
(
value
:
OrchestratorName
)
=>
void
;
switchOrchestrator
:
(
value
:
OrchestratorName
)
=>
void
;
isOrchestratorSelectable
:
boolean
;
orchestratorError
:
string
|
null
;
setTaskRef
:
(
value
:
SelectedTaskRef
|
null
)
=>
void
;
setTaskFile
:
(
fileId
:
string
)
=>
void
;
setTaskId
:
(
taskId
:
string
)
=>
void
;
...
...
@@ -56,13 +67,21 @@ export const getDefaultTaskId = (tasks: Array<{ task_id: string }>): string =>
export
function
TutorSessionProvider
({
children
}:
PropsWithChildren
)
{
const
[
chatSessionId
,
setChatSessionId
]
=
useState
<
string
>
(()
=>
createSessionId
());
const
[
isTaskModeEnabled
,
setIsTaskModeEnabled
]
=
useState
(
false
);
const
[
selectedOrchestrator
,
setSelectedOrchestratorState
]
=
useState
<
OrchestratorName
>
(
"
qa
"
);
const
[
availableOrchestrators
,
setAvailableOrchestrators
]
=
useState
<
OrchestratorName
[]
>
(
()
=>
getFallbackOrchestrators
()
);
const
[
isOrchestratorSelectable
,
setIsOrchestratorSelectable
]
=
useState
(
false
);
const
[
orchestratorError
,
setOrchestratorError
]
=
useState
<
string
|
null
>
(
null
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
const
[
selectedTaskRef
,
setSelectedTaskRef
]
=
useState
<
SelectedTaskRef
|
null
>
(
null
);
const
[
tasksError
,
setTasksError
]
=
useState
<
string
|
null
>
(
null
);
const
[
taskLocked
,
setTaskLocked
]
=
useState
(
false
);
const
isTaskModeEnabled
=
selectedOrchestrator
===
"
task
"
;
const
selectedTask
=
useMemo
<
SelectedTask
|
null
>
(()
=>
{
if
(
!
selectedTaskRef
)
{
return
null
;
...
...
@@ -108,39 +127,50 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
initTasks
=
useCallback
(
async
()
=>
{
setTasksError
(
null
);
setOrchestratorError
(
null
);
try
{
const
orchestratorPayload
=
await
fetchOrchestratorConfig
();
const
available
=
orchestratorPayload
.
available_orchestrators
.
length
?
orchestratorPayload
.
available_orchestrators
:
getFallbackOrchestrators
();
setAvailableOrchestrators
(
available
);
setIsOrchestratorSelectable
(
true
);
setSelectedOrchestratorState
((
prev
)
=>
available
.
includes
(
prev
)
?
prev
:
orchestratorPayload
.
default_orchestrator
);
}
catch
(
error
)
{
setAvailableOrchestrators
(
getFallbackOrchestrators
());
setSelectedOrchestratorState
(
"
qa
"
);
setIsOrchestratorSelectable
(
false
);
setOrchestratorError
(
t
(
"
failedLoadOrchestratorConfig
"
));
void
error
;
}
try
{
const
payload
=
await
fetchTasks
();
const
enabled
=
Boolean
(
payload
.
enabled
);
const
files
=
payload
.
task_files
||
[];
setIsTaskModeEnabled
(
enabled
);
setTaskFiles
(
files
);
if
(
!
enabled
)
{
setSelectedTaskRef
(
null
);
}
else
{
setSelectedTaskRef
((
prev
)
=>
{
if
(
prev
)
{
const
file
=
files
.
find
((
item
)
=>
item
.
file_id
===
prev
.
fileId
);
if
(
file
&&
file
.
tasks
.
some
((
task
)
=>
task
.
task_id
===
prev
.
taskId
))
{
return
prev
;
}
setSelectedTaskRef
((
prev
)
=>
{
if
(
prev
)
{
const
file
=
files
.
find
((
item
)
=>
item
.
file_id
===
prev
.
fileId
);
if
(
file
&&
file
.
tasks
.
some
((
task
)
=>
task
.
task_id
===
prev
.
taskId
))
{
return
prev
;
}
}
const
firstFile
=
files
[
0
];
if
(
!
firstFile
)
{
return
null
;
}
const
defaultTaskId
=
getDefaultTaskId
(
firstFile
.
tasks
||
[]);
if
(
!
defaultTaskId
)
{
return
null
;
}
return
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
});
}
const
firstFile
=
files
[
0
];
if
(
!
firstFile
)
{
return
null
;
}
const
defaultTaskId
=
getDefaultTaskId
(
firstFile
.
tasks
||
[]);
if
(
!
defaultTaskId
)
{
return
null
;
}
return
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
});
}
catch
(
error
)
{
setTasksError
(
t
(
"
failedLoadTasks
"
));
setIsTaskModeEnabled
(
false
);
setTaskFiles
([]);
setSelectedTaskRef
(
null
);
void
error
;
...
...
@@ -153,27 +183,46 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
void
initTasks
();
},
[
initTasks
]);
const
setTaskFile
=
useCallback
((
fileId
:
string
)
=>
{
if
(
!
fileId
)
{
return
;
}
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
});
},
[
taskFiles
]);
const
setTaskFile
=
useCallback
(
(
fileId
:
string
)
=>
{
if
(
!
fileId
)
{
return
;
}
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
});
},
[
taskFiles
]
);
const
setTaskId
=
useCallback
((
taskId
:
string
)
=>
{
if
(
!
taskId
||
!
selectedTaskRef
?.
fileId
)
{
return
;
const
setTaskId
=
useCallback
(
(
taskId
:
string
)
=>
{
if
(
!
taskId
||
!
selectedTaskRef
?.
fileId
)
{
return
;
}
setSelectedTaskRef
({
fileId
:
selectedTaskRef
.
fileId
,
taskId
});
},
[
selectedTaskRef
]
);
const
setSelectedOrchestrator
=
useCallback
((
value
:
OrchestratorName
)
=>
{
setSelectedOrchestratorState
(
value
);
},
[]);
const
switchOrchestrator
=
useCallback
((
value
:
OrchestratorName
)
=>
{
setSelectedOrchestratorState
(
value
);
setChatSessionId
(
createSessionId
());
setTaskLocked
(
false
);
if
(
value
!==
"
task
"
)
{
setSelectedTaskRef
(
null
);
}
setSelectedTaskRef
({
fileId
:
selectedTaskRef
.
fileId
,
taskId
});
},
[
selectedTaskRef
]);
},
[]);
const
lockTask
=
useCallback
(()
=>
{
setTaskLocked
(
true
);
...
...
@@ -193,6 +242,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
chatSessionId
,
setChatSessionId
,
initTasks
,
selectedOrchestrator
,
availableOrchestrators
,
setSelectedOrchestrator
,
switchOrchestrator
,
isOrchestratorSelectable
,
orchestratorError
,
taskFiles
,
selectedTaskRef
,
selectedTask
,
...
...
math-tutor/frontend/src/styles/theme.css
View file @
ff6c3d7f
...
...
@@ -61,6 +61,27 @@ body {
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
8px
;
align-items
:
center
;
}
.orchestrator-select-wrap
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
4px
8px
;
border
:
1px
solid
#d8d1c4
;
border-radius
:
999px
;
background
:
#fef9f0
;
}
.orchestrator-select-label
{
font-size
:
12px
;
color
:
#6f675d
;
}
.orchestrator-select
{
min-width
:
96px
;
padding
:
4px
8px
;
}
.pill
{
...
...
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