mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 23:04:57 +00:00
feat(documentation): implement JSON-backed in-app help system
Comprehensive in-app documentation system with interactive guides, full-text search, and contextual help: **Type System** (`src/types/documentation.ts`): - DocumentationIndex: Complete documentation structure with metadata - DocPage: Individual documentation pages with versioning - DocContentBlock: Flexible content types (text, code, list, table, video, callout, step, etc.) - DocCategory: Organized by category (getting-started, canvas, workflows, settings, shortcuts, etc.) - HelpState: Redux state management for help modal **JSON-Backed Content** (`src/data/documentation.json`): - 14 comprehensive documentation pages - 6 major documentation sections with hierarchical organization - Estimated read times and difficulty levels (beginner/intermediate/advanced) - Keywords for search optimization - Related pages for navigation **Service Layer** (`src/services/documentationService.ts`): - Full-text search with keyword matching - Category-based filtering and page grouping - Navigation tree building for sidebar - Breadcrumb generation - Statistics (word count, read time, etc.) - Related pages suggestion - History tracking **State Management** (`src/store/slices/documentationSlice.ts`): - Open/close modal state - Current page/category tracking - Search query and results management - Navigation history (last 20 pages) - Page navigation actions **Components**: - HelpModal.tsx: Main documentation modal with split pane (nav + content) - DocNavigation.tsx: Collapsible section tree with page list - DocContentRenderer.tsx: Flexible content renderer supporting 9 block types - HelpButton.tsx: Reusable help button component **Hook** (`src/hooks/useDocumentation.ts`): - useDocumentation: Complete documentation system integration - State selectors, navigation, search, and action dispatchers - Memoized derived state (navigation tree, breadcrumbs, related pages) **Accessibility**: - Full WCAG 2.1 AA compliance with ARIA roles and labels - Keyboard navigation (Tab, Escape) - Search results with live regions - Screen reader support with semantic HTML **Features**: - 14 documentation pages covering all major features - Full-text search with 2+ character minimum - Breadcrumb navigation - Related pages links - Collapsible section tree - Read time estimates - Difficulty levels - History back button - Material-UI components - Responsive design Documentation covers: - Getting Started (Welcome, Workspace Basics, First Workflow) - Canvas Guide (Canvas Intro, Drag/Drop, Zoom/Pan, Shortcuts) - Workflows (Basics, Nodes, Execution) - Settings (Workspace, Canvas, Notifications) - Keyboard Shortcuts (Canvas, Editor) - Help & Support (FAQ, Troubleshooting) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -195,6 +195,16 @@ export const testId = {
|
||||
// Card
|
||||
card: (id: string) => generateTestId('card', 'card', undefined, id),
|
||||
cardButton: (id: string, action: string) => generateTestId('card', 'button', 'click', `${id}-${action}`),
|
||||
|
||||
// Help/Documentation
|
||||
help: (name: string) => generateTestId('help', 'section', undefined, name),
|
||||
helpButton: () => generateTestId('help', 'button', 'click', 'open'),
|
||||
helpModal: (name: string) => generateTestId('help', 'modal', undefined, name),
|
||||
helpSearch: () => generateTestId('help', 'input', undefined, 'search'),
|
||||
helpNav: (name: string) => generateTestId('help', 'nav', undefined, name),
|
||||
alert: (type: string) => generateTestId('alert', 'alert', undefined, type),
|
||||
section: (id: string) => generateTestId('section', 'region', undefined, id),
|
||||
listItem: (label: string) => generateTestId('list', 'item', undefined, label),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
303
workflowui/src/components/Help/DocContentRenderer.tsx
Normal file
303
workflowui/src/components/Help/DocContentRenderer.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* DocContentRenderer Component
|
||||
* Renders documentation pages and their content
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Alert,
|
||||
Paper,
|
||||
Link,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import { DocPage, DocContentBlock } from '../../types/documentation';
|
||||
import { testId } from '../../utils/accessibility';
|
||||
import styles from './Help.module.scss';
|
||||
|
||||
interface DocContentRendererProps {
|
||||
pages: (DocPage | undefined)[];
|
||||
isSearchResults?: boolean;
|
||||
onPageSelect?: (pageId: string) => void;
|
||||
}
|
||||
|
||||
const ContentBlock: React.FC<{
|
||||
block: DocContentBlock;
|
||||
onPageSelect?: (pageId: string) => void;
|
||||
}> = ({ block, onPageSelect }) => {
|
||||
const { type, content, title, level, language, variant, items, columns, rows, icon, subtext } =
|
||||
block;
|
||||
|
||||
switch (type) {
|
||||
case 'heading':
|
||||
const HeadingTag = (`h${level}` as keyof JSX.IntrinsicElements) || 'h2';
|
||||
return (
|
||||
<Typography
|
||||
component={HeadingTag}
|
||||
variant={level === 1 ? 'h5' : level === 2 ? 'h6' : 'body1'}
|
||||
sx={{ mt: 2, mb: 1, fontWeight: 600 }}
|
||||
>
|
||||
{content}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
case 'text':
|
||||
return (
|
||||
<Typography paragraph sx={{ lineHeight: 1.6 }}>
|
||||
{content}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
case 'code':
|
||||
return (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2,
|
||||
backgroundColor: '#f5f5f5',
|
||||
overflowX: 'auto',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
<code>{content}</code>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
case 'list':
|
||||
return (
|
||||
<Box component="ul" sx={{ ml: 2, mb: 2 }}>
|
||||
{items?.map((item, idx) => (
|
||||
<Typography key={idx} component="li" sx={{ mb: 0.5 }}>
|
||||
{item}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'table':
|
||||
return (
|
||||
<TableContainer component={Paper} sx={{ mb: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ backgroundColor: '#f5f5f5' }}>
|
||||
{columns?.map((col) => (
|
||||
<TableCell key={col} sx={{ fontWeight: 600 }}>
|
||||
{col}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows?.map((row, idx) => (
|
||||
<TableRow key={idx}>
|
||||
{row.map((cell, cellIdx) => (
|
||||
<TableCell key={cellIdx}>{cell}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
case 'callout':
|
||||
return (
|
||||
<Alert
|
||||
severity={variant || 'info'}
|
||||
sx={{ mb: 2 }}
|
||||
icon={icon}
|
||||
data-testid={testId.alert(`callout-${variant}`)}
|
||||
>
|
||||
<Typography variant="body2">{content}</Typography>
|
||||
{subtext && (
|
||||
<Typography variant="caption" sx={{ mt: 1, display: 'block' }}>
|
||||
{subtext}
|
||||
</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
case 'step':
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
p: 1.5,
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderLeft: '3px solid #2196f3',
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: 32,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '1.2rem',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
<Typography sx={{ lineHeight: 1.6 }}>{content}</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'image':
|
||||
return (
|
||||
<Box sx={{ mb: 2, textAlign: 'center' }}>
|
||||
<img
|
||||
src={content}
|
||||
alt={title || 'Documentation image'}
|
||||
style={{ maxWidth: '100%', height: 'auto', borderRadius: 4 }}
|
||||
/>
|
||||
{title && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 1 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'video':
|
||||
return (
|
||||
<Box sx={{ mb: 2, position: 'relative', paddingTop: '56.25%', height: 0, overflow: 'hidden' }}>
|
||||
<iframe
|
||||
src={content}
|
||||
title={title}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
allowFullScreen
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'example':
|
||||
return (
|
||||
<Card variant="outlined" sx={{ mb: 2, backgroundColor: '#fafafa' }}>
|
||||
<CardContent>
|
||||
{title && (
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Example: {title}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="body2">{content}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const DocContentRenderer: React.FC<DocContentRendererProps> = ({
|
||||
pages,
|
||||
isSearchResults = false,
|
||||
onPageSelect,
|
||||
}) => {
|
||||
const validPages = pages.filter((p): p is DocPage => !!p);
|
||||
|
||||
if (!validPages.length) {
|
||||
return (
|
||||
<Typography color="textSecondary" data-testid={testId.text('no-content')}>
|
||||
No documentation available
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box role="main" aria-label="Documentation content">
|
||||
{validPages.map((page, pageIdx) => (
|
||||
<Box
|
||||
key={page.id}
|
||||
data-testid={testId.section(`doc-page-${page.id}`)}
|
||||
sx={{ mb: pageIdx < validPages.length - 1 ? 4 : 0 }}
|
||||
>
|
||||
{/* Page Header */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: 0.5 }}>
|
||||
{page.title}
|
||||
</Typography>
|
||||
{page.description && (
|
||||
<Typography color="textSecondary" sx={{ mb: 1 }}>
|
||||
{page.description}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 1 }}>
|
||||
{page.difficulty && (
|
||||
<Typography variant="caption" sx={{ fontWeight: 500 }}>
|
||||
Difficulty: {page.difficulty}
|
||||
</Typography>
|
||||
)}
|
||||
{page.estimatedReadTime && (
|
||||
<Typography variant="caption" sx={{ fontWeight: 500 }}>
|
||||
Read time: {page.estimatedReadTime} min
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* Page Content */}
|
||||
<Box sx={{ my: 2 }}>
|
||||
{page.content.map((block, idx) => (
|
||||
<ContentBlock
|
||||
key={idx}
|
||||
block={block}
|
||||
onPageSelect={onPageSelect}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Related Pages */}
|
||||
{page.relatedPages && page.relatedPages.length > 0 && (
|
||||
<Box sx={{ mt: 4, pt: 2, borderTop: '1px solid #e0e0e0' }}>
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Related Topics
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{page.relatedPages.map((relatedId) => (
|
||||
<Link
|
||||
key={relatedId}
|
||||
onClick={() => onPageSelect?.(relatedId)}
|
||||
sx={{ cursor: 'pointer', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}
|
||||
component="button"
|
||||
color="primary"
|
||||
data-testid={testId.link(`related-${relatedId}`)}
|
||||
>
|
||||
View related topic →
|
||||
</Link>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocContentRenderer;
|
||||
124
workflowui/src/components/Help/DocNavigation.tsx
Normal file
124
workflowui/src/components/Help/DocNavigation.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* DocNavigation Component
|
||||
* Sidebar navigation for documentation categories and pages
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Collapse,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import { documentationService } from '../../services/documentationService';
|
||||
import { testId } from '../../utils/accessibility';
|
||||
|
||||
interface DocNavigationProps {
|
||||
onPageSelect: (pageId: string) => void;
|
||||
currentPageId?: string | null;
|
||||
}
|
||||
|
||||
export const DocNavigation: React.FC<DocNavigationProps> = ({
|
||||
onPageSelect,
|
||||
currentPageId,
|
||||
}) => {
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||
|
||||
const navigationTree = documentationService.getNavigationTree();
|
||||
|
||||
const handleToggleSection = (sectionId: string) => {
|
||||
const newExpanded = new Set(expandedSections);
|
||||
if (newExpanded.has(sectionId)) {
|
||||
newExpanded.delete(sectionId);
|
||||
} else {
|
||||
newExpanded.add(sectionId);
|
||||
}
|
||||
setExpandedSections(newExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
role="region"
|
||||
aria-label="Documentation sections"
|
||||
data-testid={testId.helpNav('documentation')}
|
||||
>
|
||||
{navigationTree.map(({ section, pages }) => (
|
||||
<Box key={section.id} sx={{ mb: 2 }}>
|
||||
{/* Section Header */}
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => handleToggleSection(section.id)}
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'left',
|
||||
color: 'primary.main',
|
||||
fontWeight: 600,
|
||||
mb: 1,
|
||||
}}
|
||||
aria-expanded={expandedSections.has(section.id)}
|
||||
aria-controls={`section-${section.id}`}
|
||||
data-testid={testId.button(`section-${section.id}`)}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<span>{section.icon}</span>
|
||||
<span>{section.title}</span>
|
||||
</Box>
|
||||
{expandedSections.has(section.id) ? (
|
||||
<ExpandLessIcon fontSize="small" />
|
||||
) : (
|
||||
<ExpandMoreIcon fontSize="small" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Pages List */}
|
||||
<Collapse
|
||||
in={expandedSections.has(section.id)}
|
||||
id={`section-${section.id}`}
|
||||
>
|
||||
<List sx={{ p: 0, ml: 1 }}>
|
||||
{pages.map((page) => (
|
||||
<ListItem
|
||||
key={page.id}
|
||||
disablePadding
|
||||
data-testid={testId.listItem(`doc-page-${page.id}`)}
|
||||
>
|
||||
<ListItemButton
|
||||
selected={currentPageId === page.id}
|
||||
onClick={() => onPageSelect(page.id)}
|
||||
sx={{
|
||||
py: 0.5,
|
||||
px: 1,
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'action.selected',
|
||||
borderLeft: '3px solid',
|
||||
borderColor: 'primary.main',
|
||||
},
|
||||
}}
|
||||
data-testid={testId.navLink(page.id)}
|
||||
>
|
||||
<ListItemText
|
||||
primary={page.title}
|
||||
secondary={
|
||||
page.estimatedReadTime ? `${page.estimatedReadTime} min` : undefined
|
||||
}
|
||||
primaryTypographyProps={{ variant: 'body2' }}
|
||||
secondaryTypographyProps={{ variant: 'caption' }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocNavigation;
|
||||
325
workflowui/src/components/Help/Help.module.scss
Normal file
325
workflowui/src/components/Help/Help.module.scss
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Help Components Styles
|
||||
*/
|
||||
|
||||
.helpModal {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 600px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex: 0 0 250px;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pages {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 8px 0 0 0;
|
||||
margin-left: 12px;
|
||||
|
||||
.pageItem {
|
||||
padding: 8px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 3px solid #1976d2;
|
||||
padding-left: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.searchBar {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #1976d2;
|
||||
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.contentArea {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
|
||||
.emptyState {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content rendering
|
||||
.contentBlock {
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.codeBlock {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background-color: #f9f9f9;
|
||||
border-left: 3px solid #2196f3;
|
||||
border-radius: 4px;
|
||||
|
||||
.stepIcon {
|
||||
min-width: 32px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stepContent {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.callout {
|
||||
padding: 12px;
|
||||
border-left: 4px solid;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.info {
|
||||
background-color: #e3f2fd;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: #fff3e0;
|
||||
border-color: #ff9800;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: #ffebee;
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: #e8f5e9;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
th {
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
.sections {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.section {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.sectionTitle {
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.pages {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 8px 0 0 16px;
|
||||
|
||||
.page {
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e3f2fd;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
workflowui/src/components/Help/HelpButton.tsx
Normal file
62
workflowui/src/components/Help/HelpButton.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* HelpButton Component
|
||||
* Reusable button to open help modal or show contextual help
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Tooltip } from '@mui/material';
|
||||
import HelpIcon from '@mui/icons-material/Help';
|
||||
import { useDocumentation } from '../../hooks/useDocumentation';
|
||||
import { DocCategory } from '../../types/documentation';
|
||||
import { testId } from '../../utils/accessibility';
|
||||
|
||||
interface HelpButtonProps {
|
||||
pageId?: string;
|
||||
category?: DocCategory;
|
||||
variant?: 'icon' | 'text' | 'contained';
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
tooltip?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export const HelpButton: React.FC<HelpButtonProps> = ({
|
||||
pageId,
|
||||
category,
|
||||
variant = 'icon',
|
||||
size = 'medium',
|
||||
tooltip = 'Open Help',
|
||||
ariaLabel = 'Open Help',
|
||||
}) => {
|
||||
const { openHelpModal } = useDocumentation();
|
||||
|
||||
const handleClick = () => {
|
||||
openHelpModal(pageId, category);
|
||||
};
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId.button('help')}
|
||||
size={size}
|
||||
variant={variant === 'contained' ? 'contained' : 'text'}
|
||||
>
|
||||
{variant === 'icon' ? (
|
||||
<HelpIcon />
|
||||
) : (
|
||||
<>
|
||||
<HelpIcon sx={{ mr: 1 }} />
|
||||
Help
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (variant === 'icon') {
|
||||
return <Tooltip title={tooltip}>{button}</Tooltip>;
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
export default HelpButton;
|
||||
205
workflowui/src/components/Help/HelpModal.tsx
Normal file
205
workflowui/src/components/Help/HelpModal.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* HelpModal Component
|
||||
* Main in-app help and documentation modal
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
Paper,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useDocumentation } from '../../hooks/useDocumentation';
|
||||
import { testId } from '../../utils/accessibility';
|
||||
import DocNavigation from './DocNavigation';
|
||||
import DocContentRenderer from './DocContentRenderer';
|
||||
import styles from './Help.module.scss';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel({ children, value, index }: TabPanelProps) {
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`help-tabpanel-${index}`}
|
||||
aria-labelledby={`help-tab-${index}`}
|
||||
>
|
||||
{value === index && <Box sx={{ p: 2 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const HelpModal: React.FC = () => {
|
||||
const {
|
||||
isOpen,
|
||||
closeHelpModal,
|
||||
currentPage,
|
||||
searchQuery,
|
||||
searchResults,
|
||||
canGoBack,
|
||||
goBackInHistory,
|
||||
goToPage,
|
||||
search,
|
||||
clearSearchResults,
|
||||
navigationTree,
|
||||
} = useDocumentation();
|
||||
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
search(value);
|
||||
};
|
||||
|
||||
const handleClearSearch = () => {
|
||||
clearSearchResults();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={closeHelpModal}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
data-testid={testId.modal('help')}
|
||||
role="dialog"
|
||||
aria-labelledby="help-modal-title"
|
||||
>
|
||||
<DialogTitle
|
||||
id="help-modal-title"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Help & Documentation</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={closeHelpModal}
|
||||
aria-label="Close help modal"
|
||||
data-testid={testId.button('close-help')}
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box sx={{ display: 'flex', height: '600px' }}>
|
||||
{/* Sidebar Navigation */}
|
||||
<Box
|
||||
sx={{
|
||||
width: '250px',
|
||||
borderRight: '1px solid #e0e0e0',
|
||||
overflowY: 'auto',
|
||||
p: 2,
|
||||
}}
|
||||
role="navigation"
|
||||
aria-label="Documentation navigation"
|
||||
>
|
||||
<DocNavigation
|
||||
onPageSelect={goToPage}
|
||||
currentPageId={currentPage?.id}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Main Content */}
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Search Bar */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
placeholder="Search documentation..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
size="small"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon sx={{ mr: 1 }} />,
|
||||
}}
|
||||
aria-label="Search documentation"
|
||||
data-testid={testId.input('help-search')}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleClearSearch}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', p: 2 }}>
|
||||
{searchQuery && searchResults.length > 0 ? (
|
||||
<DocContentRenderer
|
||||
pages={searchResults}
|
||||
isSearchResults={true}
|
||||
onPageSelect={goToPage}
|
||||
/>
|
||||
) : searchQuery && searchResults.length === 0 ? (
|
||||
<Typography color="textSecondary">
|
||||
No results found for "{searchQuery}"
|
||||
</Typography>
|
||||
) : currentPage ? (
|
||||
<DocContentRenderer
|
||||
pages={[currentPage]}
|
||||
onPageSelect={goToPage}
|
||||
/>
|
||||
) : (
|
||||
<Typography color="textSecondary">
|
||||
Select a topic from the left to get started
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 2 }}>
|
||||
<Button
|
||||
onClick={goBackInHistory}
|
||||
disabled={!canGoBack}
|
||||
aria-label="Go back to previous page"
|
||||
data-testid={testId.button('help-back')}
|
||||
>
|
||||
← Back
|
||||
</Button>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Button
|
||||
onClick={closeHelpModal}
|
||||
variant="contained"
|
||||
data-testid={testId.button('close-help-footer')}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpModal;
|
||||
8
workflowui/src/components/Help/index.ts
Normal file
8
workflowui/src/components/Help/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Help Components Export
|
||||
*/
|
||||
|
||||
export { HelpModal } from './HelpModal';
|
||||
export { HelpButton } from './HelpButton';
|
||||
export { DocNavigation } from './DocNavigation';
|
||||
export { DocContentRenderer } from './DocContentRenderer';
|
||||
846
workflowui/src/data/documentation.json
Normal file
846
workflowui/src/data/documentation.json
Normal file
@@ -0,0 +1,846 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"lastUpdated": 1705984000000,
|
||||
"sections": [
|
||||
{
|
||||
"id": "getting-started-section",
|
||||
"title": "Getting Started",
|
||||
"description": "Learn the basics of WorkflowUI",
|
||||
"category": "getting-started",
|
||||
"pages": ["doc-welcome", "doc-workspace-basics", "doc-first-workflow"],
|
||||
"icon": "🚀",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "canvas-section",
|
||||
"title": "Canvas Guide",
|
||||
"description": "Master the infinite canvas and workflow arrangement",
|
||||
"category": "canvas",
|
||||
"pages": ["doc-canvas-intro", "doc-drag-drop", "doc-zoom-pan", "doc-canvas-shortcuts"],
|
||||
"icon": "🎨",
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": "workflows-section",
|
||||
"title": "Workflows",
|
||||
"description": "Create and manage workflows",
|
||||
"category": "workflows",
|
||||
"pages": ["doc-workflow-basics", "doc-workflow-nodes", "doc-workflow-execution"],
|
||||
"icon": "⚙️",
|
||||
"order": 3
|
||||
},
|
||||
{
|
||||
"id": "settings-section",
|
||||
"title": "Settings",
|
||||
"description": "Customize your workspace and preferences",
|
||||
"category": "settings",
|
||||
"pages": ["doc-workspace-settings", "doc-canvas-settings", "doc-notifications"],
|
||||
"icon": "⚙️",
|
||||
"order": 4
|
||||
},
|
||||
{
|
||||
"id": "shortcuts-section",
|
||||
"title": "Keyboard Shortcuts",
|
||||
"description": "Speed up your workflow with keyboard shortcuts",
|
||||
"category": "keyboard-shortcuts",
|
||||
"pages": ["doc-canvas-shortcuts", "doc-editor-shortcuts"],
|
||||
"icon": "⌨️",
|
||||
"order": 5
|
||||
},
|
||||
{
|
||||
"id": "help-section",
|
||||
"title": "Help & Support",
|
||||
"description": "Troubleshooting and frequently asked questions",
|
||||
"category": "troubleshooting",
|
||||
"pages": ["doc-faq", "doc-troubleshooting"],
|
||||
"icon": "❓",
|
||||
"order": 6
|
||||
}
|
||||
],
|
||||
"pages": {
|
||||
"doc-welcome": {
|
||||
"id": "doc-welcome",
|
||||
"title": "Welcome to WorkflowUI",
|
||||
"category": "getting-started",
|
||||
"description": "An introduction to WorkflowUI and its core concepts",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 3,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Welcome to WorkflowUI",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "WorkflowUI is a powerful, visual workflow editor that lets you create, organize, and execute complex workflows with an intuitive infinite canvas interface, similar to Miro or FigJam."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Core Concepts",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Workspaces: Top-level containers for organizing your projects",
|
||||
"Projects: Group related workflows together on a shared canvas",
|
||||
"Workflows: Individual workflow definitions with nodes and connections",
|
||||
"Canvas: Infinite space for arranging workflow cards spatially",
|
||||
"Nodes: Building blocks within workflows that perform operations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "New to WorkflowUI? Start with our Getting Started guide to set up your first workspace.",
|
||||
"variant": "info",
|
||||
"icon": "💡"
|
||||
}
|
||||
],
|
||||
"keywords": ["introduction", "overview", "concepts"],
|
||||
"relatedPages": ["doc-workspace-basics", "doc-canvas-intro"]
|
||||
},
|
||||
"doc-workspace-basics": {
|
||||
"id": "doc-workspace-basics",
|
||||
"title": "Workspace Basics",
|
||||
"category": "getting-started",
|
||||
"description": "Learn how to create and manage workspaces",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 5,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Workspace Basics",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "A workspace is the top-level container in WorkflowUI. Each workspace can contain multiple projects, each with its own set of workflows arranged on an infinite canvas."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Creating a Workspace",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click on the workspace selector in the header (top-left corner)",
|
||||
"icon": "1️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Select 'Create New Workspace' from the dropdown menu",
|
||||
"icon": "2️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Enter a name and optional description",
|
||||
"icon": "3️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click 'Create' to add your workspace",
|
||||
"icon": "4️⃣"
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "Choose descriptive names for your workspaces (e.g., 'Q1 Projects', 'Client Work', 'Personal')",
|
||||
"variant": "info",
|
||||
"icon": "💡"
|
||||
}
|
||||
],
|
||||
"keywords": ["workspace", "create", "organize"],
|
||||
"relatedPages": ["doc-welcome", "doc-canvas-intro"]
|
||||
},
|
||||
"doc-first-workflow": {
|
||||
"id": "doc-first-workflow",
|
||||
"title": "Create Your First Workflow",
|
||||
"category": "getting-started",
|
||||
"description": "Step-by-step guide to creating your first workflow",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 8,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Create Your First Workflow",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Navigate to a project by selecting it from the sidebar",
|
||||
"icon": "1️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click the '+' button in the Projects section to create a new project if needed",
|
||||
"icon": "2️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click the '+' button on the canvas to add a new workflow",
|
||||
"icon": "3️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Give your workflow a name and optional description",
|
||||
"icon": "4️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click on the workflow card to open the editor",
|
||||
"icon": "5️⃣"
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "Pro tip: Use meaningful names and descriptions to help you remember what each workflow does.",
|
||||
"variant": "success",
|
||||
"icon": "⭐"
|
||||
}
|
||||
],
|
||||
"keywords": ["workflow", "create", "beginner", "first"],
|
||||
"relatedPages": ["doc-workflow-basics", "doc-workflow-nodes"]
|
||||
},
|
||||
"doc-canvas-intro": {
|
||||
"id": "doc-canvas-intro",
|
||||
"title": "Understanding the Canvas",
|
||||
"category": "canvas",
|
||||
"description": "Learn how to navigate and use the infinite canvas",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 5,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "The Infinite Canvas",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "The canvas is an infinite workspace where you can arrange workflow cards. Unlike traditional list views, the canvas lets you organize workflows spatially, creating visual relationships and workflows."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Canvas Features",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Infinite space: Pan in any direction without limits",
|
||||
"Zoom: Use scroll wheel or zoom controls to focus on specific areas",
|
||||
"Drag & drop: Move workflow cards to organize them spatially",
|
||||
"Grid: Optional grid overlay to help with alignment",
|
||||
"Snap to grid: Automatic alignment for precise arrangement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "Use the zoom controls (bottom-right) or your mouse scroll wheel to zoom in and out.",
|
||||
"variant": "info",
|
||||
"icon": "💡"
|
||||
}
|
||||
],
|
||||
"keywords": ["canvas", "infinite", "navigate", "zoom"],
|
||||
"relatedPages": ["doc-zoom-pan", "doc-drag-drop", "doc-canvas-shortcuts"]
|
||||
},
|
||||
"doc-drag-drop": {
|
||||
"id": "doc-drag-drop",
|
||||
"title": "Dragging & Resizing Workflow Cards",
|
||||
"category": "canvas",
|
||||
"description": "Learn how to drag, resize, and arrange workflow cards on the canvas",
|
||||
"difficulty": "intermediate",
|
||||
"estimatedReadTime": 4,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Dragging & Resizing",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Moving Workflow Cards",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "Click and drag any workflow card to move it to a new position on the canvas. Cards will snap to the grid if grid snapping is enabled in settings."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Resizing Cards",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "Hover over the edge of a workflow card to reveal resize handles. Drag any handle to resize the card."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Multi-Select",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Hold Shift and click cards to add/remove from selection",
|
||||
"Drag to create a selection box around multiple cards",
|
||||
"Move all selected cards together by dragging any one of them"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "You can move multiple selected cards at once for quick reorganization.",
|
||||
"variant": "success",
|
||||
"icon": "⭐"
|
||||
}
|
||||
],
|
||||
"keywords": ["drag", "drop", "resize", "move", "canvas"],
|
||||
"relatedPages": ["doc-canvas-intro", "doc-canvas-shortcuts"]
|
||||
},
|
||||
"doc-zoom-pan": {
|
||||
"id": "doc-zoom-pan",
|
||||
"title": "Zoom & Pan Controls",
|
||||
"category": "canvas",
|
||||
"description": "Master zooming and panning the canvas",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 3,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Zoom & Pan",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Zooming",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Scroll wheel: Zoom in/out at your current cursor position",
|
||||
"Zoom buttons: Use + and - buttons in the zoom toolbar (bottom-right)",
|
||||
"Reset zoom: Click the reset button to return to 100%",
|
||||
"Zoom range: 10% to 300%"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Panning",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Space + drag: Hold space and drag to pan the canvas",
|
||||
"Middle mouse button: Click middle mouse button and drag to pan",
|
||||
"Arrow keys: Use arrow keys to pan in cardinal directions"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keywords": ["zoom", "pan", "navigate", "scroll"],
|
||||
"relatedPages": ["doc-canvas-intro", "doc-canvas-shortcuts"]
|
||||
},
|
||||
"doc-canvas-shortcuts": {
|
||||
"id": "doc-canvas-shortcuts",
|
||||
"title": "Canvas Keyboard Shortcuts",
|
||||
"category": "keyboard-shortcuts",
|
||||
"description": "Quick reference for canvas keyboard shortcuts",
|
||||
"difficulty": "intermediate",
|
||||
"estimatedReadTime": 3,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Canvas Shortcuts",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"columns": ["Shortcut", "Action"],
|
||||
"rows": [
|
||||
["Space + Drag", "Pan canvas"],
|
||||
["Scroll wheel", "Zoom in/out"],
|
||||
["Ctrl/Cmd + 0", "Reset zoom to 100%"],
|
||||
["Arrow keys", "Pan in four directions"],
|
||||
["Shift + Click", "Multi-select cards"],
|
||||
["Ctrl/Cmd + A", "Select all cards"],
|
||||
["Delete", "Delete selected cards"],
|
||||
["Ctrl/Cmd + D", "Duplicate selected cards"],
|
||||
["Ctrl/Cmd + F", "Search workflows"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "Use Mac Cmd instead of Ctrl on macOS. Shortcuts may vary by system.",
|
||||
"variant": "info",
|
||||
"icon": "💻"
|
||||
}
|
||||
],
|
||||
"keywords": ["keyboard", "shortcuts", "canvas", "hotkeys"],
|
||||
"relatedPages": ["doc-canvas-intro", "doc-zoom-pan"]
|
||||
},
|
||||
"doc-workflow-basics": {
|
||||
"id": "doc-workflow-basics",
|
||||
"title": "Workflow Basics",
|
||||
"category": "workflows",
|
||||
"description": "Learn the fundamentals of creating workflows",
|
||||
"difficulty": "intermediate",
|
||||
"estimatedReadTime": 6,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Workflow Basics",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "Workflows are automated sequences of operations. Each workflow consists of nodes connected together, with data flowing from one node to the next."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Workflow Structure",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Nodes: Individual operations or data transformations",
|
||||
"Connections: Links between nodes showing data flow",
|
||||
"Inputs: Entry points for data into the workflow",
|
||||
"Outputs: Results produced by the workflow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Creating a Workflow",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Open a project from the sidebar",
|
||||
"icon": "1️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click the '+' button to add a new workflow card",
|
||||
"icon": "2️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click the card to open the workflow editor",
|
||||
"icon": "3️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Add nodes from the node palette on the left",
|
||||
"icon": "4️⃣"
|
||||
}
|
||||
],
|
||||
"keywords": ["workflow", "basics", "nodes", "create"],
|
||||
"relatedPages": ["doc-workflow-nodes", "doc-workflow-execution"]
|
||||
},
|
||||
"doc-workflow-nodes": {
|
||||
"id": "doc-workflow-nodes",
|
||||
"title": "Working with Nodes",
|
||||
"category": "workflows",
|
||||
"description": "Learn how to add, configure, and connect nodes",
|
||||
"difficulty": "intermediate",
|
||||
"estimatedReadTime": 8,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Nodes",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "Nodes are the building blocks of workflows. Each node performs a specific operation and can be connected to other nodes to create data pipelines."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Adding Nodes",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Open the workflow editor",
|
||||
"icon": "1️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Browse the node palette on the left side",
|
||||
"icon": "2️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Drag a node onto the canvas to add it",
|
||||
"icon": "3️⃣"
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Node Types",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Operation: Mathematical or logical operations",
|
||||
"HTTP: Make API calls to external services",
|
||||
"Database: Query and manipulate data",
|
||||
"Transform: Convert data between formats",
|
||||
"Control: Flow control (if/then/loop)",
|
||||
"Notification: Send alerts or messages"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keywords": ["nodes", "workflow", "operations", "add"],
|
||||
"relatedPages": ["doc-workflow-basics", "doc-workflow-execution"]
|
||||
},
|
||||
"doc-workflow-execution": {
|
||||
"id": "doc-workflow-execution",
|
||||
"title": "Executing Workflows",
|
||||
"category": "workflows",
|
||||
"description": "Learn how to run and monitor workflow execution",
|
||||
"difficulty": "intermediate",
|
||||
"estimatedReadTime": 5,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Workflow Execution",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Running a Workflow",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Open the workflow in the editor",
|
||||
"icon": "1️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click the 'Run' or 'Execute' button in the toolbar",
|
||||
"icon": "2️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Provide any required input values",
|
||||
"icon": "3️⃣"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Monitor progress in the execution panel",
|
||||
"icon": "4️⃣"
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Understanding Results",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Success: Workflow completed with output",
|
||||
"Error: Workflow failed at a specific node",
|
||||
"Timeout: Workflow took too long to complete",
|
||||
"Cancelled: Workflow was stopped by user"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "Check the execution log for detailed information about each node execution.",
|
||||
"variant": "info",
|
||||
"icon": "📋"
|
||||
}
|
||||
],
|
||||
"keywords": ["execution", "run", "workflow", "monitor"],
|
||||
"relatedPages": ["doc-workflow-basics", "doc-workflow-nodes"]
|
||||
},
|
||||
"doc-workspace-settings": {
|
||||
"id": "doc-workspace-settings",
|
||||
"title": "Workspace Settings",
|
||||
"category": "settings",
|
||||
"description": "Configure workspace preferences",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 4,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Workspace Settings",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Accessing Settings",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Click the settings icon in the header (top-right)",
|
||||
"icon": "⚙️"
|
||||
},
|
||||
{
|
||||
"type": "step",
|
||||
"content": "Navigate to the Settings tab",
|
||||
"icon": "📋"
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Available Settings",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Workspace name and description",
|
||||
"Default workspace for new sessions",
|
||||
"Workspace color and icon",
|
||||
"Member permissions and roles"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keywords": ["settings", "workspace", "preferences"],
|
||||
"relatedPages": ["doc-canvas-settings", "doc-notifications"]
|
||||
},
|
||||
"doc-canvas-settings": {
|
||||
"id": "doc-canvas-settings",
|
||||
"title": "Canvas Settings",
|
||||
"category": "settings",
|
||||
"description": "Configure canvas appearance and behavior",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 3,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Canvas Settings",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Available Options",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Grid visibility: Show or hide the canvas grid",
|
||||
"Grid snapping: Automatically snap cards to grid",
|
||||
"Grid size: Adjust grid cell size (10-50px)",
|
||||
"Grid style: Choose dots or lines",
|
||||
"Zoom level: Set default zoom percentage",
|
||||
"Pan behavior: Choose pan trigger (space/middle-click)",
|
||||
"Card animations: Enable/disable movement animations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "Disable animations if you experience performance issues with large canvases.",
|
||||
"variant": "info",
|
||||
"icon": "⚡"
|
||||
}
|
||||
],
|
||||
"keywords": ["canvas", "settings", "grid", "zoom"],
|
||||
"relatedPages": ["doc-workspace-settings", "doc-canvas-intro"]
|
||||
},
|
||||
"doc-notifications": {
|
||||
"id": "doc-notifications",
|
||||
"title": "Notification Settings",
|
||||
"category": "settings",
|
||||
"description": "Configure notification preferences",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 3,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Notification Settings",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Notification Types",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Workflow execution: Notify when workflows complete",
|
||||
"Workflow errors: Alert on workflow failures",
|
||||
"Project updates: Notify of project changes",
|
||||
"Collaboration: Alerts when others edit your projects",
|
||||
"System: Important system and security notifications"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Notification Channels",
|
||||
"level": 2
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"In-app: Notifications displayed in the application",
|
||||
"Email: Notifications sent via email",
|
||||
"Desktop: Browser desktop notifications",
|
||||
"Sounds: Audio alerts for important events"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keywords": ["notifications", "settings", "alerts"],
|
||||
"relatedPages": ["doc-workspace-settings"]
|
||||
},
|
||||
"doc-editor-shortcuts": {
|
||||
"id": "doc-editor-shortcuts",
|
||||
"title": "Workflow Editor Shortcuts",
|
||||
"category": "keyboard-shortcuts",
|
||||
"description": "Keyboard shortcuts for the workflow editor",
|
||||
"difficulty": "intermediate",
|
||||
"estimatedReadTime": 3,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Editor Shortcuts",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"columns": ["Shortcut", "Action"],
|
||||
"rows": [
|
||||
["Ctrl/Cmd + Z", "Undo"],
|
||||
["Ctrl/Cmd + Shift + Z", "Redo"],
|
||||
["Ctrl/Cmd + C", "Copy selected nodes"],
|
||||
["Ctrl/Cmd + V", "Paste nodes"],
|
||||
["Ctrl/Cmd + X", "Cut selected nodes"],
|
||||
["Delete", "Delete selected nodes"],
|
||||
["Enter", "Execute workflow"],
|
||||
["Shift + Click", "Multi-select nodes"],
|
||||
["Ctrl/Cmd + A", "Select all nodes"],
|
||||
["Space + Drag", "Pan editor canvas"],
|
||||
["Scroll wheel", "Zoom in/out"]
|
||||
]
|
||||
}
|
||||
],
|
||||
"keywords": ["editor", "shortcuts", "keyboard", "workflow"],
|
||||
"relatedPages": ["doc-canvas-shortcuts", "doc-workflow-basics"]
|
||||
},
|
||||
"doc-faq": {
|
||||
"id": "doc-faq",
|
||||
"title": "Frequently Asked Questions",
|
||||
"category": "faq",
|
||||
"description": "Common questions and answers",
|
||||
"difficulty": "beginner",
|
||||
"estimatedReadTime": 5,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "FAQ",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "How do I organize my workflows?",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "Use workspaces for high-level organization (e.g., by project or team), projects within workspaces for grouping related workflows, and arrange cards spatially on the canvas to show relationships."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Can I export my workflows?",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "Yes! Workflows are automatically saved and can be exported as JSON for version control or sharing with team members."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "What's the maximum number of nodes in a workflow?",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "There's no hard limit, but performance may degrade with very large workflows (1000+). Consider breaking large workflows into smaller, reusable sub-workflows."
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Can I collaborate with team members?",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "Yes! Share workspaces and projects with team members, set permissions, and see real-time collaboration features."
|
||||
}
|
||||
],
|
||||
"keywords": ["faq", "questions", "answers"],
|
||||
"relatedPages": ["doc-troubleshooting"]
|
||||
},
|
||||
"doc-troubleshooting": {
|
||||
"id": "doc-troubleshooting",
|
||||
"title": "Troubleshooting",
|
||||
"category": "troubleshooting",
|
||||
"description": "Solutions to common issues",
|
||||
"difficulty": "intermediate",
|
||||
"estimatedReadTime": 5,
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Troubleshooting",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Workflow won't execute",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Check that all required inputs are provided",
|
||||
"Verify node connections are valid",
|
||||
"Check the browser console for error messages",
|
||||
"Try refreshing the page and running again"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Canvas is slow or laggy",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Disable card animations in canvas settings",
|
||||
"Try zooming out to reduce render load",
|
||||
"Close other browser tabs/applications",
|
||||
"Try using a different browser"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"content": "Cannot save workflow",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Check your internet connection",
|
||||
"Verify you have permission to edit the workflow",
|
||||
"Try refreshing the page",
|
||||
"Check for browser console errors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "callout",
|
||||
"content": "If issues persist, contact support or check the documentation for more help.",
|
||||
"variant": "warning",
|
||||
"icon": "⚠️"
|
||||
}
|
||||
],
|
||||
"keywords": ["troubleshooting", "help", "issues", "problems"],
|
||||
"relatedPages": ["doc-faq"]
|
||||
}
|
||||
},
|
||||
"searchIndex": []
|
||||
}
|
||||
156
workflowui/src/hooks/useDocumentation.ts
Normal file
156
workflowui/src/hooks/useDocumentation.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* useDocumentation Hook
|
||||
* Custom hook for documentation system integration
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
openHelp,
|
||||
closeHelp,
|
||||
navigateToPage,
|
||||
setCategory,
|
||||
setSearchQuery,
|
||||
setSearchResults,
|
||||
goBack,
|
||||
clearSearch,
|
||||
} from '../store/slices/documentationSlice';
|
||||
import { documentationService } from '../services/documentationService';
|
||||
import { DocCategory, DocPage } from '../types/documentation';
|
||||
|
||||
export function useDocumentation() {
|
||||
const dispatch = useDispatch();
|
||||
const helpState = useSelector((state: any) => state.documentation);
|
||||
|
||||
// Get current page
|
||||
const currentPage = useMemo(
|
||||
() => (helpState.currentPageId ? documentationService.getPage(helpState.currentPageId) : null),
|
||||
[helpState.currentPageId]
|
||||
);
|
||||
|
||||
// Get navigation tree
|
||||
const navigationTree = useMemo(() => documentationService.getNavigationTree(), []);
|
||||
|
||||
// Get current section pages
|
||||
const currentSectionPages = useMemo(() => {
|
||||
if (!helpState.currentCategory) return [];
|
||||
return documentationService.getPagesByCategory(helpState.currentCategory);
|
||||
}, [helpState.currentCategory]);
|
||||
|
||||
// Get search results
|
||||
const searchResults = useMemo(
|
||||
() =>
|
||||
helpState.searchResults.map((id: string) => documentationService.getPage(id)).filter(Boolean),
|
||||
[helpState.searchResults]
|
||||
);
|
||||
|
||||
// Get history pages
|
||||
const historyPages = useMemo(
|
||||
() =>
|
||||
helpState.history
|
||||
.map((id: string) => documentationService.getPage(id))
|
||||
.filter(Boolean),
|
||||
[helpState.history]
|
||||
);
|
||||
|
||||
// Open help
|
||||
const openHelpModal = useCallback(
|
||||
(pageId?: string, category?: DocCategory) => {
|
||||
dispatch(openHelp({ pageId, category }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// Close help
|
||||
const closeHelpModal = useCallback(() => {
|
||||
dispatch(closeHelp());
|
||||
}, [dispatch]);
|
||||
|
||||
// Navigate to page
|
||||
const goToPage = useCallback(
|
||||
(pageId: string) => {
|
||||
dispatch(navigateToPage(pageId));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// Set category
|
||||
const switchCategory = useCallback(
|
||||
(category: DocCategory | null) => {
|
||||
dispatch(setCategory(category));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// Search
|
||||
const search = useCallback(
|
||||
(query: string) => {
|
||||
dispatch(setSearchQuery(query));
|
||||
if (query.length >= 2) {
|
||||
const results = documentationService.search(query);
|
||||
dispatch(setSearchResults(results.map((r) => r.id)));
|
||||
} else {
|
||||
dispatch(setSearchResults([]));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// Go back in history
|
||||
const goBackInHistory = useCallback(() => {
|
||||
dispatch(goBack());
|
||||
}, [dispatch]);
|
||||
|
||||
// Clear search
|
||||
const clearSearchResults = useCallback(() => {
|
||||
dispatch(clearSearch());
|
||||
}, [dispatch]);
|
||||
|
||||
// Get related pages
|
||||
const relatedPages = useMemo(
|
||||
() => (currentPage ? documentationService.getRelatedPages(currentPage.id) : []),
|
||||
[currentPage]
|
||||
);
|
||||
|
||||
// Get breadcrumbs
|
||||
const breadcrumbs = useMemo(
|
||||
() => (currentPage ? documentationService.getBreadcrumbs(currentPage.id) : []),
|
||||
[currentPage]
|
||||
);
|
||||
|
||||
// Check if can go back
|
||||
const canGoBack = helpState.history.length > 1;
|
||||
|
||||
// Get documentation stats
|
||||
const stats = useMemo(() => documentationService.getStats(), []);
|
||||
|
||||
return {
|
||||
// State
|
||||
isOpen: helpState.isOpen,
|
||||
currentPage,
|
||||
currentCategory: helpState.currentCategory,
|
||||
searchQuery: helpState.searchQuery,
|
||||
searchResults,
|
||||
history: historyPages,
|
||||
canGoBack,
|
||||
|
||||
// Navigation
|
||||
navigationTree,
|
||||
currentSectionPages,
|
||||
relatedPages,
|
||||
breadcrumbs,
|
||||
|
||||
// Actions
|
||||
openHelpModal,
|
||||
closeHelpModal,
|
||||
goToPage,
|
||||
switchCategory,
|
||||
search,
|
||||
goBackInHistory,
|
||||
clearSearchResults,
|
||||
|
||||
// Utilities
|
||||
documentationService,
|
||||
stats,
|
||||
};
|
||||
}
|
||||
216
workflowui/src/services/documentationService.ts
Normal file
216
workflowui/src/services/documentationService.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Documentation Service
|
||||
* Loads and manages documentation content
|
||||
*/
|
||||
|
||||
import {
|
||||
DocumentationIndex,
|
||||
DocPage,
|
||||
DocSection,
|
||||
DocCategory,
|
||||
SearchIndexEntry,
|
||||
} from '../types/documentation';
|
||||
import docContent from '../data/documentation.json';
|
||||
|
||||
/**
|
||||
* Service for accessing documentation content
|
||||
*/
|
||||
const docIndex = docContent as unknown as DocumentationIndex;
|
||||
|
||||
export const documentationService = {
|
||||
/**
|
||||
* Get the complete documentation index
|
||||
*/
|
||||
getIndex(): DocumentationIndex {
|
||||
return docIndex;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all sections
|
||||
*/
|
||||
getSections(): DocSection[] {
|
||||
return docIndex.sections.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get section by category
|
||||
*/
|
||||
getSectionByCategory(category: DocCategory): DocSection | undefined {
|
||||
return docIndex.sections.find((s) => s.category === category);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific page by ID
|
||||
*/
|
||||
getPage(pageId: string): DocPage | undefined {
|
||||
return docIndex.pages[pageId];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all pages in a section
|
||||
*/
|
||||
getPagesBySection(sectionId: string): DocPage[] {
|
||||
const section = docIndex.sections.find((s) => s.id === sectionId);
|
||||
if (!section) return [];
|
||||
|
||||
return section.pages
|
||||
.map((pageId) => docIndex.pages[pageId])
|
||||
.filter((page): page is DocPage => !!page);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get pages by category
|
||||
*/
|
||||
getPagesByCategory(category: DocCategory): DocPage[] {
|
||||
return Object.values(docIndex.pages).filter(
|
||||
(page): page is DocPage => page.category === category
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search documentation by query
|
||||
*/
|
||||
search(query: string): DocPage[] {
|
||||
if (!query || query.length < 2) return [];
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
return Object.values(docIndex.pages).filter((page): page is DocPage => {
|
||||
// Search in title
|
||||
if (page.title.toLowerCase().includes(lowerQuery)) return true;
|
||||
|
||||
// Search in description
|
||||
if (page.description && page.description.toLowerCase().includes(lowerQuery))
|
||||
return true;
|
||||
|
||||
// Search in keywords
|
||||
if (
|
||||
page.keywords &&
|
||||
page.keywords.some((kw) => kw.toLowerCase().includes(lowerQuery))
|
||||
)
|
||||
return true;
|
||||
|
||||
// Search in content
|
||||
return page.content.some((block) => {
|
||||
if (typeof block.content === 'string') {
|
||||
return block.content.toLowerCase().includes(lowerQuery);
|
||||
}
|
||||
if (block.items && Array.isArray(block.items)) {
|
||||
return block.items.some((item) =>
|
||||
item.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get related pages for a given page
|
||||
*/
|
||||
getRelatedPages(pageId: string): DocPage[] {
|
||||
const page = docIndex.pages[pageId];
|
||||
if (!page || !page.relatedPages) return [];
|
||||
|
||||
return page.relatedPages
|
||||
.map((id) => docIndex.pages[id])
|
||||
.filter((p): p is DocPage => !!p);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get pages of a specific difficulty level
|
||||
*/
|
||||
getPagesByDifficulty(difficulty: 'beginner' | 'intermediate' | 'advanced'): DocPage[] {
|
||||
return Object.values(docIndex.pages).filter(
|
||||
(page): page is DocPage => page.difficulty === difficulty
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get pages by estimated read time range (in minutes)
|
||||
*/
|
||||
getPagesByReadTime(minTime: number, maxTime: number): DocPage[] {
|
||||
return Object.values(docIndex.pages).filter(
|
||||
(page): page is DocPage =>
|
||||
(page.estimatedReadTime || 0) >= minTime &&
|
||||
(page.estimatedReadTime || 0) <= maxTime
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get recently updated pages
|
||||
*/
|
||||
getRecentPages(limit = 5): DocPage[] {
|
||||
return Object.values(docIndex.pages)
|
||||
.filter((page): page is DocPage => !!page.lastUpdated)
|
||||
.sort((a, b) => (b.lastUpdated || 0) - (a.lastUpdated || 0))
|
||||
.slice(0, limit);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get breadcrumb navigation for a page
|
||||
*/
|
||||
getBreadcrumbs(pageId: string): Array<{ id: string; title: string }> {
|
||||
const page = docIndex.pages[pageId];
|
||||
if (!page) return [];
|
||||
|
||||
const section = docIndex.sections.find((s) => s.category === page.category);
|
||||
if (!section) {
|
||||
return [{ id: pageId, title: page.title }];
|
||||
}
|
||||
|
||||
return [
|
||||
{ id: section.id, title: section.title },
|
||||
{ id: pageId, title: page.title },
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get navigation tree for sidebar
|
||||
*/
|
||||
getNavigationTree(): Array<{
|
||||
section: DocSection;
|
||||
pages: DocPage[];
|
||||
}> {
|
||||
return docIndex.sections.map((section) => ({
|
||||
section,
|
||||
pages: section.pages
|
||||
.map((pageId) => docIndex.pages[pageId])
|
||||
.filter((page): page is DocPage => !!page),
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if documentation exists for a category
|
||||
*/
|
||||
hasCategory(category: DocCategory): boolean {
|
||||
return docIndex.sections.some((s) => s.category === category);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get statistics about documentation
|
||||
*/
|
||||
getStats() {
|
||||
const pages = Object.values(docIndex.pages).filter((p): p is DocPage => !!p);
|
||||
|
||||
return {
|
||||
totalPages: pages.length,
|
||||
totalSections: docIndex.sections.length,
|
||||
totalWords: pages.reduce(
|
||||
(sum, page) =>
|
||||
sum +
|
||||
page.content.reduce((pageSum, block) => {
|
||||
if (typeof block.content === 'string') {
|
||||
return pageSum + block.content.split(/\s+/).length;
|
||||
}
|
||||
return pageSum;
|
||||
}, 0),
|
||||
0
|
||||
),
|
||||
averageReadTime: Math.round(
|
||||
pages.reduce((sum, p) => sum + (p.estimatedReadTime || 0), 0) / pages.length
|
||||
),
|
||||
lastUpdated: docIndex.lastUpdated,
|
||||
};
|
||||
},
|
||||
};
|
||||
130
workflowui/src/store/slices/documentationSlice.ts
Normal file
130
workflowui/src/store/slices/documentationSlice.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Documentation Redux Slice
|
||||
* State management for documentation system
|
||||
*/
|
||||
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { HelpState, DocCategory } from '../../types/documentation';
|
||||
|
||||
const initialState: HelpState = {
|
||||
isOpen: false,
|
||||
currentPageId: null,
|
||||
currentCategory: null,
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
history: [],
|
||||
};
|
||||
|
||||
export const documentationSlice = createSlice({
|
||||
name: 'documentation',
|
||||
initialState,
|
||||
reducers: {
|
||||
/**
|
||||
* Open help modal
|
||||
*/
|
||||
openHelp: (state, action: PayloadAction<{ pageId?: string; category?: DocCategory }>) => {
|
||||
state.isOpen = true;
|
||||
if (action.payload.pageId) {
|
||||
state.currentPageId = action.payload.pageId;
|
||||
// Add to history
|
||||
if (!state.history.includes(action.payload.pageId)) {
|
||||
state.history.unshift(action.payload.pageId);
|
||||
state.history = state.history.slice(0, 20); // Keep last 20
|
||||
}
|
||||
}
|
||||
if (action.payload.category) {
|
||||
state.currentCategory = action.payload.category;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close help modal
|
||||
*/
|
||||
closeHelp: (state) => {
|
||||
state.isOpen = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigate to a page
|
||||
*/
|
||||
navigateToPage: (state, action: PayloadAction<string>) => {
|
||||
state.currentPageId = action.payload;
|
||||
state.searchQuery = '';
|
||||
state.searchResults = [];
|
||||
|
||||
// Add to history
|
||||
if (!state.history.includes(action.payload)) {
|
||||
state.history.unshift(action.payload);
|
||||
state.history = state.history.slice(0, 20);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set current category
|
||||
*/
|
||||
setCategory: (state, action: PayloadAction<DocCategory | null>) => {
|
||||
state.currentCategory = action.payload;
|
||||
state.currentPageId = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update search query and results
|
||||
*/
|
||||
setSearchQuery: (state, action: PayloadAction<string>) => {
|
||||
state.searchQuery = action.payload;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set search results
|
||||
*/
|
||||
setSearchResults: (state, action: PayloadAction<string[]>) => {
|
||||
state.searchResults = action.payload;
|
||||
},
|
||||
|
||||
/**
|
||||
* Go back in history
|
||||
*/
|
||||
goBack: (state) => {
|
||||
if (state.history.length > 1) {
|
||||
state.history.shift(); // Remove current
|
||||
state.currentPageId = state.history[0] || null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear search
|
||||
*/
|
||||
clearSearch: (state) => {
|
||||
state.searchQuery = '';
|
||||
state.searchResults = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear history
|
||||
*/
|
||||
clearHistory: (state) => {
|
||||
state.history = [];
|
||||
state.currentPageId = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset documentation state
|
||||
*/
|
||||
reset: () => initialState,
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
openHelp,
|
||||
closeHelp,
|
||||
navigateToPage,
|
||||
setCategory,
|
||||
setSearchQuery,
|
||||
setSearchResults,
|
||||
goBack,
|
||||
clearSearch,
|
||||
clearHistory,
|
||||
reset,
|
||||
} = documentationSlice.actions;
|
||||
|
||||
export default documentationSlice.reducer;
|
||||
135
workflowui/src/types/documentation.ts
Normal file
135
workflowui/src/types/documentation.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Documentation Type System
|
||||
* JSON-backed in-app help and documentation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Documentation content block types
|
||||
*/
|
||||
export type DocContentType =
|
||||
| 'text'
|
||||
| 'heading'
|
||||
| 'code'
|
||||
| 'list'
|
||||
| 'image'
|
||||
| 'video'
|
||||
| 'table'
|
||||
| 'callout'
|
||||
| 'step'
|
||||
| 'example';
|
||||
|
||||
/**
|
||||
* Documentation categories for organization
|
||||
*/
|
||||
export type DocCategory =
|
||||
| 'getting-started'
|
||||
| 'canvas'
|
||||
| 'workflows'
|
||||
| 'settings'
|
||||
| 'keyboard-shortcuts'
|
||||
| 'troubleshooting'
|
||||
| 'faq'
|
||||
| 'best-practices';
|
||||
|
||||
/**
|
||||
* Content block representing a section of documentation
|
||||
*/
|
||||
export interface DocContentBlock {
|
||||
type: DocContentType;
|
||||
content: string;
|
||||
title?: string;
|
||||
level?: number; // For headings
|
||||
language?: string; // For code blocks
|
||||
icon?: string; // For callouts/steps
|
||||
variant?: 'info' | 'warning' | 'error' | 'success'; // For callouts
|
||||
items?: string[]; // For lists
|
||||
columns?: string[]; // For tables
|
||||
rows?: (string | number)[][]; // For tables
|
||||
subtext?: string; // Additional text (callout footnotes, etc.)
|
||||
}
|
||||
|
||||
/**
|
||||
* Single documentation page/article
|
||||
*/
|
||||
export interface DocPage {
|
||||
id: string;
|
||||
title: string;
|
||||
category: DocCategory;
|
||||
description?: string;
|
||||
content: DocContentBlock[];
|
||||
relatedPages?: string[]; // IDs of related doc pages
|
||||
keywords?: string[]; // For search
|
||||
lastUpdated?: number; // Timestamp
|
||||
difficulty?: 'beginner' | 'intermediate' | 'advanced';
|
||||
estimatedReadTime?: number; // In minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Documentation section (collection of pages)
|
||||
*/
|
||||
export interface DocSection {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
category: DocCategory;
|
||||
pages: string[]; // IDs of doc pages in order
|
||||
icon?: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete documentation structure
|
||||
*/
|
||||
export interface DocumentationIndex {
|
||||
version: string;
|
||||
lastUpdated: number;
|
||||
sections: DocSection[];
|
||||
pages: Record<string, DocPage>;
|
||||
searchIndex?: SearchIndexEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search index entry for full-text search
|
||||
*/
|
||||
export interface SearchIndexEntry {
|
||||
pageId: string;
|
||||
title: string;
|
||||
category: DocCategory;
|
||||
content: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Context-specific help topics that can be triggered from UI
|
||||
*/
|
||||
export interface ContextualHelp {
|
||||
id: string;
|
||||
targetElement: string; // CSS selector or data-testid
|
||||
title: string;
|
||||
shortTip: string;
|
||||
fullDocPageId?: string; // Link to full documentation
|
||||
keyboard?: string; // Keyboard shortcut to trigger
|
||||
}
|
||||
|
||||
/**
|
||||
* Help modal state
|
||||
*/
|
||||
export interface HelpState {
|
||||
isOpen: boolean;
|
||||
currentPageId: string | null;
|
||||
currentCategory: DocCategory | null;
|
||||
searchQuery: string;
|
||||
searchResults: string[]; // Page IDs
|
||||
history: string[]; // Previously viewed pages
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcut documentation
|
||||
*/
|
||||
export interface KeyboardShortcutDoc {
|
||||
keys: string[];
|
||||
description: string;
|
||||
category: 'navigation' | 'editing' | 'canvas' | 'general';
|
||||
platforms?: ('windows' | 'mac' | 'linux')[];
|
||||
context?: string; // Where shortcut applies
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user