diff --git a/app/Http/Controllers/Meet/SummaryController.php b/app/Http/Controllers/Meet/SummaryController.php index 6017acf..919dd5e 100644 --- a/app/Http/Controllers/Meet/SummaryController.php +++ b/app/Http/Controllers/Meet/SummaryController.php @@ -12,6 +12,21 @@ class SummaryController extends Controller { use ScopesToAccount; + public function index(Request $request): View + { + $this->authorizeAbility($request, 'meetings.view'); + $owner = $this->ownerRef($request); + $organization = $this->organization($request); + + $summaries = AiSummary::owned($owner) + ->whereHas('session.room', fn ($q) => $q->where('organization_id', $organization->id)) + ->with(['session.room', 'actionItems']) + ->orderByDesc('created_at') + ->paginate(20); + + return view('meet.summaries.index', compact('summaries')); + } + public function show(Request $request, AiSummary $summary): View { $this->authorizeAbility($request, 'meetings.view'); diff --git a/resources/views/meet/summaries/index.blade.php b/resources/views/meet/summaries/index.blade.php new file mode 100644 index 0000000..f9cf12d --- /dev/null +++ b/resources/views/meet/summaries/index.blade.php @@ -0,0 +1,58 @@ + +
+
+

AI Summaries

+

Meeting summaries, decisions, and action items from your sessions.

+
+
+ +
+ @forelse ($summaries as $summary) + @php + $title = $summary->session?->room?->title ?? 'Meeting'; + $status = $summary->status ?? 'ready'; + $statusLabel = match ($status) { + 'ready' => 'Ready', + 'processing' => 'Processing', + 'failed' => 'Failed', + default => ucfirst((string) $status), + }; + $statusClass = match ($status) { + 'ready' => 'bg-emerald-50 text-emerald-700', + 'processing' => 'bg-amber-50 text-amber-800', + 'failed' => 'bg-rose-50 text-rose-700', + default => 'bg-slate-100 text-slate-600', + }; + $preview = \Illuminate\Support\Str::limit(trim((string) $summary->summary), 140); + @endphp +
+
+
+

{{ $title }}

+ + {{ $statusLabel }} + +
+

+ {{ $summary->created_at?->format('M j, Y g:i A') }} + @if ($summary->actionItems->isNotEmpty()) + · {{ $summary->actionItems->count() }} action item{{ $summary->actionItems->count() === 1 ? '' : 's' }} + @endif +

+ @if ($preview !== '') +

{{ $preview }}

+ @endif +
+
+ View +
+
+ @empty +

+ No AI summaries yet. Summaries appear after meetings end when auto-summary is enabled. +

+ @endforelse +
+ +
{{ $summaries->links() }}
+
diff --git a/resources/views/meet/summaries/show.blade.php b/resources/views/meet/summaries/show.blade.php index 546a0bb..83b7465 100644 --- a/resources/views/meet/summaries/show.blade.php +++ b/resources/views/meet/summaries/show.blade.php @@ -1,5 +1,8 @@
+

{{ $summary->session->room->title }}

AI meeting summary

diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 0e2424a..c8cf165 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -26,6 +26,8 @@ 'icon' => ''], ['name' => 'Recordings', 'route' => route('meet.recordings.index'), 'active' => request()->routeIs('meet.recordings.*'), 'icon' => ''], + ['name' => 'Summaries', 'route' => route('meet.summaries.index'), 'active' => request()->routeIs('meet.summaries.*'), + 'icon' => ''], ['name' => 'Reports', 'route' => route('meet.reports.index'), 'active' => request()->routeIs('meet.reports.*'), 'icon' => ''], ['name' => 'Channels', 'route' => route('meet.channels.index'), 'active' => request()->routeIs('meet.channels.*'), diff --git a/routes/web.php b/routes/web.php index a584816..69a2516 100644 --- a/routes/web.php +++ b/routes/web.php @@ -213,6 +213,7 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/recordings/{recording}/download', [RecordingController::class, 'download'])->name('meet.recordings.download'); Route::delete('/recordings/{recording}', [RecordingController::class, 'destroy'])->name('meet.recordings.destroy'); + Route::get('/summaries', [SummaryController::class, 'index'])->name('meet.summaries.index'); Route::get('/summaries/{summary}', [SummaryController::class, 'show'])->name('meet.summaries.show'); Route::get('/settings/security', [SettingsController::class, 'security'])->name('meet.settings.security'); diff --git a/tests/Feature/MeetWebTest.php b/tests/Feature/MeetWebTest.php index c560a5a..5d7a040 100644 --- a/tests/Feature/MeetWebTest.php +++ b/tests/Feature/MeetWebTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature; use App\Http\Middleware\EnsurePlatformSession; use App\Models\Member; use App\Models\Organization; +use App\Models\AiSummary; use App\Models\Recording; use App\Models\Room; use App\Models\Session; @@ -423,6 +424,38 @@ class MeetWebTest extends TestCase ->assertSee('Recordings'); } + public function test_user_can_view_summaries_index(): void + { + $room = $this->createRoom(); + $session = Session::create([ + 'owner_ref' => $this->user->public_id, + 'room_id' => $room->id, + 'media_room_name' => 'room-'.$room->uuid, + 'status' => 'ended', + 'started_at' => now()->subHour(), + 'ended_at' => now(), + ]); + AiSummary::create([ + 'owner_ref' => $this->user->public_id, + 'session_id' => $session->id, + 'status' => 'ready', + 'summary' => 'We agreed to ship the dashboard on Friday.', + 'decisions' => ['Ship Friday'], + ]); + + $this->actingAs($this->user) + ->get('/summaries') + ->assertOk() + ->assertSee('AI Summaries') + ->assertSee($room->title) + ->assertSee('We agreed to ship the dashboard on Friday.'); + + $this->actingAs($this->user) + ->get('/summaries/') + ->assertOk() + ->assertSee('AI Summaries'); + } + public function test_admin_can_update_security_policy(): void { $this->actingAs($this->user)