+
{t('Output')}
+ {outputData !== undefined && outputData !== null && (
+
+ )}
<_DataBlock data={outputData} />
<_FileLinkList files={outputFiles} />
@@ -1349,12 +1426,12 @@ export const AutomationsDashboardPage: React.FC = () => {
},
{
id: 'dashboard',
- label: t('Dashboard'),
+ label: t('Workflow-Durchläufe'),
content: <_DashboardTab workflowFilter={workflowFilter} onRunClick={_handleRunClick} />,
},
{
id: 'workspace',
- label: t('Workspace'),
+ label: t('Durchlauf-Details'),
content: <_WorkspaceTab runId={selectedRunId} onBack={_handleBackFromWorkspace} />,
},
], [t, _handleWorkflowClick, workflowFilter, _handleRunClick, selectedRunId, _handleBackFromWorkspace]);
diff --git a/src/pages/basedata/FilesPage.tsx b/src/pages/basedata/FilesPage.tsx
index 36c7802..d1d019e 100644
--- a/src/pages/basedata/FilesPage.tsx
+++ b/src/pages/basedata/FilesPage.tsx
@@ -1,23 +1,25 @@
/**
* FilesPage
*
- * Full-width file management using FormGeneratorTable with persistent grouping.
- * Organisation exclusively via groupTree/groupId — no physical folder navigation.
+ * Split-view file management: tree panel on the left (FormGeneratorTree),
+ * FormGeneratorTable on the right. Two modes:
+ * - "Ordner-Sicht": table filtered by selected folder in the tree
+ * - "Alle Dateien": table shows all files without folder filter
*/
-import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
+import React, { useState, useMemo, useEffect, useRef, useCallback, type PointerEvent as RPointerEvent } from 'react';
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
-import { FaSync, FaUpload, FaDownload, FaLock, FaLockOpen, FaFileArchive, FaTrash } from 'react-icons/fa';
+import { FormGeneratorTree } from '../../components/FormGenerator/FormGeneratorTree';
+import { createFolderFileProvider } from '../../components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider';
+import type { TreeNode } from '../../components/FormGenerator/FormGeneratorTree';
+import { FaSync, FaUpload, FaDownload, FaTree, FaTable } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
-import { useApiRequest } from '../../hooks/useApi';
-import { patchGroupScope, downloadGroupZip, deleteGroup } from '../../api/fileApi';
import styles from '../admin/Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { getUserDataCache } from '../../utils/userCache';
import { resolveColumnTypes } from '../../utils/columnTypeResolver';
-import type { GroupBulkAction } from '../../components/FormGenerator/GroupingManager/GroupRow';
interface UserFile {
id: string;
@@ -28,11 +30,16 @@ interface UserFile {
[key: string]: any;
}
+type ViewMode = 'folder' | 'all';
+
export const FilesPage: React.FC = () => {
const { t } = useLanguage();
const fileInputRef = useRef
(null);
const { showSuccess, showError } = useToast();
- const { request } = useApiRequest();
+
+ const [viewMode, setViewMode] = useState('folder');
+ const provider = useMemo(() => createFolderFileProvider(), []);
+ const [treeKey, setTreeKey] = useState(0);
// ── Table data ────────────────────────────────────────────────────────
const {
@@ -43,7 +50,6 @@ export const FilesPage: React.FC = () => {
error,
refetch: tableRefetch,
pagination,
- groupTree,
fetchFileById,
updateFileOptimistically,
} = useUserFiles();
@@ -63,20 +69,68 @@ export const FilesPage: React.FC = () => {
const [editingFile, setEditingFile] = useState(null);
const [selectedFiles, setSelectedFiles] = useState([]);
+ const [selectedFolderId, setSelectedFolderId] = useState(null);
+ const [highlightedFileId, setHighlightedFileId] = useState(null);
- // ── Table refetch wrapper ──────────────────────────────────────────────
+ const [treeWidth, setTreeWidth] = useState(300);
+ const [treeVisible, setTreeVisible] = useState(true);
+ const [tableVisible, setTableVisible] = useState(true);
+ const draggingRef = useRef(false);
+ const splitContainerRef = useRef(null);
+
+ const _handleDividerPointerDown = useCallback((e: RPointerEvent) => {
+ e.preventDefault();
+ draggingRef.current = true;
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
+ }, []);
+
+ const _handleDividerPointerMove = useCallback((e: RPointerEvent) => {
+ if (!draggingRef.current || !splitContainerRef.current) return;
+ const rect = splitContainerRef.current.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ setTreeWidth(Math.max(180, Math.min(x, rect.width - 200)));
+ }, []);
+
+ const _handleDividerPointerUp = useCallback(() => {
+ draggingRef.current = false;
+ }, []);
+
+ // ── Table refetch wrapper (filters by selectedFolderId in folder mode) ──
const _tableRefetch = useCallback(async (params?: any) => {
- await tableRefetch(params);
- }, [tableRefetch]);
+ const nextParams = { ...(params || {}) };
+ const nextFilters = { ...(nextParams.filters || {}) };
+ if (viewMode === 'folder' && selectedFolderId) {
+ nextFilters.folderId = selectedFolderId;
+ } else {
+ delete nextFilters.folderId;
+ }
+ nextParams.filters = nextFilters;
+ await tableRefetch(nextParams);
+ }, [tableRefetch, selectedFolderId, viewMode]);
const _refreshAll = useCallback(async () => {
await _tableRefetch({ page: 1, pageSize: 25 });
+ setTreeKey(k => k + 1);
}, [_tableRefetch]);
- // Initial fetch
useEffect(() => {
_tableRefetch({ page: 1, pageSize: 25 });
- }, [_tableRefetch]);
+ }, [selectedFolderId, viewMode, _tableRefetch]);
+
+ // ── Tree interaction ──────────────────────────────────────────────────
+ const _handleTreeNodeClick = useCallback((node: TreeNode) => {
+ if (node.type === 'folder') {
+ setSelectedFolderId(node.id);
+ } else if (node.type === 'file') {
+ setSelectedFolderId(node.parentId);
+ setHighlightedFileId(node.id);
+ requestAnimationFrame(() => {
+ const row = document.querySelector('tr[data-highlighted="true"]');
+ if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ });
+ setTimeout(() => setHighlightedFileId(null), 2500);
+ }
+ }, []);
// ── Columns ───────────────────────────────────────────────────────────
const columns = useMemo(() => {
@@ -181,6 +235,7 @@ export const FilesPage: React.FC = () => {
}
if (fileInputRef.current) fileInputRef.current.value = '';
await _tableRefetch();
+ setTreeKey(k => k + 1);
if (successCount > 0) {
showSuccess(
t('Upload erfolgreich'),
@@ -194,55 +249,6 @@ export const FilesPage: React.FC = () => {
}
};
- const _groupBulkActionsProvider = useCallback((groupId: string, itemIds: string[]): GroupBulkAction[] => {
- return [
- {
- icon: ,
- title: t('Scope: personal'),
- onClick: async () => {
- try {
- await patchGroupScope(request, groupId, 'personal');
- showSuccess(t('Scope aktualisiert'), t('{n} Dateien auf personal gesetzt', { n: String(itemIds.length) }));
- await _tableRefetch();
- } catch (e) { showError(t('Fehler'), String(e)); }
- },
- },
- {
- icon: ,
- title: t('Scope: mandate'),
- onClick: async () => {
- try {
- await patchGroupScope(request, groupId, 'mandate');
- showSuccess(t('Scope aktualisiert'), t('{n} Dateien auf mandate gesetzt', { n: String(itemIds.length) }));
- await _tableRefetch();
- } catch (e) { showError(t('Fehler'), String(e)); }
- },
- },
- {
- icon: ,
- title: t('ZIP herunterladen'),
- onClick: async () => {
- try { await downloadGroupZip(groupId); }
- catch (e) { showError(t('Fehler'), String(e)); }
- },
- disabled: itemIds.length === 0,
- },
- {
- icon: ,
- title: t('Gruppe + Dateien löschen'),
- variant: 'danger' as const,
- onClick: async () => {
- try {
- await deleteGroup(request, groupId, true);
- showSuccess(t('Gelöscht'), t('Gruppe und {n} Dateien gelöscht', { n: String(itemIds.length) }));
- await _tableRefetch();
- } catch (e) { showError(t('Fehler'), String(e)); }
- },
- disabled: itemIds.length === 0,
- },
- ];
- }, [request, showSuccess, showError, _tableRefetch, t]);
-
const _onRowDragStart = useCallback((e: React.DragEvent, row: UserFile) => {
const isInSelection = selectedFiles.some(f => f.id === row.id);
if (isInSelection && selectedFiles.length > 1) {
@@ -262,7 +268,7 @@ export const FilesPage: React.FC = () => {
return (
-
⚠️
+
⚠️
{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}
+
+
+
+
-
-
- {canCreate && (
-
- )}
-
+
+ {/* Left panel: Tree */}
+ {treeVisible && (
+
+ _tableRefetch()}
+ />
+
+
+ )}
-
-
setSelectedFiles(rows as UserFile[])}
- rowDraggable={true}
- onRowDragStart={_onRowDragStart}
- actionButtons={[
- {
- type: 'view' as const,
- onAction: () => {},
- title: t('Vorschau'),
- idField: 'id',
- nameField: 'fileName',
- typeField: 'mimeType',
- loadingStateName: 'previewingFiles',
- },
- ...(canUpdate ? [{
- type: 'edit' as const,
- onAction: handleEditClick,
- title: t('Bearbeiten'),
- disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann bearbeiten') } : false,
- }] : []),
- ...(canDelete ? [{
- type: 'delete' as const,
- title: t('Löschen'),
- loading: (row: UserFile) => deletingFiles.has(row.id),
- disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann löschen') } : false,
- }] : []),
- ]}
- customActions={[
- {
- id: 'download',
- icon: ,
- onClick: handleDownload,
- title: t('Herunterladen'),
- loading: (row: UserFile) => downloadingFiles.has(row.id),
- },
- ]}
- onDelete={handleDelete}
- onDeleteMultiple={handleDeleteMultiple}
- hookData={{
- refetch: _tableRefetch,
- pagination,
- permissions,
- handleDelete: handleFileDelete,
- handleInlineUpdate,
- updateOptimistically: updateFileOptimistically,
- previewingFiles,
- groupTree,
+ {/* Resizable divider */}
+ {treeVisible && tableVisible && (
+
+ >
+
+
+ )}
+
+ {/* Right panel: Table with view-mode toggle */}
+ {tableVisible && (
+
+
+
+
+
+
+
+ {canCreate && (
+
+ )}
+
+
+
+ setSelectedFiles(rows as UserFile[])}
+ rowDraggable={true}
+ onRowDragStart={_onRowDragStart}
+ getRowDataAttributes={(row: UserFile) => ({
+ highlighted: row.id === highlightedFileId ? 'true' : 'false',
+ })}
+ actionButtons={[
+ {
+ type: 'view' as const,
+ onAction: () => {},
+ title: t('Vorschau'),
+ idField: 'id',
+ nameField: 'fileName',
+ typeField: 'mimeType',
+ loadingStateName: 'previewingFiles',
+ },
+ ...(canUpdate ? [{
+ type: 'edit' as const,
+ onAction: handleEditClick,
+ title: t('Bearbeiten'),
+ disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentuemer kann bearbeiten') } : false,
+ }] : []),
+ ...(canDelete ? [{
+ type: 'delete' as const,
+ title: t('Loeschen'),
+ loading: (row: UserFile) => deletingFiles.has(row.id),
+ disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentuemer kann loeschen') } : false,
+ }] : []),
+ ]}
+ customActions={[
+ {
+ id: 'download',
+ icon: ,
+ onClick: handleDownload,
+ title: t('Herunterladen'),
+ loading: (row: UserFile) => downloadingFiles.has(row.id),
+ },
+ ]}
+ onDelete={handleDelete}
+ onDeleteMultiple={handleDeleteMultiple}
+ hookData={{
+ refetch: _tableRefetch,
+ pagination,
+ permissions,
+ handleDelete: handleFileDelete,
+ handleInlineUpdate,
+ updateOptimistically: updateFileOptimistically,
+ previewingFiles,
+ }}
+ emptyMessage={t('Keine Dateien gefunden')}
+ />
+
+ )}
{editingFile && (
@@ -378,7 +477,7 @@ export const FilesPage: React.FC = () => {
{t('Datei bearbeiten')}
-
+
{formAttributes.length === 0 ? (
From 0055b8ce44bba10baba012762160f025362f1663 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 3 May 2026 22:19:20 +0200
Subject: [PATCH 2/5] fixed ux for expand object scrolling
---
.../FormGeneratorTree/FormGeneratorTree.tsx | 20 +++++++++++++-
.../TreeNavigation/TreeNavigation.tsx | 26 ++++++++++++++-----
2 files changed, 39 insertions(+), 7 deletions(-)
diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx
index aa1bee9..e480963 100644
--- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx
+++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx
@@ -452,6 +452,7 @@ export function FormGeneratorTree({
const _handleToggleExpand = useCallback(
async (id: string) => {
+ const wasExpanded = expandedIds.has(id);
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
@@ -463,7 +464,7 @@ export function FormGeneratorTree({
});
const node = nodes.find((n) => n.id === id);
- if (node && !expandedIds.has(id)) {
+ if (node && !wasExpanded) {
const childMap = _buildChildMap(nodes);
const existingChildren = childMap.get(id);
if (!existingChildren || existingChildren.length === 0) {
@@ -472,11 +473,28 @@ export function FormGeneratorTree({
setNodes((prev) => [...prev, ...childNodes]);
}
}
+ setTimeout(() => {
+ _scrollExpandedNodeToCenter(id);
+ }, 50);
}
},
[nodes, expandedIds, provider, ownership],
);
+ const _scrollExpandedNodeToCenter = useCallback((nodeId: string) => {
+ const container = treeContentRef.current;
+ if (!container) return;
+ const el = container.querySelector(`[data-node-id="${nodeId}"]`) as HTMLElement | null;
+ if (!el) return;
+ const containerRect = container.getBoundingClientRect();
+ const elRect = el.getBoundingClientRect();
+ const midpoint = containerRect.top + containerRect.height / 2;
+ if (elRect.top > midpoint) {
+ const scrollTarget = container.scrollTop + (elRect.top - midpoint);
+ container.scrollTo({ top: scrollTarget, behavior: 'smooth' });
+ }
+ }, []);
+
const _handleToggleSelect = useCallback(
(id: string, e: React.MouseEvent) => {
const newSelection = new Set(selectedIds);
diff --git a/src/components/Navigation/TreeNavigation/TreeNavigation.tsx b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx
index 4b9beee..2f81d1b 100644
--- a/src/components/Navigation/TreeNavigation/TreeNavigation.tsx
+++ b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx
@@ -10,7 +10,7 @@
* - NavLink integration with React Router
*/
-import React, { useState, useEffect, ReactNode } from 'react';
+import React, { useState, useEffect, useRef, useCallback, ReactNode } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import styles from './TreeNavigation.module.css';
@@ -151,6 +151,7 @@ const TreeNode: React.FC = ({
const [isExpanded, setIsExpanded] = useState(
node.defaultExpanded ?? shouldAutoExpand ?? false
);
+ const containerRef = useRef(null);
// Auto-expand when path becomes active
useEffect(() => {
@@ -159,6 +160,16 @@ const TreeNode: React.FC = ({
}
}, [currentPath, autoExpandActive, node]);
+ const _scrollAfterExpand = useCallback(() => {
+ const el = containerRef.current;
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ const viewportMid = window.innerHeight / 2;
+ if (rect.top > viewportMid) {
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ }
+ }, []);
+
// Check if this node is active (exact match or ancestor of active path)
const isActive = node.path ? currentPath === node.path || currentPath.startsWith(node.path + '/') : false;
// Differentiate: leaf active (strong highlight) vs group active (subtle text only)
@@ -179,12 +190,13 @@ const TreeNode: React.FC = ({
}
if (isExpandable && !node.path) {
- // If only expandable (no path), toggle expand
- setIsExpanded(!isExpanded);
+ const willExpand = !isExpanded;
+ setIsExpanded(willExpand);
+ if (willExpand) setTimeout(_scrollAfterExpand, 50);
} else if (isExpandable && node.path) {
- // If both expandable and has path, expand on click but allow navigation
if (!isExpanded) {
setIsExpanded(true);
+ setTimeout(_scrollAfterExpand, 50);
}
}
@@ -197,7 +209,9 @@ const TreeNode: React.FC = ({
const handleToggleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
- setIsExpanded(!isExpanded);
+ const willExpand = !isExpanded;
+ setIsExpanded(willExpand);
+ if (willExpand) setTimeout(_scrollAfterExpand, 50);
};
// Render the node content (actions are rendered outside to avoid button-in-button nesting)
@@ -255,7 +269,7 @@ const TreeNode: React.FC = ({
const canRenderChildren = maxDepth === 0 || level < maxDepth;
return (
-
+
{nodeElement}
{node.actions && (
e.stopPropagation()}>
From 1219d616d427dc2f94991fa7adc7ed3db369f451 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 3 May 2026 22:24:47 +0200
Subject: [PATCH 3/5] fix import
---
src/components/UnifiedDataBar/FilesTab.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/components/UnifiedDataBar/FilesTab.tsx b/src/components/UnifiedDataBar/FilesTab.tsx
index 388ce93..43a20e4 100644
--- a/src/components/UnifiedDataBar/FilesTab.tsx
+++ b/src/components/UnifiedDataBar/FilesTab.tsx
@@ -1,5 +1,4 @@
import React, { useCallback, useRef, useMemo, useState } from 'react';
-import { FaFileImport } from 'react-icons/fa';
import type { UdbContext } from './UnifiedDataBar';
import api from '../../api';
import { useApiRequest } from '../../hooks/useApi';
From a912db3feedf4b5b57e0e9cdf3a56bc56f030318 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 6 May 2026 23:28:15 +0200
Subject: [PATCH 4/5] refactored comcoach und teamsbot
---
src/App.tsx | 7 +-
src/api/commcoachApi.ts | 158 +++--
src/api/teamsbotApi.ts | 46 ++
.../FormGeneratorTree.module.css | 29 +-
.../FormGeneratorTree/FormGeneratorTree.tsx | 87 +--
src/components/UnifiedDataBar/FilesTab.tsx | 12 +-
.../UnifiedDataBar/UnifiedDataBar.tsx | 2 +-
src/hooks/useCommcoach.ts | 30 +
src/hooks/useTtsPlayback.ts | 5 +-
src/layouts/MainLayout.tsx | 2 +-
src/pages/FeatureView.tsx | 14 +-
.../views/commcoach/Commcoach.module.css | 349 ++++++++++
.../commcoach/CommcoachAssistantView.tsx | 175 +++++
.../commcoach/CommcoachDashboardView.tsx | 31 +-
.../commcoach/CommcoachDossierView.module.css | 20 +-
.../views/commcoach/CommcoachDossierView.tsx | 44 +-
.../views/commcoach/CommcoachKeepAlive.tsx | 21 +-
.../views/commcoach/CommcoachModulesView.tsx | 293 +++++++++
.../views/commcoach/CommcoachSessionView.tsx | 610 ++++++++++++++++++
.../CommcoachSettingsView.module.css | 132 ++++
.../views/commcoach/CommcoachSettingsView.tsx | 359 +++++++++--
src/pages/views/commcoach/index.ts | 3 +
src/pages/views/teamsbot/Teamsbot.module.css | 355 +++++++++-
.../views/teamsbot/TeamsbotAssistantView.tsx | 190 ++++++
.../views/teamsbot/TeamsbotDashboardView.tsx | 7 +-
.../views/teamsbot/TeamsbotModulesView.tsx | 189 ++++++
.../views/teamsbot/TeamsbotSessionView.tsx | 181 ++++--
src/types/mandate.ts | 9 +-
28 files changed, 3131 insertions(+), 229 deletions(-)
create mode 100644 src/pages/views/commcoach/Commcoach.module.css
create mode 100644 src/pages/views/commcoach/CommcoachAssistantView.tsx
create mode 100644 src/pages/views/commcoach/CommcoachModulesView.tsx
create mode 100644 src/pages/views/commcoach/CommcoachSessionView.tsx
create mode 100644 src/pages/views/teamsbot/TeamsbotAssistantView.tsx
create mode 100644 src/pages/views/teamsbot/TeamsbotModulesView.tsx
diff --git a/src/App.tsx b/src/App.tsx
index aac8210..c81e8fb 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -179,12 +179,15 @@ function App() {
} />
} />
+ {/* Shared: assistant + modules routes (ComCoach + TeamsBot) */}
+ } />
+ } />
+
{/* Neutralization Feature Views */}
} />
{/* CommCoach Feature Views */}
- } />
- } />
+ } />
{/* Redmine Feature Views */}
} />
diff --git a/src/api/commcoachApi.ts b/src/api/commcoachApi.ts
index df0ed6c..5d758a1 100644
--- a/src/api/commcoachApi.ts
+++ b/src/api/commcoachApi.ts
@@ -109,8 +109,8 @@ export interface CoachingUserProfile {
}
export interface DashboardData {
- totalContexts: number;
- activeContexts: number;
+ totalModules: number;
+ activeModules: number;
totalSessions: number;
totalMinutes: number;
streakDays: number;
@@ -122,7 +122,11 @@ export interface DashboardData {
goalProgress?: number;
badges?: CoachingBadge[];
level?: { number: number; label: string; totalSessions: number };
- contexts: Array<{ id: string; title: string; category: string; sessionCount: number; lastSessionAt?: string; goalProgress?: number }>;
+ modules: Array<{ id: string; title: string; moduleType: string; sessionCount: number; lastSessionAt?: string; goalProgress?: number }>;
+ /** @deprecated Use totalModules/activeModules/modules instead */
+ totalContexts?: number;
+ activeContexts?: number;
+ contexts?: Array<{ id: string; title: string; category: string; sessionCount: number; lastSessionAt?: string; goalProgress?: number }>;
}
export interface SSEEvent {
@@ -133,31 +137,73 @@ export interface SSEEvent {
export type ApiRequestFunction = (options: ApiRequestOptions) => Promise;
+export function getApiRequest(): ApiRequestFunction {
+ return async (options: ApiRequestOptions) => {
+ const response = await api(options);
+ return response.data;
+ };
+}
+
// ============================================================================
-// Context API
+// Module API (TrainingModule — replaces Context API)
+// ============================================================================
+
+export async function listModulesApi(request: ApiRequestFunction, instanceId: string): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'get' });
+ return data.modules || [];
+}
+
+export async function createModuleApi(request: ApiRequestFunction, instanceId: string, body: {
+ title: string; moduleType?: string; goals?: string; personaId?: string; kpiTargets?: string;
+}): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'post', data: body });
+ return data.module;
+}
+
+export async function getModuleDetailApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}`, method: 'get' });
+ return data;
+}
+
+export async function updateModuleApi(request: ApiRequestFunction, instanceId: string, moduleId: string, body: any): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}`, method: 'put', data: body });
+ return data.module;
+}
+
+export async function deleteModuleApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise {
+ await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}`, method: 'delete' });
+}
+
+export async function listSessionsApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}/sessions`, method: 'get' });
+ return data.sessions || [];
+}
+
+// ============================================================================
+// Context / Module API (uses /modules/ endpoints)
// ============================================================================
export async function getContextsApi(request: ApiRequestFunction, instanceId: string): Promise {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts`, method: 'get' });
- return data.contexts || [];
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'get' });
+ return data.modules || [];
}
export async function createContextApi(request: ApiRequestFunction, instanceId: string, body: {
title: string; description?: string; category?: string; goals?: string[];
}): Promise {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts`, method: 'post', data: body });
- return data.context;
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'post', data: body });
+ return data.module;
}
export async function getContextDetailApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<{
context: CoachingContext; tasks: CoachingTask[]; scores: CoachingScore[]; sessions: CoachingSession[];
}> {
const data = await request({
- url: `/api/commcoach/${instanceId}/contexts/${contextId}`,
+ url: `/api/commcoach/${instanceId}/modules/${contextId}`,
method: 'get',
params: { _t: Date.now() },
});
- const ctx = data?.context ?? data;
+ const ctx = data?.module ?? data;
return {
context: ctx,
tasks: data?.tasks ?? [],
@@ -167,22 +213,22 @@ export async function getContextDetailApi(request: ApiRequestFunction, instanceI
}
export async function updateContextApi(request: ApiRequestFunction, instanceId: string, contextId: string, body: any): Promise {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}`, method: 'put', data: body });
- return data.context;
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}`, method: 'put', data: body });
+ return data.module;
}
export async function deleteContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise {
- await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}`, method: 'delete' });
+ await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}`, method: 'delete' });
}
export async function archiveContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/archive`, method: 'post' });
- return data.context;
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/archive`, method: 'post' });
+ return data.module;
}
export async function activateContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/activate`, method: 'post' });
- return data.context;
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/activate`, method: 'post' });
+ return data.module;
}
// ============================================================================
@@ -192,7 +238,7 @@ export async function activateContextApi(request: ApiRequestFunction, instanceId
export async function startSessionApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<{
session: CoachingSession; messages: CoachingMessage[]; resumed: boolean;
}> {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start`, method: 'post' });
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/sessions/start`, method: 'post' });
return data;
}
@@ -207,7 +253,7 @@ export async function startSessionStreamApi(
try {
const baseURL = api.defaults.baseURL || '';
const personaParam = personaId ? `?personaId=${encodeURIComponent(personaId)}` : '';
- const url = `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start${personaParam}`;
+ const url = `${baseURL}/api/commcoach/${instanceId}/modules/${contextId}/sessions/start${personaParam}`;
const headers: Record = { 'Content-Type': 'application/json' };
const authToken = localStorage.getItem('authToken');
@@ -243,14 +289,11 @@ export async function startSessionStreamApi(
for (const line of lines) {
if (line.startsWith('data: ')) {
- try {
- const jsonStr = line.slice(6);
- if (jsonStr.trim()) {
- const event: SSEEvent = JSON.parse(jsonStr);
- onEvent(event);
- }
- } catch {
- // skip malformed lines
+ const jsonStr = line.slice(6);
+ if (jsonStr.trim()) {
+ let event: SSEEvent;
+ try { event = JSON.parse(jsonStr); } catch { continue; }
+ onEvent(event);
}
}
}
@@ -348,14 +391,11 @@ export async function sendMessageStreamApi(
for (const line of lines) {
if (line.startsWith('data: ')) {
- try {
- const jsonStr = line.slice(6);
- if (jsonStr.trim()) {
- const event: SSEEvent = JSON.parse(jsonStr);
- onEvent(event);
- }
- } catch {
- // skip malformed lines
+ const jsonStr = line.slice(6);
+ if (jsonStr.trim()) {
+ let event: SSEEvent;
+ try { event = JSON.parse(jsonStr); } catch { continue; }
+ onEvent(event);
}
}
}
@@ -424,10 +464,12 @@ export async function sendAudioStreamApi(
for (const line of lines) {
if (line.startsWith('data: ')) {
- try {
- const jsonStr = line.slice(6);
- if (jsonStr.trim()) onEvent(JSON.parse(jsonStr));
- } catch { /* skip */ }
+ const jsonStr = line.slice(6);
+ if (jsonStr.trim()) {
+ let event: SSEEvent;
+ try { event = JSON.parse(jsonStr); } catch { continue; }
+ onEvent(event);
+ }
}
}
}
@@ -446,14 +488,14 @@ export async function sendAudioStreamApi(
// ============================================================================
export async function getTasksApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/tasks`, method: 'get' });
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/tasks`, method: 'get' });
return data.tasks || [];
}
export async function createTaskApi(request: ApiRequestFunction, instanceId: string, contextId: string, body: {
title: string; description?: string; priority?: string; dueDate?: string;
}): Promise {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/tasks`, method: 'post', data: body });
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/tasks`, method: 'post', data: body });
return data.task;
}
@@ -500,7 +542,14 @@ export async function updateProfileApi(request: ApiRequestFunction, instanceId:
export async function getPersonasApi(request: ApiRequestFunction, instanceId: string): Promise {
const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'get' });
- return data.personas || [];
+ return data.items || data.personas || [];
+}
+
+export async function fetchPersonasPaginated(request: ApiRequestFunction, instanceId: string, params?: any): Promise {
+ const queryParams: Record = {};
+ if (params) queryParams.pagination = JSON.stringify(params);
+ const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'get', params: queryParams });
+ return data;
}
export async function createPersonaApi(request: ApiRequestFunction, instanceId: string, body: {
@@ -510,10 +559,31 @@ export async function createPersonaApi(request: ApiRequestFunction, instanceId:
return data.persona;
}
+export async function updatePersonaApi(request: ApiRequestFunction, instanceId: string, personaId: string, body: {
+ label?: string; description?: string; gender?: string; systemPromptOverride?: string; isActive?: boolean;
+}): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/personas/${personaId}`, method: 'put', data: body });
+ return data.persona;
+}
+
export async function deletePersonaApi(request: ApiRequestFunction, instanceId: string, personaId: string): Promise {
await request({ url: `/api/commcoach/${instanceId}/personas/${personaId}`, method: 'delete' });
}
+// ============================================================================
+// Module-Persona Mapping API
+// ============================================================================
+
+export async function getModulePersonasApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}/personas`, method: 'get' });
+ return data.personaIds || [];
+}
+
+export async function setModulePersonasApi(request: ApiRequestFunction, instanceId: string, moduleId: string, personaIds: string[]): Promise {
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}/personas`, method: 'put', data: { personaIds } });
+ return data.personaIds || [];
+}
+
// ============================================================================
// Badge API (Iteration 2)
// ============================================================================
@@ -529,7 +599,7 @@ export async function getBadgesApi(request: ApiRequestFunction, instanceId: stri
export function getDossierExportUrl(instanceId: string, contextId: string, format: string = 'md'): string {
const baseURL = api.defaults.baseURL || '';
- return `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/export?format=${format}`;
+ return `${baseURL}/api/commcoach/${instanceId}/modules/${contextId}/export?format=${format}`;
}
export function getSessionExportUrl(instanceId: string, sessionId: string, format: string = 'md'): string {
@@ -544,6 +614,6 @@ export function getSessionExportUrl(instanceId: string, sessionId: string, forma
export async function getScoreHistoryApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise>> {
- const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/scores/history`, method: 'get' });
+ const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/scores/history`, method: 'get' });
return data.history || {};
}
diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts
index 462268d..3918f7c 100644
--- a/src/api/teamsbotApi.ts
+++ b/src/api/teamsbotApi.ts
@@ -9,6 +9,7 @@ export interface TeamsbotSession {
id: string;
instanceId: string;
mandateId: string;
+ moduleId?: string;
meetingLink: string;
botName: string;
status: 'pending' | 'joining' | 'active' | 'leaving' | 'ended' | 'error';
@@ -574,3 +575,48 @@ export async function deleteDirectorPrompt(
);
return response.data;
}
+
+
+// ============================================================================
+// Meeting Module API
+// ============================================================================
+
+export interface MeetingModule {
+ id: string;
+ instanceId: string;
+ mandateId: string;
+ ownerUserId: string;
+ title: string;
+ seriesType: string;
+ defaultBotId?: string;
+ defaultDirectorPrompts?: string;
+ goals?: string;
+ kpiTargets?: string;
+ status: string;
+}
+
+export async function listModules(instanceId: string): Promise