mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
track meeting recoring and show active recording in the sidebar row
This commit is contained in:
parent
5eea5907e7
commit
1a3e5a4e2e
2 changed files with 84 additions and 28 deletions
|
|
@ -4567,12 +4567,14 @@ function App() {
|
||||||
const pendingCalendarEventRef = useRef<CalendarEventMeta | undefined>(undefined)
|
const pendingCalendarEventRef = useRef<CalendarEventMeta | undefined>(undefined)
|
||||||
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
|
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
|
||||||
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
|
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
|
||||||
|
const [recordingMeetingSource, setRecordingMeetingSource] = useState<string | null>(null)
|
||||||
|
|
||||||
const [checkingPermission, setCheckingPermission] = useState(false)
|
const [checkingPermission, setCheckingPermission] = useState(false)
|
||||||
|
|
||||||
const startMeetingNow = useCallback(async () => {
|
const startMeetingNow = useCallback(async () => {
|
||||||
const calEvent = pendingCalendarEventRef.current
|
const calEvent = pendingCalendarEventRef.current
|
||||||
pendingCalendarEventRef.current = undefined
|
pendingCalendarEventRef.current = undefined
|
||||||
|
setRecordingMeetingSource(calEvent?.source ?? null)
|
||||||
const notePath = await meetingTranscription.start(calEvent)
|
const notePath = await meetingTranscription.start(calEvent)
|
||||||
if (notePath) {
|
if (notePath) {
|
||||||
meetingNotePathRef.current = notePath
|
meetingNotePathRef.current = notePath
|
||||||
|
|
@ -4600,6 +4602,7 @@ function App() {
|
||||||
const handleToggleMeeting = useCallback(async () => {
|
const handleToggleMeeting = useCallback(async () => {
|
||||||
if (meetingTranscription.state === 'recording') {
|
if (meetingTranscription.state === 'recording') {
|
||||||
await meetingTranscription.stop()
|
await meetingTranscription.stop()
|
||||||
|
setRecordingMeetingSource(null)
|
||||||
|
|
||||||
// Read the final transcript and generate meeting notes via LLM
|
// Read the final transcript and generate meeting notes via LLM
|
||||||
const notePath = meetingNotePathRef.current
|
const notePath = meetingNotePathRef.current
|
||||||
|
|
@ -5119,6 +5122,9 @@ function App() {
|
||||||
isSuggestedTopicsOpen={isSuggestedTopicsOpen}
|
isSuggestedTopicsOpen={isSuggestedTopicsOpen}
|
||||||
onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })}
|
onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })}
|
||||||
onOpenMeetings={openMeetingsView}
|
onOpenMeetings={openMeetingsView}
|
||||||
|
meetingRecordingState={meetingTranscription.state}
|
||||||
|
recordingMeetingSource={recordingMeetingSource}
|
||||||
|
onToggleMeetingRecording={() => { void handleToggleMeeting() }}
|
||||||
onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
||||||
isEmailOpen={isEmailOpen}
|
isEmailOpen={isEmailOpen}
|
||||||
onOpenEmail={openEmailView}
|
onOpenEmail={openEmailView}
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,9 @@ type SidebarContentPanelProps = {
|
||||||
isSuggestedTopicsOpen?: boolean
|
isSuggestedTopicsOpen?: boolean
|
||||||
onOpenSuggestedTopics?: () => void
|
onOpenSuggestedTopics?: () => void
|
||||||
onOpenMeetings?: () => void
|
onOpenMeetings?: () => void
|
||||||
|
meetingRecordingState?: 'idle' | 'connecting' | 'recording' | 'stopping'
|
||||||
|
recordingMeetingSource?: string | null
|
||||||
|
onToggleMeetingRecording?: () => void
|
||||||
onOpenBgTasks?: () => void
|
onOpenBgTasks?: () => void
|
||||||
isEmailOpen?: boolean
|
isEmailOpen?: boolean
|
||||||
onOpenEmail?: () => void
|
onOpenEmail?: () => void
|
||||||
|
|
@ -443,6 +446,9 @@ export function SidebarContentPanel({
|
||||||
isSuggestedTopicsOpen = false,
|
isSuggestedTopicsOpen = false,
|
||||||
onOpenSuggestedTopics,
|
onOpenSuggestedTopics,
|
||||||
onOpenMeetings,
|
onOpenMeetings,
|
||||||
|
meetingRecordingState,
|
||||||
|
recordingMeetingSource,
|
||||||
|
onToggleMeetingRecording,
|
||||||
onOpenBgTasks,
|
onOpenBgTasks,
|
||||||
isEmailOpen = false,
|
isEmailOpen = false,
|
||||||
onOpenEmail,
|
onOpenEmail,
|
||||||
|
|
@ -542,7 +548,12 @@ export function SidebarContentPanel({
|
||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<MeetingsSidebarSection onOpenMeetingsView={onOpenMeetings} />
|
<MeetingsSidebarSection
|
||||||
|
onOpenMeetingsView={onOpenMeetings}
|
||||||
|
recordingState={meetingRecordingState ?? 'idle'}
|
||||||
|
recordingSource={recordingMeetingSource ?? null}
|
||||||
|
onToggleRecording={onToggleMeetingRecording}
|
||||||
|
/>
|
||||||
<TasksSidebarSection
|
<TasksSidebarSection
|
||||||
tasks={bgTaskSummaries}
|
tasks={bgTaskSummaries}
|
||||||
onOpenTask={onOpenBgTask}
|
onOpenTask={onOpenBgTask}
|
||||||
|
|
@ -1183,8 +1194,14 @@ function formatMeetingTime(event: UpcomingMeeting): string {
|
||||||
|
|
||||||
function MeetingsSidebarSection({
|
function MeetingsSidebarSection({
|
||||||
onOpenMeetingsView,
|
onOpenMeetingsView,
|
||||||
|
recordingState,
|
||||||
|
recordingSource,
|
||||||
|
onToggleRecording,
|
||||||
}: {
|
}: {
|
||||||
onOpenMeetingsView?: () => void
|
onOpenMeetingsView?: () => void
|
||||||
|
recordingState: 'idle' | 'connecting' | 'recording' | 'stopping'
|
||||||
|
recordingSource: string | null
|
||||||
|
onToggleRecording?: () => void
|
||||||
}) {
|
}) {
|
||||||
const [meetings, setMeetings] = useState<UpcomingMeeting[]>([])
|
const [meetings, setMeetings] = useState<UpcomingMeeting[]>([])
|
||||||
|
|
||||||
|
|
@ -1268,53 +1285,86 @@ function MeetingsSidebarSection({
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{meetings.map((m) => {
|
{meetings.map((m) => {
|
||||||
const hasConference = Boolean(m.conferenceLink)
|
const hasConference = Boolean(m.conferenceLink)
|
||||||
|
const isThisRecording = recordingSource === m.source && (recordingState === 'recording' || recordingState === 'connecting' || recordingState === 'stopping')
|
||||||
|
const isBusy = isThisRecording && (recordingState === 'connecting' || recordingState === 'stopping')
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem key={m.id}>
|
<SidebarMenuItem key={m.id}>
|
||||||
<SidebarMenuButton onClick={onOpenMeetingsView} className="gap-2">
|
<SidebarMenuButton onClick={onOpenMeetingsView} className="gap-2">
|
||||||
<Mic className="size-4 shrink-0" />
|
<Mic className="size-4 shrink-0" />
|
||||||
<span className="min-w-0 flex-1 truncate text-sm">{m.summary}</span>
|
<span className="min-w-0 flex-1 truncate text-sm">{m.summary}</span>
|
||||||
<span className="shrink-0 text-[10px] text-muted-foreground group-hover/menu-item:hidden">
|
<span
|
||||||
{formatMeetingTime(m)}
|
className={`shrink-0 text-[10px] text-muted-foreground ${isThisRecording ? '' : 'group-hover/menu-item:hidden'}`}
|
||||||
|
>
|
||||||
|
{isThisRecording ? null : formatMeetingTime(m)}
|
||||||
</span>
|
</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
<div className="absolute top-1.5 right-1 flex items-center gap-0.5 opacity-0 transition-opacity group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100">
|
{isThisRecording ? (
|
||||||
<Tooltip>
|
<div className="absolute top-1.5 right-1 flex items-center gap-1.5">
|
||||||
<TooltipTrigger asChild>
|
<span className="relative flex size-2">
|
||||||
<button
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75" />
|
||||||
type="button"
|
<span className="relative inline-flex size-2 rounded-full bg-red-500" />
|
||||||
aria-label="Take notes"
|
</span>
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
triggerMeetingCapture(m, false)
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
|
||||||
className="flex aspect-square w-5 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
||||||
>
|
|
||||||
<Mic className="size-4" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">Take notes</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
{hasConference && (
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Join & take notes"
|
aria-label="Stop recording"
|
||||||
|
disabled={isBusy}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
triggerMeetingCapture(m, true)
|
onToggleRecording?.()
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="flex aspect-square w-5 items-center justify-center rounded-md text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isBusy ? <LoaderIcon className="size-4 animate-spin" /> : <Square className="size-3.5 fill-current" />}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
{recordingState === 'connecting' ? 'Starting…' : recordingState === 'stopping' ? 'Stopping…' : 'Stop recording'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="absolute top-1.5 right-1 flex items-center gap-0.5 opacity-0 transition-opacity group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Take notes"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
triggerMeetingCapture(m, false)
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
className="flex aspect-square w-5 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
className="flex aspect-square w-5 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Video className="size-4" />
|
<Mic className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">Join & take notes</TooltipContent>
|
<TooltipContent side="bottom">Take notes</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
{hasConference && (
|
||||||
</div>
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Join & take notes"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
triggerMeetingCapture(m, true)
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="flex aspect-square w-5 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
<Video className="size-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">Join & take notes</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue