Commit f35d87d3 authored by Kantz's avatar Kantz
Browse files

deeplinks für Sokrates hinzugefuegt

parent 7a683fea
...@@ -75,19 +75,19 @@ Notes: ...@@ -75,19 +75,19 @@ Notes:
You can open a socratic chat directly with URL query parameters: You can open a socratic chat directly with URL query parameters:
`/chat?orchestrator=socratic&subsection_key=<subsection_key>` `/chat?orchestrator=socratic&topic_key=<topic_key>`
Example: Example:
`http://localhost:5173/chat?orchestrator=socratic&subsection_key=mengen` `http://localhost:5173/chat?orchestrator=socratic&topic_key=klammerrechnung`
Notes: Notes:
- `subsection_key` must match an existing subsection entry from the task catalog. - `topic_key` must match an existing socratic topic from the task catalog.
- The links use hyphenated subsection slugs in the URL, while the app resolves them back to the catalog key. - The links use normalized topic keys in the URL, and the app resolves them back to the catalog key.
- The fallback `/select-socratic` page only shows the subsection dropdown and start button. - The fallback `/select-socratic` page only shows the topic dropdown and start button.
- If the link is invalid, the frontend falls back to `/select-socratic`. - If the link is invalid, the frontend falls back to `/select-socratic`.
- The socratic dialog is not locked by deep link, so users can still switch subsections afterwards. - The socratic dialog is not locked by deep link, so users can still switch topics 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.
......
...@@ -226,11 +226,13 @@ export default function ChatPage() { ...@@ -226,11 +226,13 @@ export default function ChatPage() {
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 [deepLinkError, setDeepLinkError] = useState<string | null>(null);
const [deepLinkRevision, setDeepLinkRevision] = useState(0);
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 processedDeepLinkRef = useRef<string>("");
const pendingDeepLinkRef = useRef<string>("");
const socraticBootstrapRef = useRef<string>(""); const socraticBootstrapRef = useRef<string>("");
const deepLinkTarget = useMemo(() => { const deepLinkTarget = useMemo(() => {
...@@ -277,10 +279,13 @@ export default function ChatPage() { ...@@ -277,10 +279,13 @@ export default function ChatPage() {
if ((!deepLinkTarget.isTaskOrchestrator && !deepLinkTarget.isSocratic) || !deepLinkTarget.hasAnyTaskParam) { if ((!deepLinkTarget.isTaskOrchestrator && !deepLinkTarget.isSocratic) || !deepLinkTarget.hasAnyTaskParam) {
return; return;
} }
if (processedDeepLinkRef.current === deepLinkTarget.key) { if (
processedDeepLinkRef.current === deepLinkTarget.key ||
pendingDeepLinkRef.current === deepLinkTarget.key
) {
return; return;
} }
processedDeepLinkRef.current = deepLinkTarget.key; pendingDeepLinkRef.current = deepLinkTarget.key;
const targetRoute = getSelectionRouteForOrchestrator( const targetRoute = getSelectionRouteForOrchestrator(
searchParams.get("orchestrator") === "socratic" ? "socratic" : "task" searchParams.get("orchestrator") === "socratic" ? "socratic" : "task"
...@@ -293,6 +298,7 @@ export default function ChatPage() { ...@@ -293,6 +298,7 @@ export default function ChatPage() {
setDeepLinkError( setDeepLinkError(
deepLinkTarget.isSocratic ? t("deepLinkInvalidTopic") : t("deepLinkInvalidTask") deepLinkTarget.isSocratic ? t("deepLinkInvalidTopic") : t("deepLinkInvalidTask")
); );
pendingDeepLinkRef.current = "";
setTaskRef(null); setTaskRef(null);
setTopicRef(null); setTopicRef(null);
unlockTask(); unlockTask();
...@@ -308,6 +314,7 @@ export default function ChatPage() { ...@@ -308,6 +314,7 @@ export default function ChatPage() {
const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId); const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId);
if (!selectedFile) { if (!selectedFile) {
setDeepLinkError(t("deepLinkInvalidTask")); setDeepLinkError(t("deepLinkInvalidTask"));
pendingDeepLinkRef.current = "";
setTaskRef(null); setTaskRef(null);
setTopicRef(null); setTopicRef(null);
unlockTask(); unlockTask();
...@@ -317,6 +324,7 @@ export default function ChatPage() { ...@@ -317,6 +324,7 @@ export default function ChatPage() {
const selectedTask = selectedFile.tasks.find((task) => task.task_id === deepLinkTarget.taskId); const selectedTask = selectedFile.tasks.find((task) => task.task_id === deepLinkTarget.taskId);
if (!selectedTask) { if (!selectedTask) {
setDeepLinkError(t("deepLinkInvalidTask")); setDeepLinkError(t("deepLinkInvalidTask"));
pendingDeepLinkRef.current = "";
setTaskRef(null); setTaskRef(null);
setTopicRef(null); setTopicRef(null);
unlockTask(); unlockTask();
...@@ -328,15 +336,32 @@ export default function ChatPage() { ...@@ -328,15 +336,32 @@ export default function ChatPage() {
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id }); setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
} }
unlockTask(); unlockTask();
const targetSessionId = createSessionId();
setChatSessionId(targetSessionId);
setMessages(initialMessages);
setDraft("");
resetDraftHistoryNavigation();
setIsSending(false);
setDirectChildren([]);
setTaskChildren([]);
setIndirectChildren([]);
setSubsections([]);
setSubsubsections([]);
setSections([]);
setRetrievalLoading(false);
setRetrievalError(null);
setCanvasStatus(null);
setIsCanvasVisible(false);
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
try { try {
if (deepLinkTarget.isSocratic) { if (deepLinkTarget.isSocratic) {
await selectTopic({ const selected = await selectTopic({
draft: chatSessionId, draft: targetSessionId,
topicKey: deepLinkTarget.topicKey, topicKey: deepLinkTarget.topicKey,
}); });
setTopicRef({ topicKey: selected.topic_key });
} else { } else {
const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId); const selectedFile = taskFiles.find((file) => file.file_id === deepLinkTarget.fileId);
const selectedTask = selectedFile?.tasks.find((task) => task.task_id === deepLinkTarget.taskId); const selectedTask = selectedFile?.tasks.find((task) => task.task_id === deepLinkTarget.taskId);
...@@ -344,16 +369,20 @@ export default function ChatPage() { ...@@ -344,16 +369,20 @@ export default function ChatPage() {
throw new Error("invalid task deep link"); throw new Error("invalid task deep link");
} }
await selectTask({ await selectTask({
draft: chatSessionId, draft: targetSessionId,
fileId: selectedFile.file_id, fileId: selectedFile.file_id,
taskId: selectedTask.task_id, taskId: selectedTask.task_id,
}); });
} }
if (!cancelled) { if (!cancelled) {
processedDeepLinkRef.current = deepLinkTarget.key;
pendingDeepLinkRef.current = "";
setDeepLinkRevision((value) => value + 1);
setDeepLinkError(null); setDeepLinkError(null);
} }
} catch (error) { } catch (error) {
if (!cancelled) { if (!cancelled) {
pendingDeepLinkRef.current = "";
setDeepLinkError( setDeepLinkError(
deepLinkTarget.isSocratic ? t("deepLinkInitFailedTopic") : t("deepLinkInitFailed") deepLinkTarget.isSocratic ? t("deepLinkInitFailedTopic") : t("deepLinkInitFailed")
); );
...@@ -375,6 +404,7 @@ export default function ChatPage() { ...@@ -375,6 +404,7 @@ export default function ChatPage() {
isTasksInitialized, isTasksInitialized,
navigate, navigate,
searchParams, searchParams,
setChatSessionId,
setSelectedOrchestrator, setSelectedOrchestrator,
setTaskRef, setTaskRef,
setTopicRef, setTopicRef,
...@@ -383,16 +413,31 @@ export default function ChatPage() { ...@@ -383,16 +413,31 @@ export default function ChatPage() {
]); ]);
useEffect(() => { useEffect(() => {
const bootstrapTopicKey =
deepLinkTarget.isSocratic && deepLinkTarget.hasRequiredTopicParams
? deepLinkTarget.topicKey
: selectedTopicRef?.topicKey || "";
if ( if (
!isTasksInitialized || !isTasksInitialized ||
selectedOrchestrator !== "socratic" || selectedOrchestrator !== "socratic" ||
!selectedTopicRef || !bootstrapTopicKey ||
messages.length > 0 messages.length > 0
) { ) {
return; return;
} }
if (
deepLinkTarget.isSocratic &&
deepLinkTarget.hasRequiredTopicParams &&
(
pendingDeepLinkRef.current === deepLinkTarget.key ||
processedDeepLinkRef.current !== deepLinkTarget.key
)
) {
return;
}
const bootstrapKey = `${chatSessionId}|${selectedTopicRef.topicKey}`; const bootstrapKey = `${chatSessionId}|${bootstrapTopicKey}`;
if (socraticBootstrapRef.current === bootstrapKey) { if (socraticBootstrapRef.current === bootstrapKey) {
return; return;
} }
...@@ -406,7 +451,7 @@ export default function ChatPage() { ...@@ -406,7 +451,7 @@ export default function ChatPage() {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
draft: chatSessionId, draft: chatSessionId,
topic_key: selectedTopicRef.topicKey, topic_key: bootstrapTopicKey,
}), }),
}); });
...@@ -463,6 +508,8 @@ export default function ChatPage() { ...@@ -463,6 +508,8 @@ export default function ChatPage() {
}; };
}, [ }, [
chatSessionId, chatSessionId,
deepLinkTarget,
deepLinkRevision,
isTasksInitialized, isTasksInitialized,
messages.length, messages.length,
selectedOrchestrator, selectedOrchestrator,
...@@ -474,9 +521,12 @@ export default function ChatPage() { ...@@ -474,9 +521,12 @@ export default function ChatPage() {
return; return;
} }
if ( if (
deepLinkTarget.isTaskOrchestrator && (deepLinkTarget.isTaskOrchestrator || deepLinkTarget.isSocratic) &&
deepLinkTarget.hasAnyTaskParam && deepLinkTarget.hasAnyTaskParam &&
processedDeepLinkRef.current !== deepLinkTarget.key (
pendingDeepLinkRef.current === deepLinkTarget.key ||
processedDeepLinkRef.current !== deepLinkTarget.key
)
) { ) {
return; return;
} }
...@@ -491,6 +541,7 @@ export default function ChatPage() { ...@@ -491,6 +541,7 @@ export default function ChatPage() {
} }
}, [ }, [
deepLinkTarget, deepLinkTarget,
deepLinkRevision,
isTaskModeEnabled, isTaskModeEnabled,
isTasksInitialized, isTasksInitialized,
navigate, navigate,
......
...@@ -118,6 +118,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -118,6 +118,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const [taskLocked, setTaskLocked] = useState(false); const [taskLocked, setTaskLocked] = useState(false);
const [selectedTaskDetails, setSelectedTaskDetails] = useState<TaskDetailsResponse | null>(null); const [selectedTaskDetails, setSelectedTaskDetails] = useState<TaskDetailsResponse | null>(null);
const taskDetailsRequestRef = useRef(0); const taskDetailsRequestRef = useRef(0);
const loadedSelectionOrchestratorRef = useRef<OrchestratorName | null>(null);
const isTaskModeEnabled = isTaskCoupledOrchestrator(selectedOrchestrator); const isTaskModeEnabled = isTaskCoupledOrchestrator(selectedOrchestrator);
...@@ -309,6 +310,8 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -309,6 +310,8 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
taskDetailsRequestRef.current += 1; taskDetailsRequestRef.current += 1;
setSelectedTaskDetails(null); setSelectedTaskDetails(null);
void error; void error;
} finally {
loadedSelectionOrchestratorRef.current = orchestrator;
} }
}, [loadTaskDetails]); }, [loadTaskDetails]);
...@@ -343,6 +346,16 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -343,6 +346,16 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
void initTasks(); void initTasks();
}, [initTasks]); }, [initTasks]);
useEffect(() => {
if (!isTasksInitialized) {
return;
}
if (loadedSelectionOrchestratorRef.current === selectedOrchestrator) {
return;
}
void loadSelectionData(selectedOrchestrator);
}, [isTasksInitialized, loadSelectionData, selectedOrchestrator]);
useEffect(() => { useEffect(() => {
if (!isTasksInitialized || selectedOrchestrator !== "task" || !selectedTaskRef) { if (!isTasksInitialized || selectedOrchestrator !== "task" || !selectedTaskRef) {
return; return;
......
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