Commit f35d87d3 authored by Kantz's avatar Kantz
Browse files

deeplinks für Sokrates hinzugefuegt

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