Commit a2044e3f authored by Kantz's avatar Kantz
Browse files

einstig zum Sokrates nun auch über Link

parent d252c23d
...@@ -71,6 +71,23 @@ Notes: ...@@ -71,6 +71,23 @@ Notes:
- If the link is invalid, the frontend falls back to `/select-task`. - 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. - The task is not locked by deep link, so users can still switch tasks afterwards.
### Socratic Deep Links
You can open a socratic chat directly with URL query parameters:
`/chat?orchestrator=socratic&subsection_key=<subsection_key>`
Example:
`http://localhost:5173/chat?orchestrator=socratic&subsection_key=quadratische%20gleichungen`
Notes:
- `subsection_key` must match an existing subsection entry from the task catalog.
- The socratic subsection list in the UI is built from `backend/sources/tasks/_subsection_map.json` and the `subsections` arrays in the task files.
- 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.
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.
......
...@@ -74,6 +74,9 @@ ...@@ -74,6 +74,9 @@
taskSelectionSubtitle: "Choose a task and start a tutor session", taskSelectionSubtitle: "Choose a task and start a tutor session",
socraticSelectionTitle: "Select a Subsection", socraticSelectionTitle: "Select a Subsection",
socraticSelectionSubtitle: "Choose a subsection and start a socratic session", socraticSelectionSubtitle: "Choose a subsection and start a socratic session",
socraticDeepLinkList: "Direct socratic links",
socraticDeepLinkListSubtitle:
"Open a socratic dialog directly for any available subsection.",
subsection: "Subsection", subsection: "Subsection",
taskFile: "Task Set", taskFile: "Task Set",
taskId: "Task ID", taskId: "Task ID",
...@@ -178,6 +181,9 @@ ...@@ -178,6 +181,9 @@
taskSelectionSubtitle: "Wähle eine Aufgabe und starte den Tutor-Chat", taskSelectionSubtitle: "Wähle eine Aufgabe und starte den Tutor-Chat",
socraticSelectionTitle: "Unterabschnitt auswählen", socraticSelectionTitle: "Unterabschnitt auswählen",
socraticSelectionSubtitle: "Wähle einen Unterabschnitt und starte den sokratischen Chat", socraticSelectionSubtitle: "Wähle einen Unterabschnitt und starte den sokratischen Chat",
socraticDeepLinkList: "Direkte sokratische Links",
socraticDeepLinkListSubtitle:
"Öffne einen sokratischen Dialog direkt für jeden verfügbaren Unterabschnitt.",
subsection: "Unterabschnitt", subsection: "Unterabschnitt",
taskFile: "Aufgabenset", taskFile: "Aufgabenset",
taskId: "Aufgaben-ID", taskId: "Aufgaben-ID",
......
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import type { OrchestratorName } from "../api/orchestratorApi"; import type { OrchestratorName } from "../api/orchestratorApi";
import { selectSubsection } from "../api/taskApi"; import { selectSubsection } from "../api/taskApi";
import OrchestratorSelect from "../components/Orchestrator/OrchestratorSelect"; import OrchestratorSelect from "../components/Orchestrator/OrchestratorSelect";
...@@ -9,6 +9,24 @@ import { getSelectionRouteForOrchestrator } from "../utils/orchestratorRoutes"; ...@@ -9,6 +9,24 @@ import { getSelectionRouteForOrchestrator } from "../utils/orchestratorRoutes";
import "../styles/theme.css"; import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png"; import sumintLogo from "../../SuMINT-Logo.png";
type SocraticLinkGroup = {
key: string;
title: string;
fileId: string;
entries: SocraticLinkEntry[];
};
type SocraticLinkEntry = {
subsectionKey: string;
label: string;
summary: string;
refsText: string;
href: string;
};
const buildSocraticHref = (subsectionKey: string): string =>
`/chat?orchestrator=socratic&subsection_key=${encodeURIComponent(subsectionKey)}`;
export default function SocraticSelectionPage() { export default function SocraticSelectionPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
...@@ -18,6 +36,7 @@ export default function SocraticSelectionPage() { ...@@ -18,6 +36,7 @@ export default function SocraticSelectionPage() {
switchOrchestrator, switchOrchestrator,
isOrchestratorSelectable, isOrchestratorSelectable,
orchestratorError, orchestratorError,
taskFiles,
subsections, subsections,
selectedSubsectionRef, selectedSubsectionRef,
selectedSubsection, selectedSubsection,
...@@ -29,6 +48,76 @@ export default function SocraticSelectionPage() { ...@@ -29,6 +48,76 @@ export default function SocraticSelectionPage() {
} = useTutorSession(); } = useTutorSession();
const subsectionDisplayRef = useRef<HTMLDivElement | null>(null); const subsectionDisplayRef = useRef<HTMLDivElement | null>(null);
const socraticLinkGroups = useMemo<SocraticLinkGroup[]>(() => {
const groupIndex = new Map<string, SocraticLinkGroup>();
const globalOrder = new Map<string, number>();
subsections.forEach((option, index) => {
globalOrder.set(option.subsection_key, index);
});
const fileBySubsectionKey = new Map<
string,
{
fileId: string;
title: string;
}
>();
taskFiles.forEach((file) => {
const title = file.title || file.file_id;
(file.subsection_options || []).forEach((option) => {
if (!fileBySubsectionKey.has(option.subsection_key)) {
fileBySubsectionKey.set(option.subsection_key, {
fileId: file.file_id,
title,
});
}
});
});
subsections.forEach((option) => {
const source = fileBySubsectionKey.get(option.subsection_key);
const groupKey = source?.fileId || "__ungrouped__";
const groupTitle = source?.title || t("subsectionFile");
const refsText = option.refs.map((ref) => ref.join(":")).join(", ");
const entry: SocraticLinkEntry = {
subsectionKey: option.subsection_key,
label: option.label || option.subsection_key,
summary: option.summary || "",
refsText,
href: buildSocraticHref(option.subsection_key),
};
const existing = groupIndex.get(groupKey);
if (existing) {
existing.entries.push(entry);
return;
}
groupIndex.set(groupKey, {
key: groupKey,
title: groupTitle,
fileId: source?.fileId || "",
entries: [entry],
});
});
return [...groupIndex.values()]
.map((group) => ({
...group,
entries: group.entries.sort((a, b) => {
const aOrder = globalOrder.get(a.subsectionKey) ?? Number.MAX_SAFE_INTEGER;
const bOrder = globalOrder.get(b.subsectionKey) ?? Number.MAX_SAFE_INTEGER;
if (aOrder !== bOrder) {
return aOrder - bOrder;
}
return a.label.localeCompare(b.label);
}),
}))
.sort((a, b) => a.title.localeCompare(b.title));
}, [subsections, taskFiles]);
const subsectionMenuOptions = useMemo( const subsectionMenuOptions = useMemo(
() => () =>
subsections.map((option) => ({ subsections.map((option) => ({
...@@ -163,6 +252,45 @@ export default function SocraticSelectionPage() { ...@@ -163,6 +252,45 @@ export default function SocraticSelectionPage() {
> >
{t("startSocratic")} {t("startSocratic")}
</button> </button>
<section className="socratic-link-section" aria-labelledby="socratic-link-list-title">
<div className="task-select-header">
<h3 className="task-select-title" id="socratic-link-list-title">
{t("socraticDeepLinkList")}
</h3>
</div>
<p className="socratic-link-subtitle">{t("socraticDeepLinkListSubtitle")}</p>
{socraticLinkGroups.length ? (
<div className="socratic-link-groups">
{socraticLinkGroups.map((group) => (
<article key={group.key} className="socratic-link-group">
<div className="socratic-link-group-header">
<div className="socratic-link-group-title">{group.title}</div>
{group.fileId ? (
<div className="socratic-link-group-meta">{group.fileId}</div>
) : null}
</div>
<div className="socratic-link-items">
{group.entries.map((entry) => (
<Link key={entry.subsectionKey} className="socratic-link-item" to={entry.href}>
<span className="socratic-link-item-title">{entry.label}</span>
<span className="socratic-link-item-key">{entry.subsectionKey}</span>
{entry.refsText ? (
<span className="socratic-link-item-meta">{entry.refsText}</span>
) : null}
{entry.summary ? (
<span className="socratic-link-item-summary">{entry.summary}</span>
) : null}
</Link>
))}
</div>
</article>
))}
</div>
) : (
<div className="task-panel-empty">{t("noSubsectionsAvailable")}</div>
)}
</section>
</section> </section>
</div> </div>
</main> </main>
......
...@@ -938,6 +938,110 @@ body { ...@@ -938,6 +938,110 @@ body {
padding: 18px; padding: 18px;
} }
.socratic-link-section {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 4px;
padding-top: 12px;
border-top: 1px solid #e2ded5;
}
.socratic-link-subtitle {
margin: 0;
color: #6f675d;
font-size: 13px;
}
.socratic-link-groups {
display: flex;
flex-direction: column;
gap: 12px;
}
.socratic-link-group {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
border: 1px solid #e2ded5;
border-radius: 14px;
background: #fffdf8;
}
.socratic-link-group-header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: baseline;
}
.socratic-link-group-title {
font-weight: 600;
font-size: 14px;
}
.socratic-link-group-meta {
color: #6f675d;
font-size: 12px;
white-space: nowrap;
}
.socratic-link-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.socratic-link-item {
display: flex;
flex-direction: column;
gap: 4px;
min-height: 100%;
padding: 12px;
border-radius: 12px;
border: 1px solid #d8d1c4;
background: linear-gradient(180deg, #fff 0%, #fff8eb 100%);
color: inherit;
text-decoration: none;
transition:
transform 0.12s ease,
box-shadow 0.12s ease,
border-color 0.12s ease;
}
.socratic-link-item:hover,
.socratic-link-item:focus-visible {
transform: translateY(-1px);
border-color: #b9b2a4;
box-shadow: 0 10px 22px rgba(29, 27, 22, 0.08);
outline: none;
}
.socratic-link-item-title {
font-weight: 600;
}
.socratic-link-item-key,
.socratic-link-item-meta,
.socratic-link-item-summary {
font-size: 12px;
color: #6f675d;
}
.socratic-link-item-summary {
white-space: pre-line;
}
.task-panel-empty {
padding: 8px 10px;
border-radius: 10px;
background: #fff;
border: 1px dashed #d8d1c4;
color: #6f675d;
font-size: 12px;
}
.task-select-title { .task-select-title {
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
......
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