@dilipod/ui 0.4.22 → 0.4.24

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.
@@ -0,0 +1,544 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useState } from 'react'
5
+ import {
6
+ Target,
7
+ Crosshair,
8
+ ListNumbers,
9
+ TreeStructure,
10
+ Lightning,
11
+ Wrench,
12
+ Plugs,
13
+ ShieldCheck,
14
+ CaretDown,
15
+ CaretRight,
16
+ Clock,
17
+ TrendUp,
18
+ CurrencyEur,
19
+ Sparkle,
20
+ Lightbulb,
21
+ Robot,
22
+ MagnifyingGlass,
23
+ FilmStrip,
24
+ FileText,
25
+ TextT,
26
+ VideoCamera,
27
+ ImageSquare,
28
+ } from '@phosphor-icons/react'
29
+ import { FlowchartDiagram } from './flowchart-diagram'
30
+ import { Badge } from './badge'
31
+ import {
32
+ Dialog,
33
+ DialogTrigger,
34
+ DialogContent,
35
+ DialogHeader,
36
+ DialogTitle,
37
+ DialogDescription,
38
+ } from './dialog'
39
+
40
+ // ============================================
41
+ // Types
42
+ // ============================================
43
+
44
+ export interface AnalysisSource {
45
+ type: string
46
+ name: string
47
+ size: number
48
+ insights?: string[]
49
+ processing_time_ms?: number
50
+ }
51
+
52
+ export interface AnalysisSummary {
53
+ total_sources: number
54
+ video_count: number
55
+ document_count: number
56
+ image_count: number
57
+ total_insights: number
58
+ analyzed_at: string
59
+ }
60
+
61
+ export interface WorkerSpecDocumentation {
62
+ scope: string | null
63
+ goal: string | null
64
+ steps: Array<{
65
+ step: number
66
+ title: string
67
+ description: string
68
+ tools_used?: string[]
69
+ }> | null
70
+ diagram: string | null
71
+ expected_impact: {
72
+ time_saved_per_occurrence_minutes: number
73
+ frequency: string
74
+ yearly_hours_saved: number
75
+ yearly_cost_savings_euros: number
76
+ qualitative_benefits: string[]
77
+ } | null
78
+ technical_requirements: string[] | null
79
+ integration_points: string[] | null
80
+ edge_cases_handled: Array<{
81
+ scenario: string
82
+ handling: string
83
+ }> | null
84
+ analysis_sources: AnalysisSource[] | null
85
+ analysis_summary: AnalysisSummary | null
86
+ analysis_context: string | null
87
+ version: number
88
+ model_used: string | null
89
+ updated_at: string
90
+ }
91
+
92
+ export interface WorkerSpecProps {
93
+ /** The Knowledge Builder final documentation */
94
+ documentation: WorkerSpecDocumentation | null
95
+ /** Optional className for the container */
96
+ className?: string
97
+ }
98
+
99
+ // ============================================
100
+ // Frequency labels for impact display
101
+ // ============================================
102
+
103
+ const frequencyLabels: Record<string, string> = {
104
+ multiple_daily: 'occurrence',
105
+ daily: 'day',
106
+ weekly: 'week',
107
+ monthly: 'month',
108
+ quarterly: 'quarter',
109
+ yearly: 'year',
110
+ }
111
+
112
+ // ============================================
113
+ // Section Header Component
114
+ // ============================================
115
+
116
+ function SectionHeader({
117
+ icon,
118
+ title,
119
+ count,
120
+ expanded,
121
+ onToggle,
122
+ iconColor = 'text-[var(--cyan)]',
123
+ }: {
124
+ icon: React.ReactNode
125
+ title: string
126
+ count?: number
127
+ expanded: boolean
128
+ onToggle: () => void
129
+ iconColor?: string
130
+ }) {
131
+ return (
132
+ <button
133
+ onClick={onToggle}
134
+ className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide hover:text-[var(--black)] transition-colors w-full"
135
+ >
136
+ {expanded ? <CaretDown size={12} /> : <CaretRight size={12} />}
137
+ <span className={iconColor}>{icon}</span>
138
+ {title}
139
+ {count !== undefined && (
140
+ <span className="text-muted-foreground font-normal">({count})</span>
141
+ )}
142
+ </button>
143
+ )
144
+ }
145
+
146
+ // ============================================
147
+ // Analysis Context Renderer
148
+ // ============================================
149
+
150
+ function AnalysisContextRenderer({ content }: { content: string }) {
151
+ const sections = content.split(/^## /gm).filter(Boolean)
152
+
153
+ const getIcon = (title: string) => {
154
+ if (title.includes('Request')) return <Target size={14} className="text-[var(--cyan)]" />
155
+ if (title.includes('Video')) return <VideoCamera size={14} className="text-[var(--cyan)]" />
156
+ if (title.includes('Document')) return <FileText size={14} className="text-[var(--cyan)]" />
157
+ if (title.includes('Rules')) return <Lightbulb size={14} className="text-amber-500" />
158
+ if (title.includes('Context')) return <TextT size={14} className="text-[var(--cyan)]" />
159
+ return null
160
+ }
161
+
162
+ return (
163
+ <div className="space-y-6">
164
+ {sections.map((section, index) => {
165
+ const lines = section.split('\n')
166
+ const title = lines[0]?.trim()
167
+ const body = lines.slice(1).join('\n').trim()
168
+
169
+ if (!body) return null
170
+
171
+ return (
172
+ <div key={index}>
173
+ <h4 className="text-sm font-medium text-[var(--black)] flex items-center gap-2 mb-3">
174
+ {getIcon(title)}
175
+ {title}
176
+ </h4>
177
+ <div className="text-sm text-muted-foreground pl-5 space-y-2 whitespace-pre-line">
178
+ {body}
179
+ </div>
180
+ </div>
181
+ )
182
+ })}
183
+ </div>
184
+ )
185
+ }
186
+
187
+ // ============================================
188
+ // Main Component
189
+ // ============================================
190
+
191
+ export function WorkerSpec({ documentation, className }: WorkerSpecProps) {
192
+ const [expandedSections, setExpandedSections] = useState<Set<string>>(
193
+ new Set(['goal', 'scope', 'steps', 'diagram', 'impact', 'requirements', 'edge_cases'])
194
+ )
195
+
196
+ const toggleSection = (section: string) => {
197
+ setExpandedSections(prev => {
198
+ const next = new Set(prev)
199
+ if (next.has(section)) {
200
+ next.delete(section)
201
+ } else {
202
+ next.add(section)
203
+ }
204
+ return next
205
+ })
206
+ }
207
+
208
+ // Empty state
209
+ if (!documentation) {
210
+ return (
211
+ <div className={className}>
212
+ <div className="flex items-center gap-3 p-6 rounded-sm border border-dashed border-gray-300 bg-gray-50/50">
213
+ <div className="w-10 h-10 rounded-sm bg-gray-100 flex items-center justify-center">
214
+ <Robot size={20} className="text-gray-400" />
215
+ </div>
216
+ <div>
217
+ <h3 className="font-semibold text-[var(--black)]">Worker Spec Pending</h3>
218
+ <p className="text-sm text-muted-foreground">
219
+ The final specification will be generated automatically after the documentation is approved.
220
+ </p>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ )
225
+ }
226
+
227
+ const freqLabel = documentation.expected_impact?.frequency
228
+ ? frequencyLabels[documentation.expected_impact.frequency] || documentation.expected_impact.frequency
229
+ : 'occurrence'
230
+
231
+ const hasAnalysis = documentation.analysis_context || (documentation.analysis_sources && documentation.analysis_sources.length > 0)
232
+
233
+ return (
234
+ <div className={className}>
235
+ <div className="space-y-5">
236
+ {/* What was analyzed */}
237
+ {hasAnalysis && (
238
+ <div>
239
+ <Dialog>
240
+ <DialogTrigger asChild>
241
+ <button className="flex items-center gap-2 text-sm font-medium text-[var(--cyan)] hover:text-[var(--cyan)]/80 transition-colors">
242
+ <MagnifyingGlass size={16} />
243
+ What was analyzed
244
+ {documentation.analysis_summary && (
245
+ <span className="text-xs text-muted-foreground font-normal">
246
+ ({documentation.analysis_summary.total_sources} source{documentation.analysis_summary.total_sources !== 1 ? 's' : ''})
247
+ </span>
248
+ )}
249
+ <CaretRight size={12} />
250
+ </button>
251
+ </DialogTrigger>
252
+ <DialogContent className="max-w-xl max-h-[80vh] overflow-y-auto">
253
+ <DialogHeader>
254
+ <DialogTitle>What was analyzed</DialogTitle>
255
+ {documentation.analysis_summary && (
256
+ <DialogDescription className="flex items-center gap-4 text-xs">
257
+ <span>{documentation.analysis_summary.total_sources} source{documentation.analysis_summary.total_sources !== 1 ? 's' : ''}</span>
258
+ {documentation.analysis_summary.video_count > 0 && (
259
+ <span className="flex items-center gap-1">
260
+ <FilmStrip size={12} />
261
+ {documentation.analysis_summary.video_count} video{documentation.analysis_summary.video_count !== 1 ? 's' : ''}
262
+ </span>
263
+ )}
264
+ {documentation.analysis_summary.document_count > 0 && (
265
+ <span className="flex items-center gap-1">
266
+ <FileText size={12} />
267
+ {documentation.analysis_summary.document_count} doc{documentation.analysis_summary.document_count !== 1 ? 's' : ''}
268
+ </span>
269
+ )}
270
+ </DialogDescription>
271
+ )}
272
+ </DialogHeader>
273
+
274
+ {/* Sources Section */}
275
+ {documentation.analysis_sources && documentation.analysis_sources.length > 0 && (
276
+ <div className="space-y-2">
277
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Sources</h4>
278
+ <div className="space-y-1.5">
279
+ {documentation.analysis_sources.map((source, i) => (
280
+ <div key={i} className="flex items-center gap-2 text-sm">
281
+ <div className="flex-shrink-0 text-muted-foreground">
282
+ {source.type === 'video' && <FilmStrip size={14} />}
283
+ {(source.type === 'document' || source.type === 'pdf') && <FileText size={14} />}
284
+ {source.type === 'spreadsheet' && <FileText size={14} />}
285
+ {source.type === 'image' && <ImageSquare size={14} />}
286
+ {source.type === 'description' && <TextT size={14} />}
287
+ </div>
288
+ <span className="truncate text-[var(--black)]">{source.name}</span>
289
+ {source.size > 0 && (
290
+ <span className="text-xs text-muted-foreground flex-shrink-0">
291
+ {source.size > 1024 * 1024
292
+ ? `${(source.size / 1024 / 1024).toFixed(1)}MB`
293
+ : `${Math.round(source.size / 1024)}KB`}
294
+ </span>
295
+ )}
296
+ </div>
297
+ ))}
298
+ </div>
299
+ </div>
300
+ )}
301
+
302
+ {/* Analysis Details Section */}
303
+ {documentation.analysis_context && (
304
+ <div className="border-t border-gray-100 pt-4 space-y-1">
305
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Analysis Details</h4>
306
+ <AnalysisContextRenderer content={documentation.analysis_context} />
307
+ </div>
308
+ )}
309
+ </DialogContent>
310
+ </Dialog>
311
+ </div>
312
+ )}
313
+
314
+ {/* Goal */}
315
+ {documentation.goal && (
316
+ <div>
317
+ <SectionHeader
318
+ icon={<Target size={12} weight="fill" />}
319
+ title="Goal"
320
+ expanded={expandedSections.has('goal')}
321
+ onToggle={() => toggleSection('goal')}
322
+ />
323
+ {expandedSections.has('goal') && (
324
+ <p className="text-sm text-muted-foreground leading-relaxed pl-5 mt-2">
325
+ {documentation.goal}
326
+ </p>
327
+ )}
328
+ </div>
329
+ )}
330
+
331
+ {/* Scope */}
332
+ {documentation.scope && (
333
+ <div>
334
+ <SectionHeader
335
+ icon={<Crosshair size={12} weight="fill" />}
336
+ title="Scope"
337
+ expanded={expandedSections.has('scope')}
338
+ onToggle={() => toggleSection('scope')}
339
+ />
340
+ {expandedSections.has('scope') && (
341
+ <p className="text-sm text-muted-foreground leading-relaxed pl-5 mt-2">
342
+ {documentation.scope}
343
+ </p>
344
+ )}
345
+ </div>
346
+ )}
347
+
348
+ {/* Detailed Steps */}
349
+ {documentation.steps && documentation.steps.length > 0 && (
350
+ <div>
351
+ <SectionHeader
352
+ icon={<ListNumbers size={12} weight="fill" />}
353
+ title="Steps"
354
+ count={documentation.steps.length}
355
+ expanded={expandedSections.has('steps')}
356
+ onToggle={() => toggleSection('steps')}
357
+ />
358
+ {expandedSections.has('steps') && (
359
+ <div className="space-y-3 pl-5 mt-2">
360
+ {documentation.steps.map((step, i) => (
361
+ <div key={i} className="flex items-start gap-3">
362
+ <span className="w-6 h-6 rounded-sm bg-[var(--cyan)]/10 flex items-center justify-center text-xs font-bold text-[var(--cyan)] shrink-0 mt-0.5">
363
+ {step.step || i + 1}
364
+ </span>
365
+ <div className="flex-1 min-w-0">
366
+ <p className="text-sm font-medium text-[var(--black)]">{step.title}</p>
367
+ <p className="text-sm text-muted-foreground mt-0.5">{step.description}</p>
368
+ {step.tools_used && step.tools_used.length > 0 && (
369
+ <div className="flex flex-wrap gap-1 mt-1.5">
370
+ {step.tools_used.map((tool, j) => (
371
+ <span key={j} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-sm bg-gray-100 text-[10px] font-medium text-gray-600">
372
+ <Wrench size={10} />
373
+ {tool}
374
+ </span>
375
+ ))}
376
+ </div>
377
+ )}
378
+ </div>
379
+ </div>
380
+ ))}
381
+ </div>
382
+ )}
383
+ </div>
384
+ )}
385
+
386
+ {/* Workflow Diagram */}
387
+ {documentation.diagram && (
388
+ <div>
389
+ <SectionHeader
390
+ icon={<TreeStructure size={12} weight="fill" />}
391
+ title="Workflow Diagram"
392
+ expanded={expandedSections.has('diagram')}
393
+ onToggle={() => toggleSection('diagram')}
394
+ iconColor="text-purple-500"
395
+ />
396
+ {expandedSections.has('diagram') && (
397
+ <div className="pl-5 mt-2">
398
+ <div className="bg-white border border-gray-100 rounded-sm p-4 overflow-x-auto">
399
+ <FlowchartDiagram mermaid={documentation.diagram} />
400
+ </div>
401
+ </div>
402
+ )}
403
+ </div>
404
+ )}
405
+
406
+ {/* Expected Impact */}
407
+ {documentation.expected_impact && (
408
+ <div>
409
+ <SectionHeader
410
+ icon={<Lightning size={12} weight="fill" />}
411
+ title="Expected Impact"
412
+ expanded={expandedSections.has('impact')}
413
+ onToggle={() => toggleSection('impact')}
414
+ iconColor="text-purple-500"
415
+ />
416
+ {expandedSections.has('impact') && (
417
+ <div className="pl-5 mt-2">
418
+ <div className="bg-emerald-50/50 border border-emerald-100 rounded-sm p-4">
419
+ <div className="grid grid-cols-3 gap-3 mb-3">
420
+ <div className="bg-white rounded-sm p-3 border border-emerald-100 text-center">
421
+ <Clock size={18} className="text-emerald-600 mx-auto mb-1" />
422
+ <p className="text-lg font-bold text-[var(--black)]">
423
+ {documentation.expected_impact.time_saved_per_occurrence_minutes} min
424
+ </p>
425
+ <p className="text-[10px] text-muted-foreground">saved per {freqLabel}</p>
426
+ </div>
427
+ <div className="bg-white rounded-sm p-3 border border-emerald-100 text-center">
428
+ <TrendUp size={18} className="text-emerald-600 mx-auto mb-1" />
429
+ <p className="text-lg font-bold text-[var(--black)]">
430
+ {documentation.expected_impact.yearly_hours_saved}h
431
+ </p>
432
+ <p className="text-[10px] text-muted-foreground">saved per year</p>
433
+ </div>
434
+ <div className="bg-white rounded-sm p-3 border border-emerald-100 text-center">
435
+ <CurrencyEur size={18} className="text-emerald-600 mx-auto mb-1" />
436
+ <p className="text-lg font-bold text-[var(--black)]">
437
+ €{documentation.expected_impact.yearly_cost_savings_euros.toLocaleString()}
438
+ </p>
439
+ <p className="text-[10px] text-muted-foreground">estimated yearly savings</p>
440
+ </div>
441
+ </div>
442
+ {documentation.expected_impact.qualitative_benefits && documentation.expected_impact.qualitative_benefits.length > 0 && (
443
+ <div className="space-y-1">
444
+ {documentation.expected_impact.qualitative_benefits.map((benefit, i) => (
445
+ <div key={i} className="flex items-start gap-2 text-sm text-emerald-700">
446
+ <Sparkle size={14} className="text-emerald-500 shrink-0 mt-0.5" weight="fill" />
447
+ {benefit}
448
+ </div>
449
+ ))}
450
+ </div>
451
+ )}
452
+ </div>
453
+ </div>
454
+ )}
455
+ </div>
456
+ )}
457
+
458
+ {/* Technical Requirements & Integration Points */}
459
+ {((documentation.technical_requirements && documentation.technical_requirements.length > 0) ||
460
+ (documentation.integration_points && documentation.integration_points.length > 0)) && (
461
+ <div>
462
+ <SectionHeader
463
+ icon={<Plugs size={12} weight="fill" />}
464
+ title="Requirements & Integrations"
465
+ expanded={expandedSections.has('requirements')}
466
+ onToggle={() => toggleSection('requirements')}
467
+ />
468
+ {expandedSections.has('requirements') && (
469
+ <div className="grid md:grid-cols-2 gap-4 pl-5 mt-2">
470
+ {documentation.technical_requirements && documentation.technical_requirements.length > 0 && (
471
+ <div>
472
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
473
+ Technical Requirements
474
+ </p>
475
+ <ul className="space-y-1.5">
476
+ {documentation.technical_requirements.map((req, i) => (
477
+ <li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
478
+ <Wrench size={12} className="text-gray-400 shrink-0 mt-1" />
479
+ {req}
480
+ </li>
481
+ ))}
482
+ </ul>
483
+ </div>
484
+ )}
485
+ {documentation.integration_points && documentation.integration_points.length > 0 && (
486
+ <div>
487
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
488
+ Integration Points
489
+ </p>
490
+ <ul className="space-y-1.5">
491
+ {documentation.integration_points.map((point, i) => (
492
+ <li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
493
+ <Plugs size={12} className="text-gray-400 shrink-0 mt-1" />
494
+ {point}
495
+ </li>
496
+ ))}
497
+ </ul>
498
+ </div>
499
+ )}
500
+ </div>
501
+ )}
502
+ </div>
503
+ )}
504
+
505
+ {/* Edge Cases Handled */}
506
+ {documentation.edge_cases_handled && documentation.edge_cases_handled.length > 0 && (
507
+ <div>
508
+ <SectionHeader
509
+ icon={<ShieldCheck size={12} weight="fill" />}
510
+ title="Edge Cases Handled"
511
+ count={documentation.edge_cases_handled.length}
512
+ expanded={expandedSections.has('edge_cases')}
513
+ onToggle={() => toggleSection('edge_cases')}
514
+ iconColor="text-amber-500"
515
+ />
516
+ {expandedSections.has('edge_cases') && (
517
+ <div className="space-y-2 pl-5 mt-2">
518
+ {documentation.edge_cases_handled.map((ec, i) => (
519
+ <div key={i} className="text-sm p-3 bg-gray-50 rounded-sm border border-gray-100">
520
+ <p className="font-medium text-[var(--black)]">{ec.scenario}</p>
521
+ <p className="text-muted-foreground mt-1">→ {ec.handling}</p>
522
+ </div>
523
+ ))}
524
+ </div>
525
+ )}
526
+ </div>
527
+ )}
528
+
529
+ {/* Version footer */}
530
+ <div className="pt-3 border-t border-gray-100 flex items-center gap-2 text-xs text-muted-foreground">
531
+ <Badge variant="outline" size="sm">v{documentation.version}</Badge>
532
+ {documentation.model_used && (
533
+ <>
534
+ <span>•</span>
535
+ <span>{documentation.model_used}</span>
536
+ </>
537
+ )}
538
+ <span>•</span>
539
+ <span>Updated {new Date(documentation.updated_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</span>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ )
544
+ }
package/src/index.ts CHANGED
@@ -293,6 +293,14 @@ export type {
293
293
  WorkflowTemplate
294
294
  } from './components/workflow-viewer'
295
295
 
296
+ // Worker Spec Components
297
+ export { WorkerSpec } from './components/worker-spec'
298
+ export type { WorkerSpecProps, WorkerSpecDocumentation, AnalysisSource, AnalysisSummary } from './components/worker-spec'
299
+
300
+ // Flowchart Diagram Components
301
+ export { FlowchartDiagram } from './components/flowchart-diagram'
302
+ export type { FlowchartDiagramProps } from './components/flowchart-diagram'
303
+
296
304
  // Utilities
297
305
  export { cn } from './lib/utils'
298
306