Commit 68c41d59 authored by Kantz's avatar Kantz
Browse files

zugang über Link ermöglichen

parent bc316911
......@@ -53,6 +53,23 @@ cd math-tutor/frontend
npm run dev
```
### Task Deep Links
You can open a task chat directly with URL query parameters:
`/chat?orchestrator=task&file_id=<task_file_id>&task_id=<task_id>`
Example:
`http://localhost:5173/chat?orchestrator=task&file_id=analysis_1&task_id=03`
Notes:
- `file_id` must match an existing task file id from the task catalog.
- `task_id` must exist inside that file.
- If the link is invalid, the frontend falls back to `/select-task`.
- The task is not locked by deep link, so users can still switch tasks afterwards.
To make it accessible over the network.
Add the frontend- and backend-adress in the `backend/.env`-file in the frontend- and backend-folder. Use the following command to run the front- and backend.
......
......@@ -70,6 +70,8 @@
"The tutor backend is currently unavailable or still starting up. We will keep trying automatically.",
retryConnection: "Retry now",
lastCheckFailed: "Last check: {detail}",
deepLinkInvalidTask: "Invalid task link. Please choose a task manually.",
deepLinkInitFailed: "Task link initialization failed. Please choose a task manually.",
},
de: {
chats: "Chats",
......@@ -147,6 +149,10 @@
"Das Tutor-Backend ist aktuell nicht erreichbar oder startet noch. Wir versuchen es automatisch weiter.",
retryConnection: "Erneut prüfen",
lastCheckFailed: "Letzte Prüfung: {detail}",
deepLinkInvalidTask:
"Ungültiger Aufgaben-Link. Bitte wähle die Aufgabe manuell aus.",
deepLinkInitFailed:
"Der Aufgaben-Link konnte nicht initialisiert werden. Bitte wähle die Aufgabe manuell aus.",
},
} as const;
......
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import "../styles/theme.css";
import ChatWindow from "../components/Chat/ChatWindow";
import CanvasDrawer from "../components/Canvas/CanvasDrawer";
......@@ -106,11 +106,13 @@ export default function ChatPage() {
.trim()
.toLowerCase() !== "false";
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const {
chatSessionId,
setChatSessionId,
isTaskModeEnabled,
isTasksInitialized,
taskFiles,
selectedTaskRef,
selectedTask,
selectedTaskFile,
......@@ -120,7 +122,6 @@ export default function ChatPage() {
switchOrchestrator,
isOrchestratorSelectable,
orchestratorError,
taskLocked,
lockTask,
unlockTask,
setTaskRef,
......@@ -142,19 +143,125 @@ export default function ChatPage() {
const [sections, setSections] = useState<RetrievedDoc[]>([]);
const [retrievalLoading, setRetrievalLoading] = useState(false);
const [retrievalError, setRetrievalError] = useState<string | null>(null);
const [deepLinkError, setDeepLinkError] = useState<string | null>(null);
const [canvasStatus, setCanvasStatus] = useState<{
kind: "info" | "error" | "ok";
message: string;
} | null>(null);
const processedDeepLinkRef = useRef<string>("");
const deepLinkTarget = useMemo(() => {
const orchestrator = String(searchParams.get("orchestrator") || "")
.trim()
.toLowerCase();
const fileId = String(searchParams.get("file_id") || "").trim();
const rawTaskId = String(searchParams.get("task_id") || "").trim();
const taskId =
/^\d{1,2}$/.test(rawTaskId) && rawTaskId.length < 2
? rawTaskId.padStart(2, "0")
: rawTaskId;
const hasFileId = Boolean(fileId);
const hasTaskId = Boolean(taskId);
const isTaskOrchestrator = orchestrator === "task";
const hasAnyTaskParam = hasFileId || hasTaskId;
const hasRequiredParams = hasFileId && hasTaskId;
const key = `${orchestrator}|${fileId}|${taskId}`;
return {
fileId,
taskId,
isTaskOrchestrator,
hasAnyTaskParam,
hasRequiredParams,
key,
};
}, [searchParams]);
useEffect(() => {
if (!isTasksInitialized) {
return;
}
if (!deepLinkTarget.isTaskOrchestrator || !deepLinkTarget.hasAnyTaskParam) {
return;
}
if (processedDeepLinkRef.current === deepLinkTarget.key) {
return;
}
processedDeepLinkRef.current = deepLinkTarget.key;
if (!deepLinkTarget.hasRequiredParams) {
setDeepLinkError(t("deepLinkInvalidTask"));
setTaskRef(null);
unlockTask();
navigate("/select-task", { replace: true });
return;
}
const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId);
const selectedTask = selectedFile?.tasks.find((task) => task.task_id === deepLinkTarget.taskId);
if (!selectedFile || !selectedTask) {
setDeepLinkError(t("deepLinkInvalidTask"));
setTaskRef(null);
unlockTask();
navigate("/select-task", { replace: true });
return;
}
setSelectedOrchestrator("task");
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
unlockTask();
let cancelled = false;
void (async () => {
try {
await selectTask({
draft: chatSessionId,
fileId: selectedFile.file_id,
taskId: selectedTask.task_id,
});
if (!cancelled) {
setDeepLinkError(null);
}
} catch (error) {
if (!cancelled) {
setDeepLinkError(t("deepLinkInitFailed"));
setTaskRef(null);
unlockTask();
navigate("/select-task", { replace: true });
}
void error;
}
})();
return () => {
cancelled = true;
};
}, [
chatSessionId,
deepLinkTarget,
isTasksInitialized,
navigate,
setSelectedOrchestrator,
setTaskRef,
taskFiles,
unlockTask,
]);
useEffect(() => {
if (!isTasksInitialized || !isTaskModeEnabled) {
return;
}
if (!selectedTaskRef || !taskLocked) {
if (
deepLinkTarget.isTaskOrchestrator &&
deepLinkTarget.hasAnyTaskParam &&
processedDeepLinkRef.current !== deepLinkTarget.key
) {
return;
}
if (!selectedTaskRef) {
navigate("/select-task", { replace: true });
}
}, [isTaskModeEnabled, isTasksInitialized, navigate, selectedTaskRef, taskLocked]);
}, [deepLinkTarget, isTaskModeEnabled, isTasksInitialized, navigate, selectedTaskRef]);
const docIndexes = useMemo(() => {
const bySourceKey: Record<string, RetrievedDoc> = {};
......@@ -709,6 +816,7 @@ export default function ChatPage() {
</div>
</header>
{orchestratorError ? <div className="chat-archive-error">{orchestratorError}</div> : null}
{deepLinkError ? <div className="chat-archive-error">{deepLinkError}</div> : null}
<main className="app-main">
<section className="chat-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