Commit c09584f1 authored by Kantz's avatar Kantz
Browse files

Merge branch 'dev' into 'main'

sokrates

See merge request kantz/tutor_react!19
parents 2dc8ef37 dde1de26
......@@ -14,12 +14,14 @@ type ModeMeta = {
| "orchestratorModeQaLabel"
| "orchestratorModeTutorLabel"
| "orchestratorModeTaskLabel"
| "orchestratorModeFeedbackLabel";
| "orchestratorModeFeedbackLabel"
| "orchestratorModeSocraticLabel";
descriptionKey:
| "orchestratorModeQaDescription"
| "orchestratorModeTutorDescription"
| "orchestratorModeTaskDescription"
| "orchestratorModeFeedbackDescription";
| "orchestratorModeFeedbackDescription"
| "orchestratorModeSocraticDescription";
};
const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
......@@ -39,6 +41,10 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
labelKey: "orchestratorModeFeedbackLabel",
descriptionKey: "orchestratorModeFeedbackDescription",
},
socratic: {
labelKey: "orchestratorModeSocraticLabel",
descriptionKey: "orchestratorModeSocraticDescription",
},
};
type ModeDescriptionDisplay = "tooltip" | "helperText" | "optionSuffix";
......
import { useEffect, useMemo, useRef } from "react";
import ReactMarkdown from "react-markdown";
import { t } from "../../i18n";
import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown";
type SocraticPanelProps = {
selectedSubsectionLabel?: string;
selectedSubsectionKey?: string;
selectedSubsectionSummary?: string;
onChangeSelection?: () => void;
};
export default function SocraticPanel({
selectedSubsectionLabel,
selectedSubsectionKey,
selectedSubsectionSummary,
onChangeSelection,
}: SocraticPanelProps) {
const contentRef = useRef<HTMLDivElement | null>(null);
const summaryMarkdown = selectedSubsectionSummary?.trim() || "";
const renderedSummary = useMemo(
() => escapeAsterisksInsideMath(summaryMarkdown),
[summaryMarkdown]
);
useEffect(() => {
if (!contentRef.current) {
return;
}
const mathjax = window.MathJax;
if (!mathjax?.typesetPromise) {
return;
}
mathjax.typesetPromise([contentRef.current]).catch(() => undefined);
}, [selectedSubsectionSummary]);
return (
<section className="task-panel">
<div className="task-panel-header">
<div className="task-panel-title">{t("orchestratorModeSocraticLabel")}</div>
{onChangeSelection ? (
<button type="button" className="btn task-panel-change-btn" onClick={onChangeSelection}>
{t("changeSubsectionArea")}
</button>
) : null}
</div>
<div className="task-panel-meta">
<div>{selectedSubsectionLabel || ""}</div>
<div>{selectedSubsectionKey ? `${t("subsectionKey")}: ${selectedSubsectionKey}` : ""}</div>
</div>
<div className="task-panel-content socratic-summary-content" ref={contentRef}>
{renderedSummary ? <ReactMarkdown>{renderedSummary}</ReactMarkdown> : selectedSubsectionLabel || t("noSubsectionSelected")}
</div>
</section>
);
}
......@@ -22,11 +22,14 @@
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic",
orchestratorModeDescriptionTitle: "Mode help",
orchestratorModeQaDescription: "Direct answers based on the script",
orchestratorModeTutorDescription: "Help with your own questions",
orchestratorModeTaskDescription: "Help with textbook exercises",
orchestratorModeFeedbackDescription: "Short feedback on your solution for textbook exercises",
orchestratorModeSocraticDescription:
"Socratic guidance based on fixed subsection context for textbook exercises",
directChildren: "Direct children",
taskChildren: "Task sources",
indirectChildren: "Indirect children",
......@@ -45,6 +48,8 @@
hideThinking: "Hide thinking",
noTasksAvailable: "No tasks available",
noTaskSelected: "No task selected.",
noSubsectionsAvailable: "No subsections available",
noSubsectionSelected: "No subsection selected.",
savedChats: "Saved Chats",
saving: "Saving...",
noSavedChatsYet: "No saved chats yet.",
......@@ -67,10 +72,16 @@
"Saving the canvas failed. Please check backend logs.",
taskSelectionTitle: "Select a Task",
taskSelectionSubtitle: "Choose a task and start a tutor session",
socraticSelectionTitle: "Select a Subsection",
socraticSelectionSubtitle: "Choose a subsection and start a socratic session",
subsection: "Subsection",
taskFile: "Task Set",
taskId: "Task ID",
subsectionKey: "Subsection Key",
solveWithTutor: "Solve with Tutor",
startSocratic: "Start Socratic",
changeTaskArea: "Change Task Area",
changeSubsectionArea: "Change Subsection Area",
previousTask: "Previous Task",
nextTask: "Next Task",
backendChecking: "Checking backend availability...",
......@@ -81,6 +92,9 @@
lastCheckFailed: "Last check: {detail}",
deepLinkInvalidTask: "Invalid task link. Please choose a task manually.",
deepLinkInitFailed: "Task link initialization failed. Please choose a task manually.",
deepLinkInvalidSubsection: "Invalid subsection link. Please choose a subsection manually.",
deepLinkInitFailedSubsection:
"Subsection link initialization failed. Please choose a subsection manually.",
},
de: {
chats: "Chats",
......@@ -106,11 +120,14 @@
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic",
orchestratorModeDescriptionTitle: "Modus-Hilfe",
orchestratorModeQaDescription: "Direkte Antworten basierend auf dem Skript",
orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen",
orchestratorModeTaskDescription: "Hilfe bei Aufgaben aus dem Lehrwerk",
orchestratorModeFeedbackDescription: "Kurzes Feedback zu deiner Lösung bei Aufgaben aus dem Lehrwerk",
orchestratorModeSocraticDescription:
"Sokratische Anleitung auf Basis fester Subsection-Kontexte für Aufgaben aus dem Lehrwerk",
directChildren: "Direkte Quellen",
taskChildren: "Aufgaben-Quellen",
indirectChildren: "Indirekte Quellen",
......@@ -129,6 +146,8 @@
hideThinking: "Thinking ausblenden",
noTasksAvailable: "Keine Aufgaben verfügbar",
noTaskSelected: "Keine Aufgabe ausgewählt.",
noSubsectionsAvailable: "Keine Unterabschnitte verfügbar",
noSubsectionSelected: "Kein Unterabschnitt ausgewählt.",
savedChats: "Gespeicherte Chats",
saving: "Speichere...",
noSavedChatsYet: "Noch keine gespeicherten Chats.",
......@@ -156,10 +175,16 @@
"Da ist wohl das Speichern des Canvas fehlgeschlagen. Gib gerne deinem Dozenten bescheid. In vielen Fällen hilft es die Seite neu zu laden.",
taskSelectionTitle: "Aufgabe auswählen",
taskSelectionSubtitle: "Wähle eine Aufgabe und starte den Tutor-Chat",
socraticSelectionTitle: "Unterabschnitt auswählen",
socraticSelectionSubtitle: "Wähle einen Unterabschnitt und starte den sokratischen Chat",
subsection: "Unterabschnitt",
taskFile: "Aufgabenset",
taskId: "Aufgaben-ID",
subsectionKey: "Unterabschnitt-Schlüssel",
solveWithTutor: "Mit Tutor lösen",
startSocratic: "Sokratischen Dialog führen",
changeTaskArea: "Aufgabengebiet ändern",
changeSubsectionArea: "Unterabschnitt ändern",
previousTask: "Vorherige Aufgabe",
nextTask: "Nächste Aufgabe",
backendChecking: "Backend-Verbindung wird geprüft...",
......@@ -172,6 +197,10 @@
"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.",
deepLinkInvalidSubsection:
"Ungültiger Unterabschnitt-Link. Bitte wähle den Unterabschnitt manuell aus.",
deepLinkInitFailedSubsection:
"Der Unterabschnitt-Link konnte nicht initialisiert werden. Bitte wähle den Unterabschnitt manuell aus.",
},
} as const;
......
import { Navigate, Route, Routes } from "react-router-dom";
import { Navigate, Route, Routes } from "react-router-dom";
import { t } from "../i18n";
import ChatPage from "./ChatPage";
import SocraticSelectionPage from "./SocraticSelectionPage";
import TaskSelectionPage from "./TaskSelectionPage";
import { TutorSessionProvider, useTutorSession } from "../state/tutorSession";
import { getSelectionRouteForOrchestrator } from "../utils/orchestratorRoutes";
function StartRoute() {
const { isTasksInitialized, isTaskModeEnabled } = useTutorSession();
const { isTasksInitialized, selectedOrchestrator } = useTutorSession();
if (!isTasksInitialized) {
return <div className="app-loading">{t("loading")}</div>;
}
return <Navigate to={isTaskModeEnabled ? "/select-task" : "/chat"} replace />;
return <Navigate to={getSelectionRouteForOrchestrator(selectedOrchestrator)} replace />;
}
export default function App() {
......@@ -20,6 +22,7 @@ export default function App() {
<Routes>
<Route path="/" element={<StartRoute />} />
<Route path="/select-task" element={<TaskSelectionPage />} />
<Route path="/select-socratic" element={<SocraticSelectionPage />} />
<Route path="/chat" element={<ChatPage />} />
<Route path="*" element={<StartRoute />} />
</Routes>
......
This diff is collapsed.
import { useEffect, useMemo, useRef } from "react";
import { useNavigate } from "react-router-dom";
import type { OrchestratorName } from "../api/orchestratorApi";
import { selectSubsection } from "../api/taskApi";
import OrchestratorSelect from "../components/Orchestrator/OrchestratorSelect";
import { t } from "../i18n";
import { useTutorSession } from "../state/tutorSession";
import { getSelectionRouteForOrchestrator } from "../utils/orchestratorRoutes";
import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png";
export default function SocraticSelectionPage() {
const navigate = useNavigate();
const {
chatSessionId,
selectedOrchestrator,
availableOrchestrators,
switchOrchestrator,
isOrchestratorSelectable,
orchestratorError,
subsections,
selectedSubsectionRef,
selectedSubsection,
tasksError,
setSubsectionKey,
lockTask,
unlockTask,
isTasksInitialized,
} = useTutorSession();
const subsectionDisplayRef = useRef<HTMLDivElement | null>(null);
const subsectionMenuOptions = useMemo(
() =>
subsections.map((option) => ({
value: option.subsection_key,
label: option.label || option.subsection_key,
})),
[subsections]
);
useEffect(() => {
if (!isTasksInitialized) {
return;
}
if (selectedOrchestrator !== "socratic") {
navigate(getSelectionRouteForOrchestrator(selectedOrchestrator), { replace: true });
return;
}
unlockTask();
}, [isTasksInitialized, navigate, selectedOrchestrator, unlockTask]);
useEffect(() => {
if (!selectedSubsection?.label || !subsectionDisplayRef.current) {
return;
}
const mathjax = window.MathJax;
if (!mathjax?.typesetPromise) {
return;
}
mathjax.typesetPromise([subsectionDisplayRef.current]).catch(() => undefined);
}, [selectedSubsection?.label]);
useEffect(() => {
if (!subsectionMenuOptions.length) {
return;
}
if (
selectedSubsectionRef &&
subsectionMenuOptions.some((option) => option.value === selectedSubsectionRef.subsectionKey)
) {
return;
}
setSubsectionKey(subsectionMenuOptions[0].value);
}, [selectedSubsectionRef, setSubsectionKey, subsectionMenuOptions]);
const handleStartSocratic = async () => {
if (!selectedSubsectionRef) {
return;
}
try {
await selectSubsection({
draft: chatSessionId,
subsectionKey: selectedSubsectionRef.subsectionKey,
});
lockTask();
navigate("/chat");
} catch (error) {
void error;
}
};
const handleSwitchOrchestrator = (next: OrchestratorName) => {
if (next === selectedOrchestrator) {
return;
}
switchOrchestrator(next);
navigate(getSelectionRouteForOrchestrator(next), { replace: true });
};
if (!isTasksInitialized) {
return <div className="app-loading">{t("loading")}</div>;
}
return (
<div className="app-shell">
<header className="app-header">
<div className="brand">
<img className="brand-logo" src={sumintLogo} alt="SuMINT Logo" />
<div className="brand-text">
<div className="brand-title">Mathe Tutor</div>
<div className="brand-subtitle">{t("socraticSelectionSubtitle")}</div>
</div>
</div>
</header>
{orchestratorError ? <div className="chat-archive-error">{orchestratorError}</div> : null}
<main className="task-select-main">
<div className="task-select-wrap">
<div className="task-mode-row">
<OrchestratorSelect
value={selectedOrchestrator}
options={availableOrchestrators}
onChange={handleSwitchOrchestrator}
disabled={!isOrchestratorSelectable}
/>
</div>
<section className="task-select-card">
<div className="task-select-header">
<h2 className="task-select-title">{t("socraticSelectionTitle")}</h2>
</div>
<div className="task-select-controls">
<label className="task-select-label" htmlFor="socratic-subsection-select">
{t("subsection")}
</label>
<select
id="socratic-subsection-select"
className="task-select"
value={selectedSubsectionRef?.subsectionKey || ""}
onChange={(event) => setSubsectionKey(event.target.value)}
disabled={!subsectionMenuOptions.length}
>
{subsectionMenuOptions.length ? (
subsectionMenuOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))
) : (
<option value="">{t("noSubsectionsAvailable")}</option>
)}
</select>
</div>
{tasksError ? <div className="task-panel-error">{tasksError}</div> : null}
<button
type="button"
className="btn primary task-solve-btn"
onClick={handleStartSocratic}
disabled={!selectedSubsectionRef}
>
{t("startSocratic")}
</button>
</section>
</div>
</main>
</div>
);
}
......@@ -5,12 +5,10 @@ import OrchestratorSelect from "../components/Orchestrator/OrchestratorSelect";
import { selectTask } from "../api/taskApi";
import { t } from "../i18n";
import { useTutorSession } from "../state/tutorSession";
import { getSelectionRouteForOrchestrator } from "../utils/orchestratorRoutes";
import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png";
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
export default function TaskSelectionPage() {
const navigate = useNavigate();
const {
......@@ -42,8 +40,12 @@ export default function TaskSelectionPage() {
navigate("/chat", { replace: true });
return;
}
if (selectedOrchestrator === "socratic") {
navigate("/select-socratic", { replace: true });
return;
}
unlockTask();
}, [isTaskModeEnabled, isTasksInitialized, navigate, unlockTask]);
}, [isTaskModeEnabled, isTasksInitialized, navigate, selectedOrchestrator, unlockTask]);
useEffect(() => {
if (!taskFileOptions.length) {
......@@ -89,7 +91,7 @@ export default function TaskSelectionPage() {
return;
}
switchOrchestrator(next);
navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat", { replace: true });
navigate(getSelectionRouteForOrchestrator(next), { replace: true });
};
if (!isTasksInitialized) {
......
......@@ -14,7 +14,13 @@ import {
getFallbackOrchestrators,
type OrchestratorName,
} from "../api/orchestratorApi";
import { fetchTasks, type SelectedTaskRef, type TaskFile } from "../api/taskApi";
import {
fetchTasks,
type SelectedSubsectionRef,
type SelectedTaskRef,
type SubsectionOption,
type TaskFile,
} from "../api/taskApi";
export type SelectOption = {
value: string;
......@@ -26,6 +32,13 @@ export type SelectedTask = SelectedTaskRef & {
fullText: string;
};
export type SelectedSubsection = SelectedSubsectionRef & {
label: string;
refs: [number, number, number][];
refsText: string;
summary: string;
};
export type TaskSelectionState = {
taskFiles: TaskFile[];
selectedTaskRef: SelectedTaskRef | null;
......@@ -33,6 +46,10 @@ export type TaskSelectionState = {
selectedTaskFile: TaskFile | null;
taskFileOptions: SelectOption[];
taskOptions: SelectOption[];
selectedSubsectionRef: SelectedSubsectionRef | null;
selectedSubsection: SelectedSubsection | null;
subsections: SubsectionOption[];
subsectionOptions: SelectOption[];
tasksError: string | null;
isTaskModeEnabled: boolean;
isTasksInitialized: boolean;
......@@ -50,8 +67,10 @@ export type TutorSessionState = TaskSelectionState & {
isOrchestratorSelectable: boolean;
orchestratorError: string | null;
setTaskRef: (value: SelectedTaskRef | null) => void;
setSubsectionRef: (value: SelectedSubsectionRef | null) => void;
setTaskFile: (fileId: string) => void;
setTaskId: (taskId: string) => void;
setSubsectionKey: (subsectionKey: string) => void;
lockTask: () => void;
unlockTask: () => void;
resetForNewChat: () => void;
......@@ -69,7 +88,10 @@ const isSelectableTaskFile = (file: TaskFile): boolean =>
Array.isArray(file.subsections) && file.subsections.length > 0;
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
value === "task" || value === "feedback" || value === "socratic";
const formatSubsectionRefs = (refs: [number, number, number][]): string =>
refs.map((ref) => ref.join(":")).join(", ");
export function TutorSessionProvider({ children }: PropsWithChildren) {
const [chatSessionId, setChatSessionId] = useState<string>(() => createSessionId());
......@@ -82,7 +104,10 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const [orchestratorError, setOrchestratorError] = useState<string | null>(null);
const [isTasksInitialized, setIsTasksInitialized] = useState(false);
const [taskFiles, setTaskFiles] = useState<TaskFile[]>([]);
const [subsections, setSubsections] = useState<SubsectionOption[]>([]);
const [selectedTaskRef, setSelectedTaskRef] = useState<SelectedTaskRef | null>(null);
const [selectedSubsectionRef, setSelectedSubsectionRef] =
useState<SelectedSubsectionRef | null>(null);
const [tasksError, setTasksError] = useState<string | null>(null);
const [taskLocked, setTaskLocked] = useState(false);
......@@ -136,6 +161,35 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
[selectedTaskFile]
);
const subsectionOptions = useMemo(
() =>
subsections.map((option) => ({
value: option.subsection_key,
label: option.label || option.subsection_key,
})),
[subsections]
);
const selectedSubsection = useMemo<SelectedSubsection | null>(() => {
if (!selectedSubsectionRef) {
return null;
}
const option = subsections.find(
(item) => item.subsection_key === selectedSubsectionRef.subsectionKey
);
if (!option) {
return null;
}
const refs = option.refs ?? [];
return {
subsectionKey: option.subsection_key,
label: option.label,
refs,
refsText: formatSubsectionRefs(refs),
summary: option.summary || "",
};
}, [selectedSubsectionRef, subsections]);
const initTasks = useCallback(async () => {
setTasksError(null);
setOrchestratorError(null);
......@@ -160,9 +214,11 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
try {
const payload = await fetchTasks();
const files = payload.task_files || [];
const subsectionsPayload = payload.subsections || [];
const selectableFiles = files.filter((file) => isSelectableTaskFile(file));
setTaskFiles(files);
setSubsections(subsectionsPayload);
setSelectedTaskRef((prev) => {
if (prev) {
const file = selectableFiles.find((item) => item.file_id === prev.fileId);
......@@ -181,10 +237,26 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
}
return { fileId: firstFile.file_id, taskId: defaultTaskId };
});
setSelectedSubsectionRef((prev) => {
if (prev) {
const option = subsectionsPayload.find((item) => item.subsection_key === prev.subsectionKey);
if (option) {
return prev;
}
}
const firstOption = subsectionsPayload[0];
if (!firstOption) {
return null;
}
return { subsectionKey: firstOption.subsection_key };
});
} catch (error) {
setTasksError(t("failedLoadTasks"));
setTaskFiles([]);
setSubsections([]);
setSelectedTaskRef(null);
setSelectedSubsectionRef(null);
void error;
} finally {
setIsTasksInitialized(true);
......@@ -223,6 +295,16 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
[selectedTaskRef]
);
const setSubsectionKey = useCallback(
(subsectionKey: string) => {
if (!subsectionKey) {
return;
}
setSelectedSubsectionRef({ subsectionKey });
},
[]
);
const setSelectedOrchestrator = useCallback((value: OrchestratorName) => {
setSelectedOrchestratorState(value);
}, []);
......@@ -231,9 +313,14 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedOrchestratorState(value);
setChatSessionId(createSessionId());
setTaskLocked(false);
if (!isTaskCoupledOrchestrator(value)) {
if (value === "socratic") {
setSelectedTaskRef(null);
} else if (!isTaskCoupledOrchestrator(value)) {
setSelectedTaskRef(null);
}
if (value !== "socratic") {
setSelectedSubsectionRef(null);
}
}, []);
const lockTask = useCallback(() => {
......@@ -247,6 +334,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const resetForNewChat = useCallback(() => {
setChatSessionId(createSessionId());
setSelectedTaskRef(null);
setSelectedSubsectionRef(null);
setTaskLocked(false);
}, []);
......@@ -261,14 +349,20 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
isOrchestratorSelectable,
orchestratorError,
taskFiles,
subsections,
selectedTaskRef,
selectedTask,
selectedTaskFile,
taskFileOptions,
taskOptions,
selectedSubsectionRef,
selectedSubsection,
subsectionOptions,
setTaskRef: setSelectedTaskRef,
setSubsectionRef: setSelectedSubsectionRef,
setTaskFile,
setTaskId,
setSubsectionKey,
tasksError,
isTaskModeEnabled,
isTasksInitialized,
......
......@@ -260,6 +260,12 @@ body {
overflow-x: auto;
}
.socratic-summary-content {
max-height: 30vh;
overflow-y: auto;
white-space: normal;
}
.task-panel-error {
padding: 8px 10px;
border-radius: 10px;
......@@ -510,6 +516,33 @@ body {
margin-left: auto;
}
.composer-loading {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-width: 28px;
min-height: 18px;
}
.composer-loading span {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 999px;
background: currentColor;
opacity: 0.55;
animation: composer-dot-bounce 1s infinite ease-in-out;
}
.composer-loading span:nth-child(2) {
animation-delay: 0.15s;
}
.composer-loading span:nth-child(3) {
animation-delay: 0.3s;
}
.composer-input {
width: 100%;
resize: vertical;
......@@ -519,6 +552,12 @@ body {
font-family: inherit;
}
.composer-input:disabled {
background: #f7f3ec;
color: #8b857b;
cursor: not-allowed;
}
.btn {
border: 1px solid #b9b2a4;
background: #ffffff;
......@@ -539,6 +578,19 @@ body {
cursor: not-allowed;
}
@keyframes composer-dot-bounce {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.45;
}
40% {
transform: translateY(-3px);
opacity: 1;
}
}
.btn.small {
padding: 4px 10px;
font-size: 11px;
......
import type { OrchestratorName } from "../api/orchestratorApi";
export const isTaskSelectionOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
export const isSocraticOrchestrator = (value: OrchestratorName): boolean =>
value === "socratic";
export const getSelectionRouteForOrchestrator = (value: OrchestratorName): string => {
if (value === "socratic") {
return "/select-socratic";
}
if (value === "task" || value === "feedback") {
return "/select-task";
}
return "/chat";
};
const collapseWhitespace = (value: string): string => value.replace(/\s+/g, " ").trim();
export const normalizeSubsectionKey = (value: string): string =>
collapseWhitespace(String(value).replace(/[-_]+/g, " ").toLowerCase());
export const subsectionKeyToSlug = (value: string): string =>
normalizeSubsectionKey(value).replace(/\s+/g, "-");
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