@dilipod/ui 0.4.21 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/flowchart-diagram.d.ts +8 -0
- package/dist/components/flowchart-diagram.d.ts.map +1 -0
- package/dist/components/impact-metrics-form.d.ts.map +1 -1
- package/dist/components/scenarios-manager.d.ts.map +1 -1
- package/dist/components/worker-spec.d.ts +35 -0
- package/dist/components/worker-spec.d.ts.map +1 -0
- package/dist/components/workflow-viewer.d.ts +1 -1
- package/dist/components/workflow-viewer.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +444 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +444 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/flowchart-diagram.tsx +319 -0
- package/src/components/impact-metrics-form.tsx +17 -6
- package/src/components/scenarios-manager.tsx +3 -0
- package/src/components/worker-spec.tsx +389 -0
- package/src/components/workflow-viewer.tsx +1 -1
- package/src/index.ts +8 -0
package/package.json
CHANGED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================
|
|
8
|
+
|
|
9
|
+
interface FlowNode {
|
|
10
|
+
id: string
|
|
11
|
+
label: string
|
|
12
|
+
type: 'action' | 'decision' | 'terminal'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FlowEdge {
|
|
16
|
+
from: string
|
|
17
|
+
to: string
|
|
18
|
+
label?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// Parser
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
function parseMermaidFlowchart(mermaid: string): { nodes: FlowNode[]; edges: FlowEdge[] } {
|
|
26
|
+
const nodes = new Map<string, FlowNode>()
|
|
27
|
+
const edges: FlowEdge[] = []
|
|
28
|
+
|
|
29
|
+
const lines = mermaid
|
|
30
|
+
.split(/\\n|\n/)
|
|
31
|
+
.map(l => l.trim())
|
|
32
|
+
.filter(l => l && !l.startsWith('flowchart') && !l.startsWith('graph'))
|
|
33
|
+
|
|
34
|
+
function parseNodeDef(str: string): { id: string; label?: string; type?: FlowNode['type'] } {
|
|
35
|
+
// Decision: D{Certificate Found?}
|
|
36
|
+
const decisionMatch = str.match(/^([A-Za-z0-9_]+)\{(.+?)\}$/)
|
|
37
|
+
if (decisionMatch) return { id: decisionMatch[1], label: decisionMatch[2], type: 'decision' }
|
|
38
|
+
// Action / terminal: A[Start] or A(Start) or A([Start])
|
|
39
|
+
const bracketMatch = str.match(/^([A-Za-z0-9_]+)\[?\(?\[?(.+?)\]?\)?\]?$/)
|
|
40
|
+
if (bracketMatch) {
|
|
41
|
+
const label = bracketMatch[2]
|
|
42
|
+
const isTerminal = /^(start|end|begin|finish|done)$/i.test(label)
|
|
43
|
+
return { id: bracketMatch[1], label, type: isTerminal ? 'terminal' : 'action' }
|
|
44
|
+
}
|
|
45
|
+
// Just an ID reference
|
|
46
|
+
return { id: str.trim() }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const edgeMatch = line.match(/^(.+?)\s*-->(?:\|(.+?)\|)?\s*(.+)$/)
|
|
51
|
+
if (!edgeMatch) continue
|
|
52
|
+
|
|
53
|
+
const leftRaw = edgeMatch[1].trim()
|
|
54
|
+
const edgeLabel = edgeMatch[2]?.trim()
|
|
55
|
+
const rightRaw = edgeMatch[3].trim()
|
|
56
|
+
|
|
57
|
+
const left = parseNodeDef(leftRaw)
|
|
58
|
+
const right = parseNodeDef(rightRaw)
|
|
59
|
+
|
|
60
|
+
if (left.label && !nodes.has(left.id)) {
|
|
61
|
+
nodes.set(left.id, { id: left.id, label: left.label, type: left.type || 'action' })
|
|
62
|
+
}
|
|
63
|
+
if (right.label && !nodes.has(right.id)) {
|
|
64
|
+
nodes.set(right.id, { id: right.id, label: right.label, type: right.type || 'action' })
|
|
65
|
+
}
|
|
66
|
+
if (!nodes.has(left.id)) {
|
|
67
|
+
nodes.set(left.id, { id: left.id, label: left.id, type: 'action' })
|
|
68
|
+
}
|
|
69
|
+
if (!nodes.has(right.id)) {
|
|
70
|
+
nodes.set(right.id, { id: right.id, label: right.id, type: 'action' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
edges.push({ from: left.id, to: right.id, label: edgeLabel })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { nodes: Array.from(nodes.values()), edges }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================
|
|
80
|
+
// Layout Builder
|
|
81
|
+
// ============================================
|
|
82
|
+
|
|
83
|
+
function findMergePoint(
|
|
84
|
+
branchStarts: string[],
|
|
85
|
+
outgoing: Map<string, FlowEdge[]>,
|
|
86
|
+
): string | null {
|
|
87
|
+
const reachable = new Map<string, Set<string>>()
|
|
88
|
+
|
|
89
|
+
for (const start of branchStarts) {
|
|
90
|
+
const q = [start]
|
|
91
|
+
const seen = new Set<string>()
|
|
92
|
+
while (q.length > 0) {
|
|
93
|
+
const id = q.shift()!
|
|
94
|
+
if (seen.has(id)) continue
|
|
95
|
+
seen.add(id)
|
|
96
|
+
if (!reachable.has(id)) reachable.set(id, new Set())
|
|
97
|
+
reachable.get(id)!.add(start)
|
|
98
|
+
const outs = outgoing.get(id) || []
|
|
99
|
+
for (const e of outs) q.push(e.to)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const allBranches = new Set(branchStarts)
|
|
104
|
+
const q = [branchStarts[0]]
|
|
105
|
+
const seen = new Set<string>()
|
|
106
|
+
while (q.length > 0) {
|
|
107
|
+
const id = q.shift()!
|
|
108
|
+
if (seen.has(id)) continue
|
|
109
|
+
seen.add(id)
|
|
110
|
+
if (!allBranches.has(id) && reachable.get(id)?.size === branchStarts.length) {
|
|
111
|
+
return id
|
|
112
|
+
}
|
|
113
|
+
const outs = outgoing.get(id) || []
|
|
114
|
+
for (const e of outs) q.push(e.to)
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type LayoutItem =
|
|
120
|
+
| { type: 'node'; nodeId: string }
|
|
121
|
+
| { type: 'arrow'; label?: string }
|
|
122
|
+
| { type: 'branch'; decision: string; branches: { label?: string; items: LayoutItem[] }[]; mergeId: string | null }
|
|
123
|
+
|
|
124
|
+
function buildLayout(
|
|
125
|
+
startId: string,
|
|
126
|
+
outgoing: Map<string, FlowEdge[]>,
|
|
127
|
+
incoming: Map<string, FlowEdge[]>,
|
|
128
|
+
nodeMap: Map<string, FlowNode>,
|
|
129
|
+
visited: Set<string>
|
|
130
|
+
): LayoutItem[] {
|
|
131
|
+
const items: LayoutItem[] = []
|
|
132
|
+
let currentId: string | null = startId
|
|
133
|
+
|
|
134
|
+
while (currentId) {
|
|
135
|
+
if (visited.has(currentId)) break
|
|
136
|
+
|
|
137
|
+
const node = nodeMap.get(currentId)
|
|
138
|
+
if (!node) break
|
|
139
|
+
|
|
140
|
+
const outs: FlowEdge[] = outgoing.get(currentId) || []
|
|
141
|
+
|
|
142
|
+
if (outs.length <= 1) {
|
|
143
|
+
visited.add(currentId)
|
|
144
|
+
items.push({ type: 'node', nodeId: currentId })
|
|
145
|
+
if (outs.length === 1) {
|
|
146
|
+
const nextId: string = outs[0].to
|
|
147
|
+
if (visited.has(nextId)) break
|
|
148
|
+
items.push({ type: 'arrow', label: outs[0].label })
|
|
149
|
+
currentId = nextId
|
|
150
|
+
} else {
|
|
151
|
+
currentId = null
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
visited.add(currentId)
|
|
155
|
+
items.push({ type: 'node', nodeId: currentId })
|
|
156
|
+
|
|
157
|
+
const branchStarts: string[] = outs.map((e: FlowEdge) => e.to)
|
|
158
|
+
const mergeId = findMergePoint(branchStarts, outgoing)
|
|
159
|
+
|
|
160
|
+
const branches: { label?: string; items: LayoutItem[] }[] = []
|
|
161
|
+
for (const edge of outs) {
|
|
162
|
+
if (visited.has(edge.to) && edge.to !== mergeId) {
|
|
163
|
+
branches.push({ label: edge.label, items: [] })
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
const branchItems = buildLayout(edge.to, outgoing, incoming, nodeMap, visited)
|
|
167
|
+
branches.push({ label: edge.label, items: branchItems })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
items.push({ type: 'branch', decision: currentId, branches, mergeId })
|
|
171
|
+
|
|
172
|
+
if (mergeId && !visited.has(mergeId)) {
|
|
173
|
+
items.push({ type: 'arrow' })
|
|
174
|
+
currentId = mergeId
|
|
175
|
+
} else {
|
|
176
|
+
currentId = null
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return items
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================
|
|
185
|
+
// Render Components
|
|
186
|
+
// ============================================
|
|
187
|
+
|
|
188
|
+
function FlowArrow({ label }: { label?: string }) {
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex flex-col items-center">
|
|
191
|
+
{label && (
|
|
192
|
+
<span className="text-[10px] font-medium text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded mb-0.5">
|
|
193
|
+
{label}
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
196
|
+
<div className="w-px h-4 bg-gray-300" />
|
|
197
|
+
<div className="w-0 h-0 border-l-[4px] border-r-[4px] border-t-[5px] border-l-transparent border-r-transparent border-t-gray-300" />
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function FlowNodeBox({ node }: { node: FlowNode }) {
|
|
203
|
+
if (node.type === 'decision') {
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
className="bg-amber-50 border-2 border-amber-300 rounded-lg px-4 py-2.5 text-xs font-medium text-amber-800 text-center my-1"
|
|
207
|
+
style={{ minWidth: '120px' }}
|
|
208
|
+
>
|
|
209
|
+
<span className="text-amber-400 mr-1">◇</span>
|
|
210
|
+
{node.label}
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
if (node.type === 'terminal') {
|
|
215
|
+
return (
|
|
216
|
+
<div className="bg-gray-100 border border-gray-200 rounded-full px-5 py-1.5 text-xs font-medium text-gray-500 text-center my-1">
|
|
217
|
+
{node.label}
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
return (
|
|
222
|
+
<div className="bg-white border border-gray-200 rounded-sm px-4 py-2 text-xs font-medium text-[var(--black)] text-center shadow-sm my-1 max-w-[220px]">
|
|
223
|
+
{node.label}
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function RenderLayoutItems({ items, nodeMap }: { items: LayoutItem[]; nodeMap: Map<string, FlowNode> }) {
|
|
229
|
+
return (
|
|
230
|
+
<>
|
|
231
|
+
{items.map((item, i) => {
|
|
232
|
+
if (item.type === 'node') {
|
|
233
|
+
const node = nodeMap.get(item.nodeId)
|
|
234
|
+
if (!node) return null
|
|
235
|
+
return <FlowNodeBox key={`node-${item.nodeId}`} node={node} />
|
|
236
|
+
}
|
|
237
|
+
if (item.type === 'arrow') {
|
|
238
|
+
return <FlowArrow key={`arrow-${i}`} label={item.label} />
|
|
239
|
+
}
|
|
240
|
+
if (item.type === 'branch') {
|
|
241
|
+
return (
|
|
242
|
+
<div key={`branch-${item.decision}-${i}`} className="flex flex-col items-center w-full">
|
|
243
|
+
<div className="w-px h-3 bg-gray-300" />
|
|
244
|
+
<div className="flex items-start justify-center gap-6 w-full">
|
|
245
|
+
{item.branches.map((branch, j) => (
|
|
246
|
+
<div key={j} className="flex flex-col items-center min-w-[100px]">
|
|
247
|
+
<div className="flex flex-col items-center">
|
|
248
|
+
{branch.label && (
|
|
249
|
+
<span className="text-[10px] font-medium text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded mb-1">
|
|
250
|
+
{branch.label}
|
|
251
|
+
</span>
|
|
252
|
+
)}
|
|
253
|
+
<div className="w-0 h-0 border-l-[4px] border-r-[4px] border-t-[5px] border-l-transparent border-r-transparent border-t-gray-300" />
|
|
254
|
+
</div>
|
|
255
|
+
<div className="flex flex-col items-center">
|
|
256
|
+
<RenderLayoutItems items={branch.items} nodeMap={nodeMap} />
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
))}
|
|
260
|
+
</div>
|
|
261
|
+
{item.mergeId && (
|
|
262
|
+
<div className="w-px h-3 bg-gray-300" />
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
return null
|
|
268
|
+
})}
|
|
269
|
+
</>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================
|
|
274
|
+
// Public Component
|
|
275
|
+
// ============================================
|
|
276
|
+
|
|
277
|
+
export interface FlowchartDiagramProps {
|
|
278
|
+
/** Mermaid flowchart syntax string */
|
|
279
|
+
mermaid: string
|
|
280
|
+
/** Optional className for the container */
|
|
281
|
+
className?: string
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function FlowchartDiagram({ mermaid, className }: FlowchartDiagramProps) {
|
|
285
|
+
const { nodes, edges } = parseMermaidFlowchart(mermaid)
|
|
286
|
+
|
|
287
|
+
if (nodes.length === 0) {
|
|
288
|
+
return (
|
|
289
|
+
<pre className="text-xs bg-white border border-gray-100 rounded-sm p-3 overflow-x-auto whitespace-pre-wrap">
|
|
290
|
+
{mermaid}
|
|
291
|
+
</pre>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const outgoing = new Map<string, FlowEdge[]>()
|
|
296
|
+
const incoming = new Map<string, FlowEdge[]>()
|
|
297
|
+
for (const edge of edges) {
|
|
298
|
+
if (!outgoing.has(edge.from)) outgoing.set(edge.from, [])
|
|
299
|
+
outgoing.get(edge.from)!.push(edge)
|
|
300
|
+
if (!incoming.has(edge.to)) incoming.set(edge.to, [])
|
|
301
|
+
incoming.get(edge.to)!.push(edge)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]))
|
|
305
|
+
|
|
306
|
+
const roots = nodes.filter(n => !incoming.has(n.id) || incoming.get(n.id)!.length === 0)
|
|
307
|
+
const startId = roots.length > 0 ? roots[0].id : nodes[0].id
|
|
308
|
+
|
|
309
|
+
const visited = new Set<string>()
|
|
310
|
+
const layout = buildLayout(startId, outgoing, incoming, nodeMap, visited)
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className={className}>
|
|
314
|
+
<div className="flex flex-col items-center py-2">
|
|
315
|
+
<RenderLayoutItems items={layout} nodeMap={nodeMap} />
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
@@ -150,6 +150,9 @@ export function ImpactMetricsForm({
|
|
|
150
150
|
// Net annual savings
|
|
151
151
|
const netAnnualSavings = laborSavingsPerYear - workerCostPerYear
|
|
152
152
|
|
|
153
|
+
// ROI percentage: net savings relative to worker cost
|
|
154
|
+
const roiPercentage = workerCostPerYear > 0 ? (netAnnualSavings / workerCostPerYear) * 100 : 0
|
|
155
|
+
|
|
153
156
|
return (
|
|
154
157
|
<Card className={cn("border-[var(--cyan)]/20 bg-gradient-to-br from-white to-[var(--cyan)]/5", className)}>
|
|
155
158
|
<CardContent className="p-5">
|
|
@@ -274,17 +277,25 @@ export function ImpactMetricsForm({
|
|
|
274
277
|
€{netAnnualSavings.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
|
275
278
|
</p>
|
|
276
279
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
277
|
-
€{laborSavingsPerYear.toLocaleString(undefined, { maximumFractionDigits: 0 })} − €{workerCostPerYear}
|
|
280
|
+
€{laborSavingsPerYear.toLocaleString(undefined, { maximumFractionDigits: 0 })} <span className="opacity-60">labor saved</span> − €{workerCostPerYear} <span className="opacity-60">worker cost</span>
|
|
278
281
|
</p>
|
|
279
282
|
</div>
|
|
280
283
|
</div>
|
|
281
284
|
|
|
282
|
-
{/* Implied frequency
|
|
283
|
-
|
|
284
|
-
<p className=
|
|
285
|
-
|
|
285
|
+
{/* ROI & Implied frequency */}
|
|
286
|
+
<div className="mt-4 pt-3 border-t border-border/50 flex items-center justify-between">
|
|
287
|
+
<p className={cn(
|
|
288
|
+
"text-sm",
|
|
289
|
+
roiPercentage > 0 ? "font-bold text-[var(--cyan)]" : "text-muted-foreground"
|
|
290
|
+
)}>
|
|
291
|
+
ROI: {roiPercentage >= 0 ? '+' : ''}{roiPercentage.toLocaleString(undefined, { maximumFractionDigits: 0 })}%
|
|
286
292
|
</p>
|
|
287
|
-
|
|
293
|
+
{impliedFrequencyPerYear > 0 && (
|
|
294
|
+
<p className="text-xs text-muted-foreground">
|
|
295
|
+
Implied: ~{impliedFrequencyPerMonth}×/month ({impliedFrequencyPerYear}×/year)
|
|
296
|
+
</p>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
288
299
|
</CardContent>
|
|
289
300
|
</Card>
|
|
290
301
|
)
|
|
@@ -105,6 +105,9 @@ function ScenarioCard({
|
|
|
105
105
|
<Icon size={16} weight="fill" className={config.color} />
|
|
106
106
|
</div>
|
|
107
107
|
<div className="flex-1 min-w-0 pt-0.5">
|
|
108
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
109
|
+
<Badge variant="outline" size="sm" className="font-medium">{config.label}</Badge>
|
|
110
|
+
</div>
|
|
108
111
|
<p className="text-sm text-[var(--black)]">
|
|
109
112
|
<span className="font-medium">When:</span> {scenario.situation}
|
|
110
113
|
</p>
|