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 ...@@ -53,6 +53,23 @@ cd math-tutor/frontend
npm run dev 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. 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. 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 @@ ...@@ -70,6 +70,8 @@
"The tutor backend is currently unavailable or still starting up. We will keep trying automatically.", "The tutor backend is currently unavailable or still starting up. We will keep trying automatically.",
retryConnection: "Retry now", retryConnection: "Retry now",
lastCheckFailed: "Last check: {detail}", lastCheckFailed: "Last check: {detail}",
deepLinkInvalidTask: "Invalid task link. Please choose a task manually.",
deepLinkInitFailed: "Task link initialization failed. Please choose a task manually.",
}, },
de: { de: {
chats: "Chats", chats: "Chats",
...@@ -147,6 +149,10 @@ ...@@ -147,6 +149,10 @@
"Das Tutor-Backend ist aktuell nicht erreichbar oder startet noch. Wir versuchen es automatisch weiter.", "Das Tutor-Backend ist aktuell nicht erreichbar oder startet noch. Wir versuchen es automatisch weiter.",
retryConnection: "Erneut prüfen", retryConnection: "Erneut prüfen",
lastCheckFailed: "Letzte Prüfung: {detail}", 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; } as const;
......
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
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";
...@@ -106,11 +106,13 @@ export default function ChatPage() { ...@@ -106,11 +106,13 @@ export default function ChatPage() {
.trim() .trim()
.toLowerCase() !== "false"; .toLowerCase() !== "false";
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { const {
chatSessionId, chatSessionId,
setChatSessionId, setChatSessionId,
isTaskModeEnabled, isTaskModeEnabled,
isTasksInitialized, isTasksInitialized,
taskFiles,
selectedTaskRef, selectedTaskRef,
selectedTask, selectedTask,
selectedTaskFile, selectedTaskFile,
...@@ -120,7 +122,6 @@ export default function ChatPage() { ...@@ -120,7 +122,6 @@ export default function ChatPage() {
switchOrchestrator, switchOrchestrator,
isOrchestratorSelectable, isOrchestratorSelectable,
orchestratorError, orchestratorError,
taskLocked,
lockTask, lockTask,
unlockTask, unlockTask,
setTaskRef, setTaskRef,
...@@ -142,19 +143,125 @@ export default function ChatPage() { ...@@ -142,19 +143,125 @@ export default function ChatPage() {
const [sections, setSections] = useState<RetrievedDoc[]>([]); const [sections, setSections] = useState<RetrievedDoc[]>([]);
const [retrievalLoading, setRetrievalLoading] = useState(false); const [retrievalLoading, setRetrievalLoading] = useState(false);
const [retrievalError, setRetrievalError] = useState<string | null>(null); const [retrievalError, setRetrievalError] = useState<string | null>(null);
const [deepLinkError, setDeepLinkError] = useState<string | null>(null);
const [canvasStatus, setCanvasStatus] = useState<{ const [canvasStatus, setCanvasStatus] = useState<{
kind: "info" | "error" | "ok"; kind: "info" | "error" | "ok";
message: string; message: string;
} | null>(null); } | 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(() => { useEffect(() => {
if (!isTasksInitialized || !isTaskModeEnabled) { if (!isTasksInitialized || !isTaskModeEnabled) {
return; return;
} }
if (!selectedTaskRef || !taskLocked) { if (
deepLinkTarget.isTaskOrchestrator &&
deepLinkTarget.hasAnyTaskParam &&
processedDeepLinkRef.current !== deepLinkTarget.key
) {
return;
}
if (!selectedTaskRef) {
navigate("/select-task", { replace: true }); navigate("/select-task", { replace: true });
} }
}, [isTaskModeEnabled, isTasksInitialized, navigate, selectedTaskRef, taskLocked]); }, [deepLinkTarget, isTaskModeEnabled, isTasksInitialized, navigate, selectedTaskRef]);
const docIndexes = useMemo(() => { const docIndexes = useMemo(() => {
const bySourceKey: Record<string, RetrievedDoc> = {}; const bySourceKey: Record<string, RetrievedDoc> = {};
...@@ -709,6 +816,7 @@ export default function ChatPage() { ...@@ -709,6 +816,7 @@ export default function ChatPage() {
</div> </div>
</header> </header>
{orchestratorError ? <div className="chat-archive-error">{orchestratorError}</div> : null} {orchestratorError ? <div className="chat-archive-error">{orchestratorError}</div> : null}
{deepLinkError ? <div className="chat-archive-error">{deepLinkError}</div> : null}
<main className="app-main"> <main className="app-main">
<section className="chat-column"> <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