feat: KB Accelerator — convert KB articles into interactive flows

Full-stack implementation of the KB Accelerator feature that converts
static MSP knowledge base articles into interactive troubleshooting
and procedural flows using AI.

Backend:
- Migrations 054/055: kb_imports, kb_import_nodes tables + plan_limits KB columns
- SQLAlchemy models with relationships and self-referential node hierarchy
- Text extraction service (txt, paste, docx with structural metadata)
- AI conversion service with MSP-specialist prompts for both flow types
- 8 API endpoints: upload, get, list, convert, edit node, commit, delete, quota
- Tier-gated access via plan_limits (free: 3 lifetime, pro/team: unlimited)
- 8 integration tests covering upload, get/list, quota, commit, delete

Frontend:
- TypeScript types and API client for all KB Accelerator endpoints
- Multi-step wizard page: upload → processing → review → success
- Upload screen with paste/file tabs, drag-drop, target type selector
- Two-panel review screen with source highlighting and node cards
- Per-node actions: approve, edit, regenerate, insert, delete
- Confidence color indicators (green/amber/red)
- Sidebar navigation with Sparkles icon
- Code-split lazy-loaded route at /kb-accelerator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-10 20:56:28 -04:00
parent c65aa4f0b7
commit 71ff4a8c35
27 changed files with 4426 additions and 2 deletions

View File

@@ -0,0 +1,201 @@
import { useState } from 'react'
import { Check, X, Pencil, Trash2, RotateCcw, Plus, ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { KBImportNode, KBNodeEditRequest } from '@/types/kbAccelerator'
interface NodeCardProps {
node: KBImportNode
onEdit: (nodeId: string, data: KBNodeEditRequest) => Promise<void>
onHighlight: (excerpt: string | null) => void
}
function confidenceColor(score: number): string {
if (score >= 0.85) return 'border-emerald-400/40'
if (score >= 0.65) return 'border-amber-400/40'
return 'border-rose-500/40'
}
function confidenceLabel(score: number): string {
if (score >= 0.85) return 'High'
if (score >= 0.65) return 'Medium'
return 'Low'
}
function confidenceTextColor(score: number): string {
if (score >= 0.85) return 'text-emerald-400'
if (score >= 0.65) return 'text-amber-400'
return 'text-rose-500'
}
export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
const [expanded, setExpanded] = useState(false)
const [editMode, setEditMode] = useState(false)
const [editContent, setEditContent] = useState('')
const [busy, setBusy] = useState(false)
const question = (node.content?.question as string) ?? ''
const options = (node.content?.options as Array<{ label: string; next_node_id?: string }>) ?? []
const stepContent = (node.content?.content as string) ?? question
const handleAction = async (operation: KBNodeEditRequest['operation'], extra?: Partial<KBNodeEditRequest>) => {
setBusy(true)
try {
await onEdit(node.id, { operation, ...extra })
if (operation === 'edit') setEditMode(false)
} finally {
setBusy(false)
}
}
const startEdit = () => {
setEditContent(stepContent || question)
setEditMode(true)
}
return (
<div
className={cn(
'glass-card-static border-l-4 p-4 transition-all',
confidenceColor(node.confidence_score),
node.user_approved && 'opacity-75',
)}
onMouseEnter={() => onHighlight(node.source_excerpt)}
onMouseLeave={() => onHighlight(null)}
>
{/* Header */}
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
{node.node_type}
</span>
<span className={cn('font-label text-[0.625rem]', confidenceTextColor(node.confidence_score))}>
{confidenceLabel(node.confidence_score)} ({Math.round(node.confidence_score * 100)}%)
</span>
{node.user_approved && (
<span className="font-label text-[0.625rem] text-emerald-400">Approved</span>
)}
{node.user_edited && (
<span className="font-label text-[0.625rem] text-blue-400">Edited</span>
)}
</div>
{editMode ? (
<div className="space-y-2">
<textarea
value={editContent}
onChange={e => setEditContent(e.target.value)}
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-primary/30 focus:outline-hidden"
rows={3}
/>
<div className="flex gap-2">
<button
onClick={() => handleAction('edit', { content: { ...node.content, question: editContent, content: editContent } })}
disabled={busy}
className="px-3 py-1.5 text-xs font-medium rounded-md bg-gradient-brand text-[#101114] hover:opacity-90"
>
Save
</button>
<button
onClick={() => setEditMode(false)}
className="px-3 py-1.5 text-xs font-medium rounded-md bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground"
>
Cancel
</button>
</div>
</div>
) : (
<p className="text-sm text-foreground">{stepContent || question}</p>
)}
</div>
{/* Actions */}
{!editMode && (
<div className="flex items-center gap-1 shrink-0">
{!node.user_approved && (
<button
onClick={() => handleAction('approve')}
disabled={busy}
className="p-1.5 rounded-md text-muted-foreground hover:text-emerald-400 hover:bg-emerald-400/10 transition-colors"
title="Approve"
>
<Check size={14} />
</button>
)}
{node.user_approved && (
<button
onClick={() => handleAction('reject')}
disabled={busy}
className="p-1.5 rounded-md text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors"
title="Unapprove"
>
<X size={14} />
</button>
)}
<button
onClick={startEdit}
disabled={busy}
className="p-1.5 rounded-md text-muted-foreground hover:text-blue-400 hover:bg-blue-400/10 transition-colors"
title="Edit"
>
<Pencil size={14} />
</button>
<button
onClick={() => handleAction('regenerate')}
disabled={busy}
className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
title="Regenerate"
>
<RotateCcw size={14} />
</button>
<button
onClick={() => handleAction('insert_after', { content: { question: 'New node', type: node.node_type } })}
disabled={busy}
className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
title="Insert after"
>
<Plus size={14} />
</button>
<button
onClick={() => handleAction('delete')}
disabled={busy}
className="p-1.5 rounded-md text-muted-foreground hover:text-rose-500 hover:bg-rose-500/10 transition-colors"
title="Delete"
>
<Trash2 size={14} />
</button>
</div>
)}
</div>
{/* Options (troubleshooting) */}
{options.length > 0 && (
<div className="mt-3">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
{options.length} option{options.length !== 1 ? 's' : ''}
</button>
{expanded && (
<div className="mt-2 space-y-1 pl-3 border-l border-border">
{options.map((opt, i) => (
<p key={i} className="text-xs text-muted-foreground">
{opt.label} {opt.next_node_id && <span className="text-[#5a6170]"> {opt.next_node_id}</span>}
</p>
))}
</div>
)}
</div>
)}
{/* Source excerpt */}
{node.source_excerpt && (
<p className="mt-2 text-xs text-[#5a6170] italic truncate" title={node.source_excerpt}>
Source: {node.source_excerpt}
</p>
)}
</div>
)
}

View File

@@ -0,0 +1,130 @@
import { useState } from 'react'
import { CheckCircle2, AlertTriangle, BarChart3 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { NodeCard } from './NodeCard'
import { SourcePanel } from './SourcePanel'
import type { KBImport, KBNodeEditRequest, KBCommitRequest } from '@/types/kbAccelerator'
interface ReviewScreenProps {
kbImport: KBImport
onEditNode: (nodeId: string, data: KBNodeEditRequest) => Promise<void>
onCommit: (data?: KBCommitRequest) => Promise<void>
onDiscard: () => Promise<void>
loading: boolean
}
export function ReviewScreen({ kbImport, onEditNode, onCommit, onDiscard, loading }: ReviewScreenProps) {
const [highlightExcerpt, setHighlightExcerpt] = useState<string | null>(null)
const nodes = kbImport.nodes
const approvedCount = nodes.filter(n => n.user_approved).length
const lowConfidenceCount = nodes.filter(n => n.confidence_score < 0.65).length
const avgConfidence = kbImport.confidence_avg ?? 0
const title = (kbImport.source_metadata as Record<string, unknown> | null)
?._conversion as Record<string, unknown> | undefined
const flowTitle = (title?.title as string) ?? 'Untitled Flow'
const flowDescription = (title?.description as string) ?? ''
return (
<div className="flex flex-col h-full gap-4">
{/* Header */}
<div className="glass-card-static p-4 flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-lg font-heading font-semibold text-foreground">{flowTitle}</h2>
{flowDescription && (
<p className="text-sm text-muted-foreground mt-0.5">{flowDescription}</p>
)}
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm">
<BarChart3 size={14} className="text-muted-foreground" />
<span className="text-muted-foreground">
Avg confidence: <span className="text-foreground font-medium">{Math.round(avgConfidence * 100)}%</span>
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle2 size={14} className="text-emerald-400" />
<span className="text-muted-foreground">
{approvedCount}/{nodes.length} approved
</span>
</div>
{lowConfidenceCount > 0 && (
<div className="flex items-center gap-2 text-sm">
<AlertTriangle size={14} className="text-amber-400" />
<span className="text-amber-400">
{lowConfidenceCount} low confidence
</span>
</div>
)}
</div>
</div>
{/* Two-panel layout */}
<div className="flex-1 grid grid-cols-1 lg:grid-cols-2 gap-4 min-h-0">
{/* Source panel */}
<div className="overflow-hidden">
<SourcePanel
sourceText={kbImport.source_text}
sourceFormat={kbImport.source_format}
highlightExcerpt={highlightExcerpt}
/>
</div>
{/* Nodes panel */}
<div className="flex flex-col glass-card-static overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: 'var(--glass-border)' }}>
<span className="text-sm font-medium text-foreground">Generated Flow</span>
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground ml-auto">
{kbImport.target_type === 'troubleshooting' ? 'Troubleshooting' : 'Project'}
</span>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{nodes.map(node => (
<NodeCard
key={node.id}
node={node}
onEdit={onEditNode}
onHighlight={setHighlightExcerpt}
/>
))}
{nodes.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8">
No nodes generated. Try converting again.
</p>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-between gap-3">
<button
onClick={onDiscard}
disabled={loading}
className={cn(
'px-4 py-2.5 rounded-[10px] text-sm font-medium transition-colors',
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-muted-foreground',
'hover:text-foreground hover:border-[rgba(255,255,255,0.12)]',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
Discard
</button>
<button
onClick={() => onCommit()}
disabled={loading || nodes.length === 0}
className={cn(
'flex items-center gap-2 px-6 py-2.5 rounded-[10px] text-sm font-semibold transition-all',
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
'hover:opacity-90 active:scale-[0.97]',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<CheckCircle2 size={16} />
{loading ? 'Committing...' : 'Commit to Library'}
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,42 @@
import { useMemo } from 'react'
import { FileText } from 'lucide-react'
interface SourcePanelProps {
sourceText: string
sourceFormat: string
highlightExcerpt: string | null
}
export function SourcePanel({ sourceText, sourceFormat, highlightExcerpt }: SourcePanelProps) {
const renderedText = useMemo(() => {
if (!highlightExcerpt || !sourceText.includes(highlightExcerpt)) {
return <span>{sourceText}</span>
}
const idx = sourceText.indexOf(highlightExcerpt)
return (
<>
<span>{sourceText.slice(0, idx)}</span>
<mark className="bg-primary/20 text-foreground rounded px-0.5">{highlightExcerpt}</mark>
<span>{sourceText.slice(idx + highlightExcerpt.length)}</span>
</>
)
}, [sourceText, highlightExcerpt])
return (
<div className="glass-card-static flex flex-col h-full">
<div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: 'var(--glass-border)' }}>
<FileText size={16} className="text-muted-foreground" />
<span className="text-sm font-medium text-foreground">Source Document</span>
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground ml-auto">
{sourceFormat}
</span>
</div>
<div className="flex-1 overflow-y-auto p-4">
<pre className="text-sm text-muted-foreground whitespace-pre-wrap font-sans leading-relaxed">
{renderedText}
</pre>
</div>
</div>
)
}

View File

@@ -0,0 +1,56 @@
import { CheckCircle2, ArrowRight, Plus } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { KBCommitResponse } from '@/types/kbAccelerator'
interface SuccessScreenProps {
result: KBCommitResponse
onViewFlow: () => void
onConvertAnother: () => void
}
export function SuccessScreen({ result, onViewFlow, onConvertAnother }: SuccessScreenProps) {
return (
<div className="max-w-lg mx-auto text-center space-y-6 py-12">
<div className="flex justify-center">
<div className="w-16 h-16 rounded-full bg-emerald-400/10 flex items-center justify-center">
<CheckCircle2 size={32} className="text-emerald-400" />
</div>
</div>
<div>
<h2 className="text-xl font-heading font-semibold text-foreground">
Flow Created Successfully
</h2>
<p className="text-sm text-muted-foreground mt-2">
Your KB article has been converted into a {result.tree_type === 'troubleshooting' ? 'troubleshooting' : 'project'} flow
and added to your library.
</p>
</div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<button
onClick={onViewFlow}
className={cn(
'flex items-center gap-2 px-6 py-2.5 rounded-[10px] text-sm font-semibold transition-all',
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
'hover:opacity-90 active:scale-[0.97]'
)}
>
View Flow
<ArrowRight size={16} />
</button>
<button
onClick={onConvertAnother}
className={cn(
'flex items-center gap-2 px-6 py-2.5 rounded-[10px] text-sm font-medium transition-colors',
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
'hover:border-[rgba(255,255,255,0.12)]'
)}
>
<Plus size={16} />
Convert Another
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,269 @@
import { useState, useCallback, useRef } from 'react'
import { Upload, FileText, ClipboardPaste, FileUp, Sparkles, AlertCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Textarea } from '@/components/ui/Textarea'
import { Input } from '@/components/ui/Input'
import type { KBQuotaResponse } from '@/types/kbAccelerator'
type TargetType = 'troubleshooting' | 'procedural' | 'auto'
interface UploadScreenProps {
quota: KBQuotaResponse | null
onSubmitText: (content: string, title: string, targetType: TargetType) => void
onSubmitFile: (file: File, targetType: TargetType) => void
loading: boolean
}
const TARGET_TYPES = [
{
value: 'troubleshooting' as const,
label: 'Troubleshooting Flow',
description: 'Decision tree with diagnostic questions and resolutions',
},
{
value: 'procedural' as const,
label: 'Project Flow',
description: 'Step-by-step procedure with warnings and variables',
},
{
value: 'auto' as const,
label: 'Let AI Decide',
description: 'AI will determine the best flow type from your content',
},
]
const FORMAT_LABELS: Record<string, string> = {
txt: 'TXT',
paste: 'Paste',
docx: 'DOCX',
pdf: 'PDF',
html: 'HTML',
md: 'Markdown',
}
export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: UploadScreenProps) {
const [mode, setMode] = useState<'paste' | 'file'>('paste')
const [content, setContent] = useState('')
const [title, setTitle] = useState('')
const [targetType, setTargetType] = useState<TargetType>('auto')
const [file, setFile] = useState<File | null>(null)
const [dragOver, setDragOver] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const canSubmit = mode === 'paste'
? content.trim().length >= 10
: file !== null
const handleSubmit = () => {
if (loading) return
if (mode === 'paste') {
onSubmitText(content, title, targetType)
} else if (file) {
onSubmitFile(file, targetType)
}
}
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
const droppedFile = e.dataTransfer.files[0]
if (droppedFile) {
setFile(droppedFile)
setMode('file')
}
}, [])
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files?.[0]
if (selected) {
setFile(selected)
}
}
const allowedFormats = quota?.allowed_formats ?? ['txt', 'paste']
const fileFormats = allowedFormats.filter(f => f !== 'paste')
return (
<div className="max-w-3xl mx-auto space-y-6">
{/* Quota info */}
{quota && (
<div className="glass-card-static p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Sparkles size={18} className="text-primary" />
<div>
<p className="text-sm font-medium text-foreground">
{quota.lifetime_conversions_limit
? `${quota.lifetime_conversions_limit - quota.lifetime_conversions_used} conversions remaining`
: 'Unlimited conversions'}
</p>
<p className="text-xs text-muted-foreground">
{quota.plan.charAt(0).toUpperCase() + quota.plan.slice(1)} plan
</p>
</div>
</div>
{!quota.can_convert && (
<div className="flex items-center gap-2 text-amber-400 text-sm">
<AlertCircle size={16} />
<span>Conversion limit reached</span>
</div>
)}
</div>
)}
{/* Mode toggle */}
<div className="flex gap-2">
<button
onClick={() => setMode('paste')}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-[10px] text-sm font-medium transition-colors',
mode === 'paste'
? 'bg-primary/10 text-foreground border border-primary/30'
: 'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)]'
)}
>
<ClipboardPaste size={16} />
Paste Text
</button>
{fileFormats.length > 0 && (
<button
onClick={() => setMode('file')}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-[10px] text-sm font-medium transition-colors',
mode === 'file'
? 'bg-primary/10 text-foreground border border-primary/30'
: 'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)]'
)}
>
<FileUp size={16} />
Upload File
</button>
)}
</div>
{/* Content area */}
<div className="glass-card-static p-5 space-y-4">
{mode === 'paste' ? (
<>
<div>
<label htmlFor="kb-title" className="block font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-1.5">
Title (optional)
</label>
<Input
id="kb-title"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="e.g., Outlook Connectivity Troubleshooting"
maxLength={255}
/>
</div>
<div>
<label htmlFor="kb-content" className="block font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-1.5">
KB Article Content
</label>
<Textarea
id="kb-content"
value={content}
onChange={e => setContent(e.target.value)}
placeholder="Paste your KB article text here..."
rows={12}
maxLength={500000}
/>
<p className="mt-1 text-xs text-muted-foreground">
{content.length.toLocaleString()} / 500,000 characters
</p>
</div>
</>
) : (
<div
onDragOver={e => { e.preventDefault(); setDragOver(true) }}
onDragLeave={() => setDragOver(false)}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={cn(
'flex flex-col items-center justify-center gap-3 p-10 rounded-xl border-2 border-dashed cursor-pointer transition-colors',
dragOver
? 'border-primary/50 bg-primary/5'
: 'border-[rgba(255,255,255,0.08)] hover:border-[rgba(255,255,255,0.15)]'
)}
>
{file ? (
<>
<FileText size={32} className="text-primary" />
<div className="text-center">
<p className="text-sm font-medium text-foreground">{file.name}</p>
<p className="text-xs text-muted-foreground mt-1">
{(file.size / 1024).toFixed(1)} KB
</p>
<button
onClick={e => { e.stopPropagation(); setFile(null) }}
className="mt-2 text-xs text-primary hover:underline"
>
Remove
</button>
</div>
</>
) : (
<>
<Upload size={32} className="text-muted-foreground" />
<div className="text-center">
<p className="text-sm text-foreground">
Drop a file here or <span className="text-primary">browse</span>
</p>
<p className="text-xs text-muted-foreground mt-1">
Supported: {fileFormats.map(f => FORMAT_LABELS[f] || f.toUpperCase()).join(', ')}
</p>
</div>
</>
)}
<input
ref={fileInputRef}
type="file"
className="hidden"
accept={fileFormats.map(f => `.${f}`).join(',')}
onChange={handleFileSelect}
/>
</div>
)}
</div>
{/* Target type selector */}
<div>
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-2">
Target Flow Type
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{TARGET_TYPES.map(t => (
<button
key={t.value}
onClick={() => setTargetType(t.value)}
className={cn(
'glass-card-static p-4 text-left transition-all',
targetType === t.value
? 'border-primary/30 bg-primary/5'
: 'hover:border-[rgba(255,255,255,0.12)]'
)}
>
<p className="text-sm font-medium text-foreground">{t.label}</p>
<p className="text-xs text-muted-foreground mt-1">{t.description}</p>
</button>
))}
</div>
</div>
{/* Submit */}
<button
onClick={handleSubmit}
disabled={!canSubmit || loading || (quota != null && !quota.can_convert)}
className={cn(
'w-full flex items-center justify-center gap-2 px-6 py-3 rounded-[10px] text-sm font-semibold transition-all',
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
'hover:opacity-90 active:scale-[0.97]',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<Sparkles size={16} />
{loading ? 'Converting...' : 'Convert to Flow'}
</button>
</div>
)
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, BotMessageSquare, BookOpen } from 'lucide-react'
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, BotMessageSquare, BookOpen, Sparkles } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
@@ -83,6 +83,7 @@ export function Sidebar() {
<NavItem href="/shares" icon={FileText} label="Exports" collapsed />
<NavItem href="/assistant" icon={BotMessageSquare} label="AI Assistant" collapsed />
<NavItem href="/step-library" icon={Bookmark} label="Step Library" collapsed />
<NavItem href="/kb-accelerator" icon={Sparkles} label="KB Accelerator" collapsed />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" collapsed />
<NavItem href="/guides" icon={BookOpen} label="User Guides" collapsed />
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" collapsed />
@@ -115,6 +116,7 @@ export function Sidebar() {
<NavItem href="/shares" icon={FileText} label="Exports" />
<NavItem href="/assistant" icon={BotMessageSquare} label="AI Assistant" />
<NavItem href="/step-library" icon={Bookmark} label="Step Library" />
<NavItem href="/kb-accelerator" icon={Sparkles} label="KB Accelerator" />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" />
</div>
</>