Commit fee48f72 authored by Kantz's avatar Kantz
Browse files

Merge branch 'dev_feedback_mode' into 'main'

Dev feedback mode

See merge request kantz/tutor_react!14
parents 6c805864 691c5d1e
from __future__ import annotations
from app.deterministic_services import llm_client
SYSTEM_PROMPT = """
Du bist ein didaktischer Mathe-Tutor. Du gibts Feedback zu den Lösungen des Nutzers zur Gegebenen Frage.
Vergleiche das Ergebnis mit der korrekten Lösung.
Stelle Nachfragen wenn der Lösungsweg nicht vollständig ist.
Zitiere 1 zu 1 aus der AKTUELLE Eingabe des Nutzers wenn du auf Fehler aufmerksam machst.
verwende die $ für mathematische Formeln, z.B. $a^2 + b^2 = c^2$
Halte dich kurz und präzise.
Denke nach bevor du eine Ausgabe macht.
"""
def generate_hint(
query: str | None,
task: str,
hints: list[str],
solution: str,
history: list[dict] | None = None,
sources: str | None = None,
) -> str:
context_parts = [
f"Hier ist die zu lösende Aufgaben:{task}\n",
f"Hier ist eine korrekte Lösung als Referenz:{solution}\n",
f"Hier ist ein exemplarischer Lösungsweg:{hints}\n"
]
if sources:
context_parts.append(f"Kontext/Sources:\n{sources}")
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
# Kompakter Kontext als eine Nachricht (kein langer Fließtext mit History mischen)
messages.append({"role": "user", "content": "\n\n".join(context_parts)})
# History als echte Turns (und ggf. begrenzen, siehe Punkt 2)
if history:
messages.extend(history)
# Aktuelle Frage als letzte Nachricht, fett hervorgehoben durch Struktur/Delimiters
messages.append({
"role": "user",
"content": f"AKTUELLE Eingabe des Studenten (höchste Priorität):\n{query}"
})
result = llm_client.chat(messages=messages)
return llm_client.get_message_content(result)
......@@ -75,7 +75,7 @@ def chat(request: ChatRequest) -> ChatResponse:
try:
payload_messages = [{"role": m.role, "content": m.text} for m in request.messages]
if orchestrator_name == "task":
if orchestrator_name in {"task", "feedback"}:
result = orchestrator_impl.run_chat(
payload_messages,
draft=request.draft,
......
......@@ -10,6 +10,8 @@ from app.deterministic_services import context_store, task_catalog
router = APIRouter()
TASK_ORCHESTRATORS = {"task", "feedback"}
class TaskItem(BaseModel):
task_id: str
......@@ -46,7 +48,7 @@ class SelectTaskResponse(BaseModel):
@router.get("/api/tasks/config")
def get_task_config() -> dict[str, object]:
orchestrator = config.get_orchestrator()
return {"orchestrator": orchestrator, "enabled": orchestrator == "task"}
return {"orchestrator": orchestrator, "enabled": orchestrator in TASK_ORCHESTRATORS}
@router.get("/api/tasks", response_model=TasksResponse)
......@@ -55,7 +57,7 @@ def list_tasks() -> TasksResponse:
task_files = task_catalog.build_task_catalog()
return TasksResponse(
orchestrator=orchestrator,
enabled=orchestrator == "task",
enabled=orchestrator in TASK_ORCHESTRATORS,
task_files=task_files,
)
......
from __future__ import annotations
from app.LLM_services import feedback_LLM
import app.config as config
from app.deterministic_services import (
context_store,
retrieval_store,
task_catalog,
)
from app.deterministic_services.orchestrators import orchestrator_base as base
def _ensure_context_task_fields(state: base.ChatState, query_text: str) -> tuple[str, str] | None:
store_new = context_store.context_store_new
has_task = bool(store_new.get_task(state.sheet))
has_hints = bool(store_new.get_hints(state.sheet))
has_solution = bool(store_new.get_solution(state.sheet))
if has_task and has_hints and has_solution:
selected = task_catalog.get_selected_task_ids(state.sheet)
if selected[0] and selected[1]:
was_selected = task_catalog.select_task_by_ids(
state.sheet,
selected[0],
selected[1],
)
if was_selected:
return selected[0], selected[1]
sources_text = "\n".join([source.to_string() for source in context_store.get_retrieval(state.sheet)])
selection = task_catalog.select_task_for_context(
state.sheet,
query_text=query_text,
sources_text=sources_text,
history=context_store.get_history_turns(state.sheet),
)
if not selection:
return None
task_file, task_entry = selection
selected_file_id = str(task_file.get("_file_id", ""))
selected_task_id = str(task_entry.get("id", "")).zfill(2)
base.append_tool_log(
state.tool_log,
"task_json_selected",
{"tasks_dir": str(task_catalog.TASKS_DIR)},
{
"file": task_file.get("_path", ""),
"file_id": selected_file_id,
"task_id": selected_task_id,
"hint_count": len(store_new.get_hints(state.sheet)),
"has_solution": bool(store_new.get_solution(state.sheet)),
},
)
return selected_file_id, selected_task_id
def _retrieve_context_for_task(state: base.ChatState, query_text: str) -> int:
refs = task_catalog.get_selected_task_subsection_refs(state.sheet)
if not refs:
return 0
def _retrieve() -> dict:
sources = retrieval_store.retrieve_for_subsections(
pg_url=config.get_postgres_url(),
subsection_refs=refs,
)
context_store.update_retrieval_context(state.sheet, sources)
return {
"subsection_refs": refs,
"source_count": len(sources),
}
result = base.log_timed_call(
state.tool_log,
"retrieve_context_with_task_subsections",
{
"query": query_text,
"subsection_refs": refs,
},
_retrieve,
)
return int(result.get("source_count", 0))
def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
source_count = _retrieve_context_for_task(state, query_text)
print(f"_retrieve_context_for_task source_count={source_count}")
if source_count > 0:
_ensure_context_task_fields(state, query_text)
task_text = context_store.context_store_new.get_task(state.sheet).strip()
retrieval_query = query_text
if task_text:
retrieval_query = f"Aufgabe:\n{task_text}\n\n{query_text}"
base.bootstrap_retrieval(state.sheet, retrieval_query, state.tool_log)
def _on_turn_logic(state: base.ChatState) -> None:
_ensure_context_task_fields(state, state.last_user)
def _on_build_reply(state: base.ChatState) -> str | None:
store_new = context_store.context_store_new
history_turns = context_store.get_history_turns(state.sheet)
args = {
"query": state.last_user if not state.new_chat else None,
"task": store_new.get_task(state.sheet),
"hints": store_new.get_hints(state.sheet),
"solution": store_new.get_solution(state.sheet),
"history": history_turns,
"sources": "\n".join([source.to_string() for source in context_store.get_retrieval(state.sheet)]),
}
return base.log_timed_call(
state.tool_log,
"new_generate_feedback",
args,
lambda: feedback_LLM.generate_hint(**args),
)
def run_chat(
messages: list[dict],
draft: str | None = None,
selected_task: dict | None = None,
) -> dict:
def _apply_selected_task(state: base.ChatState) -> None:
if not selected_task:
return
selected_file_id = str(selected_task.get("file_id", "")).strip()
selected_task_id = str(selected_task.get("task_id", "")).strip()
if selected_file_id and selected_task_id:
task_catalog.select_task_by_ids(
state.sheet,
selected_file_id,
selected_task_id,
)
def on_bootstrap(state: base.ChatState, query_text: str) -> None:
_apply_selected_task(state)
_on_bootstrap(state, query_text)
def on_turn_logic(state: base.ChatState) -> None:
_apply_selected_task(state)
_on_turn_logic(state)
return base.run_chat_common(
messages=messages,
draft=draft,
on_bootstrap=on_bootstrap,
on_turn_logic=on_turn_logic,
on_build_reply=_on_build_reply,
)
......@@ -4,17 +4,19 @@ from typing import Any
import app.config as config
from app.deterministic_services.orchestrators import (
orchestrator_feedback,
orchestrator_qa,
orchestrator_task,
orchestrator_tutor,
)
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task")
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "feedback")
_ORCHESTRATOR_MODULES: dict[str, Any] = {
"qa": orchestrator_qa,
"tutor": orchestrator_tutor,
"task": orchestrator_task,
"feedback": orchestrator_feedback,
}
......
......@@ -8,6 +8,9 @@ services:
expose:
- "8000"
restart: unless-stopped
volumes:
- ./logs:/app/logs
frontend:
build:
......
export type OrchestratorName = "qa" | "tutor" | "task";
export type OrchestratorName = "qa" | "tutor" | "task" | "feedback";
export type OrchestratorConfigResponse = {
default_orchestrator: OrchestratorName;
available_orchestrators: OrchestratorName[];
};
const FALLBACK_ORCHESTRATORS: OrchestratorName[] = ["qa", "tutor", "task"];
const FALLBACK_ORCHESTRATORS: OrchestratorName[] = ["qa", "tutor", "task", "feedback"];
const normalizeOrchestrator = (value: string): OrchestratorName | null => {
if (value === "qa" || value === "tutor" || value === "task") {
if (value === "qa" || value === "tutor" || value === "task" || value === "feedback") {
return value;
}
return null;
......
......@@ -9,6 +9,7 @@ type ChatWindowProps = {
draft: string;
onDraftChange: (value: string) => void;
onSend: () => void;
onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void;
onInspectDoc?: (doc: RetrievedDoc) => void;
docIndex?: Record<string, RetrievedDoc>;
......@@ -20,6 +21,7 @@ export default function ChatWindow({
draft,
onDraftChange,
onSend,
onHistoryNavigate,
onToggleCanvas,
onInspectDoc,
docIndex,
......@@ -38,6 +40,7 @@ export default function ChatWindow({
value={draft}
onChange={onDraftChange}
onSend={onSend}
onHistoryNavigate={onHistoryNavigate}
onToggleCanvas={onToggleCanvas}
/>
</div>
......
......@@ -29,7 +29,104 @@ export default function MessageBubble({
}: MessageBubbleProps) {
const bubbleRef = useRef<HTMLDivElement | null>(null);
const [isRawView, setIsRawView] = useState(false);
const renderedText = useMemo(() => escapeAsterisksInsideMath(text), [text]);
const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
const { thinkingText, answerText } = useMemo(() => {
const parts = text.split("</think>");
if (role !== "assistant" || parts.length < 2) {
return {
thinkingText: "",
answerText: escapeAsterisksInsideMath(text),
};
}
const rawThinking = parts.slice(0, -1).join("</think>").trim();
const rawAnswer = parts[parts.length - 1].trim();
return {
thinkingText: escapeAsterisksInsideMath(rawThinking),
answerText: escapeAsterisksInsideMath(rawAnswer),
};
}, [role, text]);
const hasThinking = Boolean(thinkingText);
useEffect(() => {
setIsThinkingExpanded(false);
}, [text]);
const renderMarkdown = (value: string) => (
<ReactMarkdown
urlTransform={(url) => {
if (url.startsWith("doc://")) {
return url;
}
return defaultUrlTransform(url);
}}
components={{
a: ({ href, children, ...props }) => {
const target = href || "";
if (!target.startsWith("doc://")) {
const mdMatch = target.match(/([^/]+)\.md$/i);
if (!mdMatch) {
return (
<a href={href} target="_blank" rel="noreferrer" {...props}>
{children}
</a>
);
}
const file = mdMatch[1] || "";
const slugFromFile = file
.toLowerCase()
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const slug = slugFromFile.split("-").pop() || slugFromFile;
const doc = docSlugIndex ? docSlugIndex[slug] : undefined;
return (
<a
href={href}
onClick={(event) => {
if (!doc || !onInspectDoc) {
return;
}
event.preventDefault();
onInspectDoc(doc);
}}
{...props}
>
{children}
</a>
);
}
const key = decodeURIComponent(target.slice("doc://".length));
const doc = docIndex ? docIndex[key] : undefined;
return (
<a
href={href}
onClick={(event) => {
if (!doc || !onInspectDoc) {
return;
}
event.preventDefault();
onInspectDoc(doc);
}}
{...props}
>
{children}
</a>
);
},
}}
>
{value}
</ReactMarkdown>
);
useEffect(() => {
if (isRawView) {
......@@ -52,7 +149,7 @@ export default function MessageBubble({
script.addEventListener("load", typeset);
return () => script.removeEventListener("load", typeset);
}
}, [text, isRawView]);
}, [answerText, isRawView, isThinkingExpanded, thinkingText]);
return (
<div className={`message-bubble ${role}`} ref={bubbleRef}>
......@@ -74,82 +171,24 @@ export default function MessageBubble({
{isRawView ? (
<pre className="message-raw-text">{text}</pre>
) : (
<ReactMarkdown
urlTransform={(url) => {
if (url.startsWith("doc://")) {
return url;
}
return defaultUrlTransform(url);
}}
components={{
a: ({ href, children, ...props }) => {
const target = href || "";
if (!target.startsWith("doc://")) {
const mdMatch = target.match(/([^/]+)\.md$/i);
if (!mdMatch) {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
{...props}
>
{children}
</a>
);
}
const file = mdMatch[1] || "";
const slugFromFile = file
.toLowerCase()
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const slug = slugFromFile.split("-").pop() || slugFromFile;
const doc = docSlugIndex ? docSlugIndex[slug] : undefined;
return (
<a
href={href}
onClick={(event) => {
if (!doc || !onInspectDoc) {
return;
}
event.preventDefault();
onInspectDoc(doc);
}}
{...props}
>
{children}
</a>
);
}
const key = decodeURIComponent(target.slice("doc://".length));
const doc = docIndex ? docIndex[key] : undefined;
return (
<a
href={href}
onClick={(event) => {
if (!doc || !onInspectDoc) {
return;
}
event.preventDefault();
onInspectDoc(doc);
}}
{...props}
>
{children}
</a>
);
},
}}
>
{renderedText}
</ReactMarkdown>
<>
{hasThinking ? (
<div className="message-thinking">
<button
type="button"
className="message-thinking-toggle"
aria-expanded={isThinkingExpanded}
onClick={() => setIsThinkingExpanded((prev) => !prev)}
>
{isThinkingExpanded ? t("hideThinking") : t("showThinking")}
</button>
{isThinkingExpanded ? (
<div className="message-thinking-content">{renderMarkdown(thinkingText)}</div>
) : null}
</div>
) : null}
<div className="message-answer">{renderMarkdown(answerText)}</div>
</>
)}
</div>
</div>
......
......@@ -5,6 +5,7 @@ type MessageInputProps = {
value: string;
onChange: (value: string) => void;
onSend: () => void;
onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void;
};
......@@ -12,6 +13,7 @@ export default function MessageInput({
value,
onChange,
onSend,
onHistoryNavigate,
onToggleCanvas,
}: MessageInputProps) {
const canSend = value.trim().length > 0;
......@@ -46,6 +48,19 @@ export default function MessageInput({
value={value}
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
const isCursorAtStart =
event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;
if (event.key === "ArrowUp" && isCursorAtStart && onHistoryNavigate?.("older")) {
event.preventDefault();
return;
}
if (event.key === "ArrowDown" && isCursorAtStart && onHistoryNavigate?.("newer")) {
event.preventDefault();
return;
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
onSend();
......
......@@ -13,11 +13,13 @@ type ModeMeta = {
labelKey:
| "orchestratorModeQaLabel"
| "orchestratorModeTutorLabel"
| "orchestratorModeTaskLabel";
| "orchestratorModeTaskLabel"
| "orchestratorModeFeedbackLabel";
descriptionKey:
| "orchestratorModeQaDescription"
| "orchestratorModeTutorDescription"
| "orchestratorModeTaskDescription";
| "orchestratorModeTaskDescription"
| "orchestratorModeFeedbackDescription";
};
const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
......@@ -33,6 +35,10 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
labelKey: "orchestratorModeTaskLabel",
descriptionKey: "orchestratorModeTaskDescription",
},
feedback: {
labelKey: "orchestratorModeFeedbackLabel",
descriptionKey: "orchestratorModeFeedbackDescription",
},
};
type ModeDescriptionDisplay = "tooltip" | "helperText" | "optionSuffix";
......
......@@ -20,10 +20,12 @@
orchestratorModeQaLabel: "QA",
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeDescriptionTitle: "Mode help",
orchestratorModeQaDescription: "Direct answers based on the script",
orchestratorModeTutorDescription: "Help with your own questions",
orchestratorModeTaskDescription: "Help with textbook exercises",
orchestratorModeFeedbackDescription: "Short feedback on your solution for textbook exercises",
directChildren: "Direct children",
taskChildren: "Task sources",
indirectChildren: "Indirect children",
......@@ -38,6 +40,8 @@
roleAssistant: "assistant",
showSource: "Show source",
showRendered: "Show rendered",
showThinking: "Show thinking",
hideThinking: "Hide thinking",
noTasksAvailable: "No tasks available",
noTaskSelected: "No task selected.",
savedChats: "Saved Chats",
......@@ -95,10 +99,12 @@
orchestratorModeQaLabel: "QA",
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeDescriptionTitle: "Modus-Hilfe",
orchestratorModeQaDescription: "Direkte Antworten basierend auf dem Skript",
orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen",
orchestratorModeTaskDescription: "Hilfe bei Aufgaben aus dem Lehrwerk",
orchestratorModeFeedbackDescription: "Kurzes Feedback zu deiner Lösung bei Aufgaben aus dem Lehrwerk",
directChildren: "Direkte Quellen",
taskChildren: "Aufgaben-Quellen",
indirectChildren: "Indirekte Quellen",
......@@ -113,6 +119,8 @@
roleAssistant: "assistent",
showSource: "Formeltext anzeigen",
showRendered: "Gerendert anzeigen",
showThinking: "Thinking anzeigen",
hideThinking: "Thinking ausblenden",
noTasksAvailable: "Keine Aufgaben verfügbar",
noTaskSelected: "Keine Aufgabe ausgewählt.",
savedChats: "Gespeicherte Chats",
......
......@@ -17,12 +17,15 @@ import sumintLogo from "../../SuMINT-Logo.png";
const initialMessages: ChatMessage[] = [];
const normalizeOrchestrator = (value: string | null | undefined): OrchestratorName | null => {
if (value === "qa" || value === "tutor" || value === "task") {
if (value === "qa" || value === "tutor" || value === "task" || value === "feedback") {
return value;
}
return null;
};
const isTaskCoupledOrchestrator = (value: string | null | undefined): boolean =>
value === "task" || value === "feedback";
type ArchivedChatSummary = {
chat_id: string;
saved_at: string;
......@@ -131,6 +134,8 @@ export default function ChatPage() {
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
const [draft, setDraft] = useState("");
const [draftHistoryIndex, setDraftHistoryIndex] = useState<number | null>(null);
const [draftBeforeHistory, setDraftBeforeHistory] = useState<string | null>(null);
const [isArchiving, setIsArchiving] = useState(false);
const [archivedChats, setArchivedChats] = useState<ArchivedChatSummary[]>([]);
const [selectedArchiveId, setSelectedArchiveId] = useState("");
......@@ -163,7 +168,7 @@ export default function ChatPage() {
: rawTaskId;
const hasFileId = Boolean(fileId);
const hasTaskId = Boolean(taskId);
const isTaskOrchestrator = orchestrator === "task";
const isTaskOrchestrator = isTaskCoupledOrchestrator(orchestrator);
const hasAnyTaskParam = hasFileId || hasTaskId;
const hasRequiredParams = hasFileId && hasTaskId;
const key = `${orchestrator}|${fileId}|${taskId}`;
......@@ -208,7 +213,11 @@ export default function ChatPage() {
return;
}
setSelectedOrchestrator("task");
setSelectedOrchestrator(
deepLinkTarget.isTaskOrchestrator && searchParams.get("orchestrator") === "feedback"
? "feedback"
: "task"
);
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
unlockTask();
......@@ -338,6 +347,58 @@ export default function ChatPage() {
return { fileId: selectedTaskFile.file_id, taskId: previousTask.task_id };
}, [selectedTaskFile, selectedTaskRef]);
const userMessageHistory = useMemo(
() => messages.filter((message) => message.role === "user").map((message) => message.text),
[messages]
);
const resetDraftHistoryNavigation = () => {
setDraftHistoryIndex(null);
setDraftBeforeHistory(null);
};
const handleDraftChange = (value: string) => {
if (draftHistoryIndex !== null) {
resetDraftHistoryNavigation();
}
setDraft(value);
};
const handleHistoryNavigate = (direction: "older" | "newer") => {
if (!userMessageHistory.length) {
return false;
}
if (direction === "older") {
const nextIndex =
draftHistoryIndex === null
? userMessageHistory.length - 1
: Math.max(0, draftHistoryIndex - 1);
if (draftHistoryIndex === null) {
setDraftBeforeHistory(draft);
}
setDraftHistoryIndex(nextIndex);
setDraft(userMessageHistory[nextIndex]);
return true;
}
if (draftHistoryIndex === null) {
return false;
}
const nextIndex = draftHistoryIndex + 1;
if (nextIndex >= userMessageHistory.length) {
setDraft(draftBeforeHistory ?? "");
resetDraftHistoryNavigation();
return true;
}
setDraftHistoryIndex(nextIndex);
setDraft(userMessageHistory[nextIndex]);
return true;
};
const handleSend = async () => {
const trimmed = draft.trim();
if (!trimmed) {
......@@ -352,6 +413,7 @@ export default function ChatPage() {
setMessages((prev) => [...prev, userMessage]);
setDraft("");
resetDraftHistoryNavigation();
setRetrievalLoading(true);
setRetrievalError(null);
......@@ -484,6 +546,7 @@ export default function ChatPage() {
const resetChatState = () => {
setMessages(initialMessages);
setDraft("");
resetDraftHistoryNavigation();
setDirectChildren([]);
setTaskChildren([]);
setIndirectChildren([]);
......@@ -536,7 +599,7 @@ export default function ChatPage() {
: selectedOrchestrator;
setSelectedOrchestrator(nextOrchestrator);
if (nextOrchestrator === "task") {
if (isTaskCoupledOrchestrator(nextOrchestrator)) {
const restoredTask = payload.selected_task;
if (restoredTask?.file_id && restoredTask?.task_id) {
setTaskRef({
......@@ -671,7 +734,7 @@ export default function ChatPage() {
}
resetChatState();
switchOrchestrator(next);
navigate(next === "task" ? "/select-task" : "/chat");
navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat");
};
const handleCanvasSave = async (dataUrl: string) => {
......@@ -824,8 +887,9 @@ export default function ChatPage() {
<ChatWindow
messages={messages}
draft={draft}
onDraftChange={setDraft}
onDraftChange={handleDraftChange}
onSend={handleSend}
onHistoryNavigate={handleHistoryNavigate}
onToggleCanvas={handleToggleCanvas}
onInspectDoc={handleInspectDoc}
docIndex={docIndexes.bySourceKey}
......
......@@ -8,6 +8,9 @@ import { useTutorSession } from "../state/tutorSession";
import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png";
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
export default function TaskSelectionPage() {
const navigate = useNavigate();
const {
......@@ -86,7 +89,7 @@ export default function TaskSelectionPage() {
return;
}
switchOrchestrator(next);
navigate(next === "task" ? "/select-task" : "/chat", { replace: true });
navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat", { replace: true });
};
if (!isTasksInitialized) {
......
......@@ -68,6 +68,9 @@ export const getDefaultTaskId = (tasks: Array<{ task_id: string }>): string =>
const isSelectableTaskFile = (file: TaskFile): boolean =>
Array.isArray(file.subsections) && file.subsections.length > 0;
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
export function TutorSessionProvider({ children }: PropsWithChildren) {
const [chatSessionId, setChatSessionId] = useState<string>(() => createSessionId());
const [selectedOrchestrator, setSelectedOrchestratorState] =
......@@ -83,7 +86,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const [tasksError, setTasksError] = useState<string | null>(null);
const [taskLocked, setTaskLocked] = useState(false);
const isTaskModeEnabled = selectedOrchestrator === "task";
const isTaskModeEnabled = isTaskCoupledOrchestrator(selectedOrchestrator);
const selectedTask = useMemo<SelectedTask | null>(() => {
if (!selectedTaskRef) {
......@@ -228,7 +231,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedOrchestratorState(value);
setChatSessionId(createSessionId());
setTaskLocked(false);
if (value !== "task") {
if (!isTaskCoupledOrchestrator(value)) {
setSelectedTaskRef(null);
}
}, []);
......
......@@ -466,6 +466,34 @@ body {
overflow-wrap: anywhere;
}
.message-thinking {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px dashed #d8d1c4;
}
.message-thinking-toggle {
border: 1px solid #d8d1c4;
background: #f6f0e4;
color: #6f675d;
border-radius: 999px;
padding: 4px 10px;
font-size: 11px;
cursor: pointer;
}
.message-thinking-toggle:hover {
background: #efe6d4;
}
.message-thinking-content {
margin-top: 10px;
padding: 10px 12px;
border-radius: 10px;
background: #fffaf0;
border: 1px solid #e2ded5;
}
.composer {
display: flex;
flex-direction: column;
......
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