@baselineos/persona 0.1.0

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/src/ui.ts ADDED
@@ -0,0 +1,1042 @@
1
+ import { EventEmitter } from 'events';
2
+ import { randomUUID } from 'crypto';
3
+ import { BaselinePersonaEngine } from './engine.js';
4
+ import type { ActivePersona, StudioConfig } from './engine.js';
5
+
6
+ // ─── Type Definitions ────────────────────────────────────────────────────────
7
+
8
+ export interface UIComponent {
9
+ id: string;
10
+ type: string;
11
+ visible: boolean;
12
+ position: { x: number; y: number };
13
+ size: { width: number; height: number };
14
+ config: Record<string, unknown>;
15
+ state: Record<string, unknown>;
16
+ }
17
+
18
+ export interface PersonaOption {
19
+ id: string;
20
+ name: string;
21
+ description: string;
22
+ focus: string[];
23
+ icon?: string;
24
+ color?: string;
25
+ }
26
+
27
+ export interface WorkflowStep {
28
+ id: string;
29
+ name: string;
30
+ description: string;
31
+ status: 'pending' | 'active' | 'completed' | 'skipped' | 'failed';
32
+ startedAt?: Date;
33
+ completedAt?: Date;
34
+ data: Record<string, unknown>;
35
+ }
36
+
37
+ export interface WorkflowInstance {
38
+ id: string;
39
+ type: string;
40
+ personaId: string;
41
+ userId: string;
42
+ parameters: Record<string, unknown>;
43
+ startTime: Date;
44
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
45
+ progress: number;
46
+ steps: WorkflowStep[];
47
+ results: Record<string, unknown>;
48
+ endTime?: Date;
49
+ error?: string;
50
+ }
51
+
52
+ export interface ProgressState {
53
+ workflowId: string;
54
+ phase: string;
55
+ percentage: number;
56
+ currentStep: string;
57
+ stepsCompleted: number;
58
+ totalSteps: number;
59
+ estimatedTimeRemaining?: number;
60
+ startedAt: Date;
61
+ }
62
+
63
+ export interface ResultDisplay {
64
+ id: string;
65
+ workflowId: string;
66
+ type: string;
67
+ title: string;
68
+ data: Record<string, unknown>;
69
+ format: string;
70
+ createdAt: Date;
71
+ }
72
+
73
+ export interface SelectorState {
74
+ isOpen: boolean;
75
+ selectedPersonaId: string | null;
76
+ hoveredPersonaId: string | null;
77
+ searchQuery: string;
78
+ filteredOptions: PersonaOption[];
79
+ }
80
+
81
+ export interface StudioState {
82
+ activeFrames: string[];
83
+ layout: string;
84
+ theme: string;
85
+ panels: UIComponent[];
86
+ toolbars: UIComponent[];
87
+ }
88
+
89
+ export interface UISystemConfig {
90
+ defaultLayout?: string;
91
+ defaultTheme?: string;
92
+ enableAnimations?: boolean;
93
+ enableTransitions?: boolean;
94
+ selectorPosition?: string;
95
+ maxWorkflows?: number;
96
+ progressUpdateInterval?: number;
97
+ }
98
+
99
+ // ─── Persona UI System ──────────────────────────────────────────────────────
100
+
101
+ export class PersonaUISystem extends EventEmitter {
102
+ private engine: BaselinePersonaEngine;
103
+ private components: Map<string, UIComponent>;
104
+ private selectorState: SelectorState;
105
+ private studioState: StudioState;
106
+ private workflows: Map<string, WorkflowInstance>;
107
+ private progressStates: Map<string, ProgressState>;
108
+ private resultDisplays: Map<string, ResultDisplay>;
109
+ private config: Required<UISystemConfig>;
110
+ private initialized: boolean;
111
+
112
+ constructor(engine: BaselinePersonaEngine, config: UISystemConfig = {}) {
113
+ super();
114
+ this.engine = engine;
115
+ this.components = new Map();
116
+ this.workflows = new Map();
117
+ this.progressStates = new Map();
118
+ this.resultDisplays = new Map();
119
+
120
+ this.config = {
121
+ defaultLayout: config.defaultLayout || 'responsive',
122
+ defaultTheme: config.defaultTheme || 'system',
123
+ enableAnimations: config.enableAnimations ?? true,
124
+ enableTransitions: config.enableTransitions ?? true,
125
+ selectorPosition: config.selectorPosition || 'top-right',
126
+ maxWorkflows: config.maxWorkflows || 10,
127
+ progressUpdateInterval: config.progressUpdateInterval || 1000,
128
+ };
129
+
130
+ this.selectorState = {
131
+ isOpen: false,
132
+ selectedPersonaId: null,
133
+ hoveredPersonaId: null,
134
+ searchQuery: '',
135
+ filteredOptions: [],
136
+ };
137
+
138
+ this.studioState = {
139
+ activeFrames: [],
140
+ layout: this.config.defaultLayout,
141
+ theme: this.config.defaultTheme,
142
+ panels: [],
143
+ toolbars: [],
144
+ };
145
+
146
+ this.initialized = false;
147
+ this.initializeComponents();
148
+ this.initializeEventListeners();
149
+ this.initialized = true;
150
+ console.log('[PersonaUISystem] UI system initialized');
151
+ }
152
+
153
+ // ─── Component Initialization ─────────────────────────────────────────────
154
+
155
+ private initializeComponents(): void {
156
+ this.registerComponent({
157
+ id: 'persona-selector',
158
+ type: 'selector',
159
+ visible: true,
160
+ position: { x: 0, y: 0 },
161
+ size: { width: 320, height: 480 },
162
+ config: {
163
+ position: this.config.selectorPosition,
164
+ showSearch: true,
165
+ showDescriptions: true,
166
+ showFocusAreas: true,
167
+ animateTransitions: this.config.enableAnimations,
168
+ },
169
+ state: {},
170
+ });
171
+
172
+ this.registerComponent({
173
+ id: 'studio-interface',
174
+ type: 'studio',
175
+ visible: false,
176
+ position: { x: 0, y: 0 },
177
+ size: { width: 1920, height: 1080 },
178
+ config: {
179
+ layout: this.config.defaultLayout,
180
+ theme: this.config.defaultTheme,
181
+ enableDragDrop: true,
182
+ enableResize: true,
183
+ enablePanelCollapse: true,
184
+ },
185
+ state: {},
186
+ });
187
+
188
+ this.registerComponent({
189
+ id: 'workflow-engine',
190
+ type: 'workflow',
191
+ visible: false,
192
+ position: { x: 0, y: 0 },
193
+ size: { width: 800, height: 600 },
194
+ config: {
195
+ maxConcurrent: this.config.maxWorkflows,
196
+ showProgress: true,
197
+ enableCancel: true,
198
+ enablePause: false,
199
+ },
200
+ state: {},
201
+ });
202
+
203
+ this.registerComponent({
204
+ id: 'result-display',
205
+ type: 'display',
206
+ visible: false,
207
+ position: { x: 0, y: 0 },
208
+ size: { width: 800, height: 600 },
209
+ config: {
210
+ formats: ['table', 'chart', 'json', 'markdown', 'html'],
211
+ defaultFormat: 'table',
212
+ enableExport: true,
213
+ enableShare: true,
214
+ },
215
+ state: {},
216
+ });
217
+
218
+ this.registerComponent({
219
+ id: 'progress-tracker',
220
+ type: 'tracker',
221
+ visible: false,
222
+ position: { x: 0, y: 0 },
223
+ size: { width: 400, height: 200 },
224
+ config: {
225
+ updateInterval: this.config.progressUpdateInterval,
226
+ showEstimatedTime: true,
227
+ showStepDetails: true,
228
+ position: 'bottom-right',
229
+ },
230
+ state: {},
231
+ });
232
+ }
233
+
234
+ private initializeEventListeners(): void {
235
+ this.on('selector:opened', () => {
236
+ this.selectorState.isOpen = true;
237
+ this.updateComponent('persona-selector', { state: { isOpen: true } });
238
+ console.log('[PersonaUISystem] Persona selector opened');
239
+ });
240
+
241
+ this.on('selector:closed', () => {
242
+ this.selectorState.isOpen = false;
243
+ this.selectorState.hoveredPersonaId = null;
244
+ this.updateComponent('persona-selector', { state: { isOpen: false } });
245
+ console.log('[PersonaUISystem] Persona selector closed');
246
+ });
247
+
248
+ this.on('persona:selected', (data: { personaId: string; userId: string }) => {
249
+ this.handlePersonaSelection(data.personaId, data.userId);
250
+ });
251
+
252
+ this.on('workflow:started', (data: { workflowId: string }) => {
253
+ this.showComponent('progress-tracker');
254
+ console.log(`[PersonaUISystem] Workflow started: ${data.workflowId}`);
255
+ });
256
+
257
+ this.on('workflow:completed', (data: { workflowId: string }) => {
258
+ this.showComponent('result-display');
259
+ console.log(`[PersonaUISystem] Workflow completed: ${data.workflowId}`);
260
+ });
261
+
262
+ this.on('workflow:failed', (data: { workflowId: string; error: string }) => {
263
+ console.log(`[PersonaUISystem] Workflow failed: ${data.workflowId} - ${data.error}`);
264
+ });
265
+ }
266
+
267
+ // ─── Component Management ─────────────────────────────────────────────────
268
+
269
+ registerComponent(component: UIComponent): void {
270
+ this.components.set(component.id, component);
271
+ this.emit('component:registered', { componentId: component.id, type: component.type });
272
+ }
273
+
274
+ getComponent(id: string): UIComponent | undefined {
275
+ return this.components.get(id);
276
+ }
277
+
278
+ updateComponent(id: string, updates: Partial<UIComponent>): UIComponent | null {
279
+ const component = this.components.get(id);
280
+ if (!component) {
281
+ return null;
282
+ }
283
+
284
+ if (updates.config) {
285
+ component.config = { ...component.config, ...updates.config };
286
+ }
287
+ if (updates.state) {
288
+ component.state = { ...component.state, ...updates.state };
289
+ }
290
+ if (updates.position) {
291
+ component.position = updates.position;
292
+ }
293
+ if (updates.size) {
294
+ component.size = updates.size;
295
+ }
296
+ if (updates.visible !== undefined) {
297
+ component.visible = updates.visible;
298
+ }
299
+
300
+ this.emit('component:updated', { componentId: id, updates });
301
+ return component;
302
+ }
303
+
304
+ showComponent(id: string): void {
305
+ this.updateComponent(id, { visible: true });
306
+ this.emit('component:shown', { componentId: id });
307
+ }
308
+
309
+ hideComponent(id: string): void {
310
+ this.updateComponent(id, { visible: false });
311
+ this.emit('component:hidden', { componentId: id });
312
+ }
313
+
314
+ removeComponent(id: string): boolean {
315
+ const removed = this.components.delete(id);
316
+ if (removed) {
317
+ this.emit('component:removed', { componentId: id });
318
+ }
319
+ return removed;
320
+ }
321
+
322
+ getAllComponents(): UIComponent[] {
323
+ return Array.from(this.components.values());
324
+ }
325
+
326
+ getVisibleComponents(): UIComponent[] {
327
+ return Array.from(this.components.values()).filter((c) => c.visible);
328
+ }
329
+
330
+ // ─── Persona Selector ─────────────────────────────────────────────────────
331
+
332
+ openSelector(): void {
333
+ this.emit('selector:opened');
334
+ }
335
+
336
+ closeSelector(): void {
337
+ this.emit('selector:closed');
338
+ }
339
+
340
+ toggleSelector(): void {
341
+ if (this.selectorState.isOpen) {
342
+ this.closeSelector();
343
+ } else {
344
+ this.openSelector();
345
+ }
346
+ }
347
+
348
+ setSearchQuery(query: string): PersonaOption[] {
349
+ this.selectorState.searchQuery = query;
350
+ const filtered = this.filterPersonaOptions(query);
351
+ this.selectorState.filteredOptions = filtered;
352
+ this.emit('selector:filtered', { query, count: filtered.length });
353
+ return filtered;
354
+ }
355
+
356
+ private filterPersonaOptions(query: string): PersonaOption[] {
357
+ const allOptions = this.getPersonaOptions();
358
+ if (!query || query.trim() === '') {
359
+ return allOptions;
360
+ }
361
+
362
+ const lowerQuery = query.toLowerCase();
363
+ return allOptions.filter(
364
+ (option) =>
365
+ option.name.toLowerCase().includes(lowerQuery) ||
366
+ option.description.toLowerCase().includes(lowerQuery) ||
367
+ option.focus.some((f) => f.toLowerCase().includes(lowerQuery))
368
+ );
369
+ }
370
+
371
+ getPersonaOptions(): PersonaOption[] {
372
+ const options: PersonaOption[] = [];
373
+
374
+ const personaColors: Record<string, string> = {
375
+ designer: '#FF6B6B',
376
+ developer: '#4ECDC4',
377
+ business_analyst: '#45B7D1',
378
+ data_scientist: '#96CEB4',
379
+ product_manager: '#FFEAA7',
380
+ };
381
+
382
+ const personaIcons: Record<string, string> = {
383
+ designer: 'palette',
384
+ developer: 'code',
385
+ business_analyst: 'briefcase',
386
+ data_scientist: 'chart',
387
+ product_manager: 'target',
388
+ };
389
+
390
+ const corePersonaIds = ['designer', 'developer', 'business_analyst', 'data_scientist', 'product_manager'];
391
+
392
+ for (const id of corePersonaIds) {
393
+ const persona = this.engine.getActivePersona(id);
394
+ if (persona) {
395
+ options.push({
396
+ id: persona.id,
397
+ name: persona.name,
398
+ description: persona.description,
399
+ focus: persona.focus,
400
+ icon: personaIcons[id],
401
+ color: personaColors[id],
402
+ });
403
+ } else {
404
+ options.push(this.getDefaultPersonaOption(id, personaIcons[id], personaColors[id]));
405
+ }
406
+ }
407
+
408
+ if (options.length === 0) {
409
+ for (const id of corePersonaIds) {
410
+ options.push(this.getDefaultPersonaOption(id, personaIcons[id], personaColors[id]));
411
+ }
412
+ }
413
+
414
+ return options;
415
+ }
416
+
417
+ private getDefaultPersonaOption(id: string, icon?: string, color?: string): PersonaOption {
418
+ const defaults: Record<string, PersonaOption> = {
419
+ designer: {
420
+ id: 'designer',
421
+ name: 'Designer',
422
+ description: 'Visual and UX designer focused on creating beautiful, intuitive interfaces',
423
+ focus: ['visual-design', 'ux-research', 'prototyping', 'design-systems', 'accessibility'],
424
+ icon: icon || 'palette',
425
+ color: color || '#FF6B6B',
426
+ },
427
+ developer: {
428
+ id: 'developer',
429
+ name: 'Developer',
430
+ description: 'Software developer focused on building robust, scalable applications',
431
+ focus: ['coding', 'architecture', 'testing', 'deployment', 'performance'],
432
+ icon: icon || 'code',
433
+ color: color || '#4ECDC4',
434
+ },
435
+ business_analyst: {
436
+ id: 'business_analyst',
437
+ name: 'Business Analyst',
438
+ description: 'Business analyst focused on requirements gathering and process optimization',
439
+ focus: ['requirements', 'process-analysis', 'stakeholder-management', 'data-analysis'],
440
+ icon: icon || 'briefcase',
441
+ color: color || '#45B7D1',
442
+ },
443
+ data_scientist: {
444
+ id: 'data_scientist',
445
+ name: 'Data Scientist',
446
+ description: 'Data scientist focused on extracting insights through analysis and modeling',
447
+ focus: ['data-analysis', 'machine-learning', 'statistical-modeling', 'data-visualization'],
448
+ icon: icon || 'chart',
449
+ color: color || '#96CEB4',
450
+ },
451
+ product_manager: {
452
+ id: 'product_manager',
453
+ name: 'Product Manager',
454
+ description: 'Product manager focused on strategy, roadmapping, and delivering value',
455
+ focus: ['product-strategy', 'roadmapping', 'user-research', 'prioritization'],
456
+ icon: icon || 'target',
457
+ color: color || '#FFEAA7',
458
+ },
459
+ };
460
+
461
+ return defaults[id] || {
462
+ id,
463
+ name: id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
464
+ description: `Persona: ${id}`,
465
+ focus: [],
466
+ icon: icon || 'user',
467
+ color: color || '#CCCCCC',
468
+ };
469
+ }
470
+
471
+ hoverPersona(personaId: string): void {
472
+ this.selectorState.hoveredPersonaId = personaId;
473
+ this.emit('selector:hovered', { personaId });
474
+ }
475
+
476
+ selectPersona(personaId: string, userId: string): void {
477
+ this.selectorState.selectedPersonaId = personaId;
478
+ this.emit('persona:selected', { personaId, userId });
479
+ this.closeSelector();
480
+ }
481
+
482
+ getSelectorState(): SelectorState {
483
+ return { ...this.selectorState };
484
+ }
485
+
486
+ // ─── Persona Selection Handling ────────────────────────────────────────────
487
+
488
+ private handlePersonaSelection(personaId: string, userId: string): void {
489
+ try {
490
+ const activePersona = this.engine.setActivePersona(personaId, userId);
491
+ this.applyPersonaToStudio(activePersona);
492
+ this.engine.learnFromBehavior(userId, {
493
+ type: 'persona_selected',
494
+ personaId,
495
+ timestamp: new Date(),
496
+ });
497
+ console.log(`[PersonaUISystem] Persona applied: ${personaId} for user ${userId}`);
498
+ } catch (error: unknown) {
499
+ const message = error instanceof Error ? error.message : String(error);
500
+ console.log(`[PersonaUISystem] Failed to apply persona: ${message}`);
501
+ this.emit('error', { type: 'persona_selection_failed', personaId, userId, error: message });
502
+ }
503
+ }
504
+
505
+ private applyPersonaToStudio(persona: ActivePersona): void {
506
+ const studioConfig = persona.studio;
507
+
508
+ this.studioState.layout = studioConfig.interface.layout;
509
+ this.studioState.theme = studioConfig.interface.colorScheme;
510
+ this.studioState.activeFrames = [
511
+ ...(studioConfig.frames.primary || []),
512
+ ...(studioConfig.frames.secondary || []),
513
+ ];
514
+
515
+ this.updateComponent('studio-interface', {
516
+ visible: true,
517
+ config: {
518
+ layout: studioConfig.interface.layout,
519
+ theme: studioConfig.interface.colorScheme,
520
+ typography: studioConfig.interface.typography,
521
+ visualElements: studioConfig.interface.visualElements,
522
+ },
523
+ state: {
524
+ activePersonaId: persona.id,
525
+ activeFrames: this.studioState.activeFrames,
526
+ },
527
+ });
528
+
529
+ this.studioState.panels = this.generatePanels(studioConfig);
530
+ this.studioState.toolbars = this.generateToolbars(studioConfig);
531
+
532
+ this.emit('studio:configured', {
533
+ personaId: persona.id,
534
+ layout: studioConfig.interface.layout,
535
+ frameCount: this.studioState.activeFrames.length,
536
+ });
537
+ }
538
+
539
+ private generatePanels(studioConfig: StudioConfig): UIComponent[] {
540
+ const panels: UIComponent[] = [];
541
+
542
+ const visualElements = studioConfig.interface.visualElements || [];
543
+ for (let i = 0; i < visualElements.length; i++) {
544
+ panels.push({
545
+ id: `panel-${visualElements[i]}`,
546
+ type: 'panel',
547
+ visible: true,
548
+ position: { x: 0, y: i * 200 },
549
+ size: { width: 300, height: 200 },
550
+ config: {
551
+ title: visualElements[i].replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
552
+ collapsible: true,
553
+ draggable: true,
554
+ },
555
+ state: {},
556
+ });
557
+ }
558
+
559
+ return panels;
560
+ }
561
+
562
+ private generateToolbars(studioConfig: StudioConfig): UIComponent[] {
563
+ const toolbars: UIComponent[] = [];
564
+
565
+ const workflows = studioConfig.workflows || {};
566
+ for (const [category, items] of Object.entries(workflows)) {
567
+ toolbars.push({
568
+ id: `toolbar-${category}`,
569
+ type: 'toolbar',
570
+ visible: true,
571
+ position: { x: 0, y: 0 },
572
+ size: { width: 600, height: 48 },
573
+ config: {
574
+ category,
575
+ items: items,
576
+ orientation: 'horizontal',
577
+ },
578
+ state: {},
579
+ });
580
+ }
581
+
582
+ return toolbars;
583
+ }
584
+
585
+ getStudioState(): StudioState {
586
+ return { ...this.studioState };
587
+ }
588
+
589
+ // ─── Workflow Engine ──────────────────────────────────────────────────────
590
+
591
+ startWorkflow(
592
+ type: string,
593
+ personaId: string,
594
+ userId: string,
595
+ parameters: Record<string, unknown> = {}
596
+ ): WorkflowInstance {
597
+ if (this.workflows.size >= this.config.maxWorkflows) {
598
+ throw new Error(`Maximum concurrent workflows (${this.config.maxWorkflows}) reached`);
599
+ }
600
+
601
+ const steps = this.generateWorkflowSteps(type, personaId);
602
+
603
+ const workflow: WorkflowInstance = {
604
+ id: randomUUID(),
605
+ type,
606
+ personaId,
607
+ userId,
608
+ parameters,
609
+ startTime: new Date(),
610
+ status: 'pending',
611
+ progress: 0,
612
+ steps,
613
+ results: {},
614
+ };
615
+
616
+ this.workflows.set(workflow.id, workflow);
617
+
618
+ const progressState: ProgressState = {
619
+ workflowId: workflow.id,
620
+ phase: 'initialization',
621
+ percentage: 0,
622
+ currentStep: steps.length > 0 ? steps[0].name : 'unknown',
623
+ stepsCompleted: 0,
624
+ totalSteps: steps.length,
625
+ startedAt: new Date(),
626
+ };
627
+ this.progressStates.set(workflow.id, progressState);
628
+
629
+ this.emit('workflow:started', { workflowId: workflow.id, type, personaId, userId });
630
+
631
+ this.engine.learnFromBehavior(userId, {
632
+ type: 'workflow_start',
633
+ workflowType: type,
634
+ personaId,
635
+ timestamp: new Date(),
636
+ });
637
+
638
+ this.runWorkflow(workflow.id);
639
+
640
+ return workflow;
641
+ }
642
+
643
+ private generateWorkflowSteps(type: string, _personaId: string): WorkflowStep[] {
644
+ const defaultSteps: Record<string, string[]> = {
645
+ design: ['Research', 'Ideation', 'Wireframing', 'Mockup', 'Prototype', 'Testing', 'Iteration'],
646
+ development: ['Planning', 'Architecture', 'Implementation', 'Testing', 'Code Review', 'Deployment'],
647
+ analysis: ['Data Collection', 'Data Cleaning', 'Exploration', 'Analysis', 'Visualization', 'Reporting'],
648
+ 'product-planning': ['Research', 'Strategy', 'Requirements', 'Prioritization', 'Roadmap', 'Communication'],
649
+ 'sprint-planning': ['Backlog Review', 'Capacity Planning', 'Story Selection', 'Task Breakdown', 'Commitment'],
650
+ default: ['Initialize', 'Process', 'Validate', 'Complete'],
651
+ };
652
+
653
+ const stepNames = defaultSteps[type] || defaultSteps['default'];
654
+
655
+ return stepNames.map((name, index) => ({
656
+ id: randomUUID(),
657
+ name,
658
+ description: `${name} phase of ${type} workflow`,
659
+ status: index === 0 ? 'active' : 'pending' as 'pending' | 'active' | 'completed' | 'skipped' | 'failed',
660
+ data: {},
661
+ }));
662
+ }
663
+
664
+ private async runWorkflow(workflowId: string): Promise<void> {
665
+ const workflow = this.workflows.get(workflowId);
666
+ if (!workflow) {
667
+ return;
668
+ }
669
+
670
+ workflow.status = 'running';
671
+
672
+ for (let i = 0; i < workflow.steps.length; i++) {
673
+ const step = workflow.steps[i];
674
+
675
+ if ((workflow.status as string) === 'cancelled') {
676
+ step.status = 'skipped';
677
+ continue;
678
+ }
679
+
680
+ step.status = 'active';
681
+ step.startedAt = new Date();
682
+
683
+ this.updateProgress(workflowId, {
684
+ phase: step.name.toLowerCase(),
685
+ currentStep: step.name,
686
+ stepsCompleted: i,
687
+ percentage: Math.round((i / workflow.steps.length) * 100),
688
+ });
689
+
690
+ this.emit('workflow:step', { workflowId, stepId: step.id, stepName: step.name, stepIndex: i });
691
+
692
+ try {
693
+ await this.executeWorkflowStep(workflow, step);
694
+ step.status = 'completed';
695
+ step.completedAt = new Date();
696
+ } catch (error: unknown) {
697
+ const message = error instanceof Error ? error.message : String(error);
698
+ step.status = 'failed';
699
+ workflow.status = 'failed';
700
+ workflow.error = message;
701
+ workflow.endTime = new Date();
702
+
703
+ this.emit('workflow:failed', { workflowId, error: message, failedStep: step.name });
704
+ return;
705
+ }
706
+ }
707
+
708
+ workflow.status = 'completed';
709
+ workflow.progress = 100;
710
+ workflow.endTime = new Date();
711
+
712
+ this.updateProgress(workflowId, {
713
+ phase: 'complete',
714
+ currentStep: 'Done',
715
+ stepsCompleted: workflow.steps.length,
716
+ percentage: 100,
717
+ });
718
+
719
+ this.generateResults(workflow);
720
+
721
+ this.emit('workflow:completed', { workflowId, results: workflow.results });
722
+
723
+ this.engine.learnFromBehavior(workflow.userId, {
724
+ type: 'workflow_complete',
725
+ workflowType: workflow.type,
726
+ personaId: workflow.personaId,
727
+ duration: workflow.endTime.getTime() - workflow.startTime.getTime(),
728
+ timestamp: new Date(),
729
+ });
730
+ }
731
+
732
+ private async executeWorkflowStep(workflow: WorkflowInstance, step: WorkflowStep): Promise<void> {
733
+ const duration = 50 + Math.random() * 150;
734
+ await new Promise<void>((resolve) => setTimeout(resolve, duration));
735
+
736
+ step.data = {
737
+ executedAt: new Date(),
738
+ duration,
739
+ workflowType: workflow.type,
740
+ personaId: workflow.personaId,
741
+ };
742
+ }
743
+
744
+ private generateResults(workflow: WorkflowInstance): void {
745
+ const completedSteps = workflow.steps.filter((s) => s.status === 'completed');
746
+ const failedSteps = workflow.steps.filter((s) => s.status === 'failed');
747
+ const totalDuration = workflow.endTime
748
+ ? workflow.endTime.getTime() - workflow.startTime.getTime()
749
+ : 0;
750
+
751
+ workflow.results = {
752
+ summary: {
753
+ workflowType: workflow.type,
754
+ personaId: workflow.personaId,
755
+ totalSteps: workflow.steps.length,
756
+ completedSteps: completedSteps.length,
757
+ failedSteps: failedSteps.length,
758
+ totalDuration,
759
+ averageStepDuration: completedSteps.length > 0 ? totalDuration / completedSteps.length : 0,
760
+ },
761
+ steps: workflow.steps.map((s) => ({
762
+ name: s.name,
763
+ status: s.status,
764
+ duration: s.completedAt && s.startedAt
765
+ ? s.completedAt.getTime() - s.startedAt.getTime()
766
+ : null,
767
+ data: s.data,
768
+ })),
769
+ artifacts: this.generateArtifacts(workflow),
770
+ };
771
+ }
772
+
773
+ private generateArtifacts(workflow: WorkflowInstance): Record<string, unknown>[] {
774
+ const artifacts: Record<string, unknown>[] = [];
775
+
776
+ switch (workflow.type) {
777
+ case 'design':
778
+ artifacts.push(
779
+ { type: 'wireframe', name: 'Initial Wireframes', format: 'svg' },
780
+ { type: 'mockup', name: 'High-Fidelity Mockups', format: 'png' },
781
+ { type: 'prototype', name: 'Interactive Prototype', format: 'html' }
782
+ );
783
+ break;
784
+ case 'development':
785
+ artifacts.push(
786
+ { type: 'code', name: 'Source Code', format: 'typescript' },
787
+ { type: 'tests', name: 'Test Suite', format: 'typescript' },
788
+ { type: 'documentation', name: 'API Documentation', format: 'markdown' }
789
+ );
790
+ break;
791
+ case 'analysis':
792
+ artifacts.push(
793
+ { type: 'dataset', name: 'Processed Dataset', format: 'csv' },
794
+ { type: 'visualization', name: 'Data Visualizations', format: 'svg' },
795
+ { type: 'report', name: 'Analysis Report', format: 'markdown' }
796
+ );
797
+ break;
798
+ case 'product-planning':
799
+ artifacts.push(
800
+ { type: 'roadmap', name: 'Product Roadmap', format: 'json' },
801
+ { type: 'requirements', name: 'Requirements Document', format: 'markdown' },
802
+ { type: 'metrics', name: 'Success Metrics', format: 'json' }
803
+ );
804
+ break;
805
+ default:
806
+ artifacts.push(
807
+ { type: 'output', name: 'Workflow Output', format: 'json' }
808
+ );
809
+ }
810
+
811
+ return artifacts;
812
+ }
813
+
814
+ cancelWorkflow(workflowId: string): boolean {
815
+ const workflow = this.workflows.get(workflowId);
816
+ if (!workflow || workflow.status !== 'running') {
817
+ return false;
818
+ }
819
+
820
+ workflow.status = 'cancelled';
821
+ workflow.endTime = new Date();
822
+ this.emit('workflow:cancelled', { workflowId });
823
+ return true;
824
+ }
825
+
826
+ getWorkflow(workflowId: string): WorkflowInstance | undefined {
827
+ return this.workflows.get(workflowId);
828
+ }
829
+
830
+ getActiveWorkflows(): WorkflowInstance[] {
831
+ return Array.from(this.workflows.values()).filter(
832
+ (w) => w.status === 'running' || w.status === 'pending'
833
+ );
834
+ }
835
+
836
+ getCompletedWorkflows(): WorkflowInstance[] {
837
+ return Array.from(this.workflows.values()).filter(
838
+ (w) => w.status === 'completed'
839
+ );
840
+ }
841
+
842
+ getAllWorkflows(): WorkflowInstance[] {
843
+ return Array.from(this.workflows.values());
844
+ }
845
+
846
+ // ─── Progress Tracking ────────────────────────────────────────────────────
847
+
848
+ private updateProgress(workflowId: string, updates: Partial<ProgressState>): void {
849
+ const state = this.progressStates.get(workflowId);
850
+ if (!state) {
851
+ return;
852
+ }
853
+
854
+ Object.assign(state, updates);
855
+
856
+ if (state.percentage < 100 && state.stepsCompleted > 0) {
857
+ const elapsed = Date.now() - state.startedAt.getTime();
858
+ const avgStepTime = elapsed / state.stepsCompleted;
859
+ const remainingSteps = state.totalSteps - state.stepsCompleted;
860
+ state.estimatedTimeRemaining = Math.round(avgStepTime * remainingSteps);
861
+ }
862
+
863
+ this.emit('progress:updated', { workflowId, progress: { ...state } });
864
+ }
865
+
866
+ getProgress(workflowId: string): ProgressState | undefined {
867
+ return this.progressStates.get(workflowId);
868
+ }
869
+
870
+ getAllProgress(): ProgressState[] {
871
+ return Array.from(this.progressStates.values());
872
+ }
873
+
874
+ getActiveProgress(): ProgressState[] {
875
+ return Array.from(this.progressStates.values()).filter((p) => p.percentage < 100);
876
+ }
877
+
878
+ // ─── Result Display ───────────────────────────────────────────────────────
879
+
880
+ createResultDisplay(workflowId: string, type: string, title: string, data: Record<string, unknown>, format: string = 'table'): ResultDisplay {
881
+ const result: ResultDisplay = {
882
+ id: randomUUID(),
883
+ workflowId,
884
+ type,
885
+ title,
886
+ data,
887
+ format,
888
+ createdAt: new Date(),
889
+ };
890
+
891
+ this.resultDisplays.set(result.id, result);
892
+ this.showComponent('result-display');
893
+ this.emit('result:created', { resultId: result.id, workflowId, type });
894
+ return result;
895
+ }
896
+
897
+ getResultDisplay(resultId: string): ResultDisplay | undefined {
898
+ return this.resultDisplays.get(resultId);
899
+ }
900
+
901
+ getResultsForWorkflow(workflowId: string): ResultDisplay[] {
902
+ return Array.from(this.resultDisplays.values()).filter(
903
+ (r) => r.workflowId === workflowId
904
+ );
905
+ }
906
+
907
+ getAllResults(): ResultDisplay[] {
908
+ return Array.from(this.resultDisplays.values());
909
+ }
910
+
911
+ removeResultDisplay(resultId: string): boolean {
912
+ return this.resultDisplays.delete(resultId);
913
+ }
914
+
915
+ clearResults(): void {
916
+ this.resultDisplays.clear();
917
+ this.emit('results:cleared');
918
+ }
919
+
920
+ // ─── Layout & Theme ───────────────────────────────────────────────────────
921
+
922
+ setLayout(layout: string): void {
923
+ this.studioState.layout = layout;
924
+ this.updateComponent('studio-interface', {
925
+ config: { layout },
926
+ });
927
+ this.emit('layout:changed', { layout });
928
+ }
929
+
930
+ setTheme(theme: string): void {
931
+ this.studioState.theme = theme;
932
+ this.updateComponent('studio-interface', {
933
+ config: { theme },
934
+ });
935
+ this.emit('theme:changed', { theme });
936
+ }
937
+
938
+ getLayout(): string {
939
+ return this.studioState.layout;
940
+ }
941
+
942
+ getTheme(): string {
943
+ return this.studioState.theme;
944
+ }
945
+
946
+ // ─── Frame Management ─────────────────────────────────────────────────────
947
+
948
+ activateFrame(frameId: string): void {
949
+ if (!this.studioState.activeFrames.includes(frameId)) {
950
+ this.studioState.activeFrames.push(frameId);
951
+ this.emit('frame:activated', { frameId });
952
+ }
953
+ }
954
+
955
+ deactivateFrame(frameId: string): void {
956
+ const index = this.studioState.activeFrames.indexOf(frameId);
957
+ if (index > -1) {
958
+ this.studioState.activeFrames.splice(index, 1);
959
+ this.emit('frame:deactivated', { frameId });
960
+ }
961
+ }
962
+
963
+ getActiveFrames(): string[] {
964
+ return [...this.studioState.activeFrames];
965
+ }
966
+
967
+ setActiveFrames(frameIds: string[]): void {
968
+ this.studioState.activeFrames = [...frameIds];
969
+ this.emit('frames:updated', { frameIds });
970
+ }
971
+
972
+ // ─── Status & Analytics ───────────────────────────────────────────────────
973
+
974
+ getStatus(): Record<string, unknown> {
975
+ return {
976
+ initialized: this.initialized,
977
+ components: this.components.size,
978
+ visibleComponents: this.getVisibleComponents().length,
979
+ activeWorkflows: this.getActiveWorkflows().length,
980
+ completedWorkflows: this.getCompletedWorkflows().length,
981
+ totalResults: this.resultDisplays.size,
982
+ selectorState: {
983
+ isOpen: this.selectorState.isOpen,
984
+ selectedPersonaId: this.selectorState.selectedPersonaId,
985
+ },
986
+ studioState: {
987
+ layout: this.studioState.layout,
988
+ theme: this.studioState.theme,
989
+ activeFrames: this.studioState.activeFrames.length,
990
+ panels: this.studioState.panels.length,
991
+ toolbars: this.studioState.toolbars.length,
992
+ },
993
+ config: { ...this.config },
994
+ };
995
+ }
996
+
997
+ getUIAnalytics(): Record<string, unknown> {
998
+ const workflows = Array.from(this.workflows.values());
999
+ const completed = workflows.filter((w) => w.status === 'completed');
1000
+ const failed = workflows.filter((w) => w.status === 'failed');
1001
+
1002
+ const totalDuration = completed.reduce((sum, w) => {
1003
+ if (w.endTime) {
1004
+ return sum + (w.endTime.getTime() - w.startTime.getTime());
1005
+ }
1006
+ return sum;
1007
+ }, 0);
1008
+
1009
+ const workflowsByType: Record<string, number> = {};
1010
+ for (const w of workflows) {
1011
+ workflowsByType[w.type] = (workflowsByType[w.type] || 0) + 1;
1012
+ }
1013
+
1014
+ const workflowsByPersona: Record<string, number> = {};
1015
+ for (const w of workflows) {
1016
+ workflowsByPersona[w.personaId] = (workflowsByPersona[w.personaId] || 0) + 1;
1017
+ }
1018
+
1019
+ return {
1020
+ totalWorkflows: workflows.length,
1021
+ completedWorkflows: completed.length,
1022
+ failedWorkflows: failed.length,
1023
+ averageDuration: completed.length > 0 ? totalDuration / completed.length : 0,
1024
+ workflowsByType,
1025
+ workflowsByPersona,
1026
+ totalResults: this.resultDisplays.size,
1027
+ componentCount: this.components.size,
1028
+ };
1029
+ }
1030
+
1031
+ // ─── Cleanup ──────────────────────────────────────────────────────────────
1032
+
1033
+ dispose(): void {
1034
+ this.removeAllListeners();
1035
+ this.components.clear();
1036
+ this.workflows.clear();
1037
+ this.progressStates.clear();
1038
+ this.resultDisplays.clear();
1039
+ this.initialized = false;
1040
+ console.log('[PersonaUISystem] UI system disposed');
1041
+ }
1042
+ }