diff --git a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.html b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.html index acfaab1d..4114d5e8 100644 --- a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.html +++ b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.html @@ -37,21 +37,11 @@

-
-

Step 3: Platform & Features - To be implemented

-
+
- -
-

Step 4: Participants - To be implemented

-
-
-
- -

Step 5: Resources & Summary - To be implemented

diff --git a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.ts b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.ts index 1a04edaf..3145cfe7 100644 --- a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.ts +++ b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-create/meeting-create.component.ts @@ -16,12 +16,21 @@ import { MessageService } from 'primeng/api'; import { StepperModule } from 'primeng/stepper'; import { MeetingDetailsComponent } from '../meeting-details/meeting-details.component'; +import { MeetingPlatformFeaturesComponent } from '../meeting-platform-features/meeting-platform-features.component'; import { MeetingTypeSelectionComponent } from '../meeting-type-selection/meeting-type-selection.component'; @Component({ selector: 'lfx-meeting-create', standalone: true, - imports: [CommonModule, StepperModule, ButtonComponent, ReactiveFormsModule, MeetingTypeSelectionComponent, MeetingDetailsComponent], + imports: [ + CommonModule, + StepperModule, + ButtonComponent, + ReactiveFormsModule, + MeetingTypeSelectionComponent, + MeetingDetailsComponent, + MeetingPlatformFeaturesComponent, + ], templateUrl: './meeting-create.component.html', }) export class MeetingCreateComponent { @@ -33,7 +42,7 @@ export class MeetingCreateComponent { // Stepper state public currentStep = signal(0); - public readonly totalSteps = 5; + public readonly totalSteps = 4; // Form state public form = signal(this.createMeetingFormGroup()); @@ -78,6 +87,11 @@ export class MeetingCreateComponent { public nextStep(): void { const next = this.currentStep() + 1; if (next < this.totalSteps && this.canNavigateToStep(next)) { + // Auto-generate title when moving from step 1 to step 2 + if (this.currentStep() === 0 && next === 1) { + this.generateMeetingTitle(); + } + this.currentStep.set(next); this.scrollToStepper(); } @@ -218,10 +232,7 @@ export class MeetingCreateComponent { case 2: // Platform & Features return !!form.get('meetingTool')?.value; - case 3: // Participants (optional but should not have validation errors) - return true; - - case 4: // Resources & Summary (optional) + case 3: // Resources & Summary (optional) return true; default: @@ -253,7 +264,7 @@ export class MeetingCreateComponent { recurrence: new FormControl('none'), // Step 3: Platform & Features - meetingTool: new FormControl('', [Validators.required]), + meetingTool: new FormControl('zoom', [Validators.required]), recording_enabled: new FormControl(false), transcripts_enabled: new FormControl(false), youtube_enabled: new FormControl(false), @@ -459,4 +470,29 @@ export class MeetingCreateComponent { }); } } + + private generateMeetingTitle(): void { + const form = this.form(); + const meetingType = form.get('meeting_type')?.value; + const startDate = form.get('startDate')?.value; + + // Only auto-generate if we have meeting type and the title is empty + const currentTitle = form.get('topic')?.value; + if (meetingType && (!currentTitle || currentTitle.trim() === '')) { + const formattedDate = startDate + ? new Date(startDate).toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }) + : new Date().toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }); + + const generatedTitle = `${meetingType} Meeting - ${formattedDate}`; + form.get('topic')?.setValue(generatedTitle); + } + } } diff --git a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.html b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.html index be4f30db..cf0c62d0 100644 --- a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.html +++ b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.html @@ -89,7 +89,8 @@

AI Agenda Generator

id="meeting-agenda" placeholder="Enter meeting agenda and key discussion points" [rows]="6" - styleClass="w-full max-h-40 overflow-y-auto" + [autoResize]="true" + styleClass="w-full" data-testid="meeting-details-agenda-textarea">

A clear agenda helps participants prepare and keeps discussions focused

@if (form().get('agenda')?.errors?.['required'] && form().get('agenda')?.touched) { diff --git a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.ts b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.ts index dc5c34d8..c0b208d5 100644 --- a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.ts +++ b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-details/meeting-details.component.ts @@ -107,14 +107,6 @@ export class MeetingDetailsComponent implements OnInit { this.generateRecurrenceOptions(value as Date); }); - // Auto-generate title when meeting type and date are available - this.form() - .get('startDate') - ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.generateMeetingTitleIfNeeded(); - }); - // Watch for isRecurring changes to reset recurrence this.form() .get('isRecurring') @@ -201,20 +193,6 @@ export class MeetingDetailsComponent implements OnInit { return { weekOfMonth, isLastWeek }; } - // Auto-title generation - private generateMeetingTitleIfNeeded(): void { - const meetingType = this.form().get('meeting_type')?.value; - const startDate = this.form().get('startDate')?.value; - const currentTitle = this.form().get('topic')?.value; - - // Only auto-generate if we have both type and date, and the current title is empty or was auto-generated - if (meetingType && startDate && (!currentTitle || this.titleWasAutoGenerated())) { - const newTitle = this.generateMeetingTitle(meetingType, startDate); - this.form().get('topic')?.setValue(newTitle); - this.titleWasAutoGenerated.set(true); - } - } - private generateMeetingTitle(meetingType: string, date: Date): string { const formattedDate = new Date(date).toLocaleDateString('en-US', { month: '2-digit', diff --git a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-platform-features/meeting-platform-features.component.html b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-platform-features/meeting-platform-features.component.html new file mode 100644 index 00000000..8c5e5e1b --- /dev/null +++ b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-platform-features/meeting-platform-features.component.html @@ -0,0 +1,141 @@ + + + +
+ +
+

Meeting Platform & Features

+

Choose your meeting platform and enable helpful features.

+
+ + +
+

Meeting Platform *

+ +
+ @for (platform of platformOptions; track platform.value) { +
+ @if (!platform.available) { +
+ Coming Soon +
+ } + +
+
+ +
+
+

{{ platform.label }}

+

{{ platform.description }}

+
+
+
+ } +
+ + @if (form().get('meetingTool')?.errors?.['required'] && form().get('meetingTool')?.touched) { +

Please select a meeting platform

+ } +
+ + +
+
+
+ +
+
+

Meeting Features

+

+ Open source thrives on transparency and accessibility. Recording meetings, generating transcripts, and enabling features helps include community + members who can't attend live and creates valuable documentation. +

+
+
+
+ + +
+

Meeting Features

+ + @for (feature of features; track feature.key) { +
+
+
+
+ +
+
+
+ + @if (feature.recommended) { + Recommended + } +
+

{{ feature.description }}

+
+
+ +
+
+ } +
+ + + @if (form().get('recording_enabled')?.value) { +
+

Recording Options

+ + +
+ + +

Who can view the meeting recording

+
+ + + @if (form().get('zoom_ai_enabled')?.value) { +
+ + +

Who can access the AI-generated meeting summary

+ + +
+
+ +

AI summary must be reviewed before sharing

+
+ +
+
+ } +
+ } +
diff --git a/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-platform-features/meeting-platform-features.component.ts b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-platform-features/meeting-platform-features.component.ts new file mode 100644 index 00000000..7d3df46d --- /dev/null +++ b/apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-platform-features/meeting-platform-features.component.ts @@ -0,0 +1,38 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, input } from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { SelectComponent } from '@components/select/select.component'; +import { ToggleComponent } from '@components/toggle/toggle.component'; +import { AI_SUMMARY_ACCESS_OPTIONS, MEETING_FEATURES, MEETING_PLATFORMS, RECORDING_ACCESS_OPTIONS } from '@lfx-pcc/shared/constants'; +import { TooltipModule } from 'primeng/tooltip'; + +@Component({ + selector: 'lfx-meeting-platform-features', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, SelectComponent, ToggleComponent, TooltipModule], + templateUrl: './meeting-platform-features.component.html', +}) +export class MeetingPlatformFeaturesComponent { + // Form group input from parent + public readonly form = input.required(); + + // Constants from shared package + public readonly platformOptions = MEETING_PLATFORMS; + public readonly features = MEETING_FEATURES; + public readonly recordingAccessOptions = RECORDING_ACCESS_OPTIONS; + public readonly aiSummaryAccessOptions = AI_SUMMARY_ACCESS_OPTIONS; + + public selectPlatform(platform: string): void { + const platformOption = this.platformOptions.find((p) => p.value === platform); + if (platformOption?.available) { + this.form().get('meetingTool')?.setValue(platform); + } + } + + public toggleFeature(featureKey: string, enabled: boolean): void { + this.form().get(featureKey)?.setValue(enabled); + } +} diff --git a/packages/shared/src/constants/index.ts b/packages/shared/src/constants/index.ts index e24f11df..7804a95c 100644 --- a/packages/shared/src/constants/index.ts +++ b/packages/shared/src/constants/index.ts @@ -6,5 +6,6 @@ export * from './colors'; export * from './committees'; export * from './file-upload'; export * from './font-sizes'; +export * from './meeting'; export * from './server'; export * from './timezones'; diff --git a/packages/shared/src/constants/meeting.ts b/packages/shared/src/constants/meeting.ts new file mode 100644 index 00000000..e0129a93 --- /dev/null +++ b/packages/shared/src/constants/meeting.ts @@ -0,0 +1,84 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +export const MEETING_PLATFORMS = [ + { + value: 'zoom', + label: 'Zoom', + description: 'Video conferencing with recording and chat features', + available: true, + icon: 'fa-light fa-video', + color: '#0094FF', + }, + { + value: 'teams', + label: 'Microsoft Teams', + description: 'Integrated collaboration with Office 365', + available: false, + icon: 'fa-light fa-desktop', + color: '#6b7280', + }, + { + value: 'in-person', + label: 'In-Person', + description: 'Physical meeting location', + available: false, + icon: 'fa-light fa-location-dot', + color: '#6b7280', + }, +]; + +export const MEETING_FEATURES = [ + { + key: 'recording_enabled', + icon: 'fa-light fa-video', + title: 'Enable Recording', + description: 'Record the meeting for those who cannot attend live', + recommended: true, + color: '#3b82f6', // blue - matches bg-blue-50 text-blue-700 + }, + { + key: 'transcripts_enabled', + icon: 'fa-light fa-file-lines', + title: 'Generate Transcripts', + description: 'Automatically create searchable text transcripts', + recommended: false, + color: '#8b5cf6', // purple - matches bg-purple-50 text-purple-700 + }, + { + key: 'youtube_enabled', + icon: 'fa-light fa-upload', + title: 'YouTube Auto-upload', + description: "Automatically publish recordings to your project's YouTube channel", + recommended: false, + color: '#dc2626', // red - matches bg-red-50 text-red-700 + }, + { + key: 'zoom_ai_enabled', + icon: 'fa-light fa-microchip-ai', + title: 'AI Meeting Summary', + description: 'Generate key takeaways and action items automatically', + recommended: true, + color: '#16a34a', // green - matches bg-green-50 text-green-700 + }, + { + key: 'show_in_public_calendar', + icon: 'fa-light fa-calendar-check', + title: 'Show in Public Calendar', + description: 'Make this meeting visible in the public project calendar', + recommended: true, + color: '#ea580c', // orange - unique color for calendar visibility + }, +]; + +export const RECORDING_ACCESS_OPTIONS = [ + { label: 'Members Only', value: 'Members' }, + { label: 'Public', value: 'Public' }, + { label: 'Private', value: 'Private' }, +]; + +export const AI_SUMMARY_ACCESS_OPTIONS = [ + { label: 'PCC Members', value: 'PCC' }, + { label: 'Project Members', value: 'Members' }, + { label: 'Public', value: 'Public' }, +];