diff --git a/app/app-services.ts b/app/app-services.ts index 368f2c7ff359..651cdd621ad3 100644 --- a/app/app-services.ts +++ b/app/app-services.ts @@ -62,6 +62,7 @@ export { VideoSettingsService } from 'services/settings-v2/video'; export { SettingsManagerService } from 'services/settings-manager'; export { MarkersService } from 'services/markers'; export { RealmService } from 'services/realm'; +export { RealtimeHighlighterService } from 'services/highlighter/realtime-highlighter-service'; import { VirtualWebcamService } from 'services/virtual-webcam'; // ONLINE SERVICES @@ -164,6 +165,7 @@ import { PatchNotesService } from './services/patch-notes'; import { VideoService } from './services/video'; import { ChatService } from './services/chat'; import { HighlighterService } from './services/highlighter'; +import { RealtimeHighlighterService } from './services/highlighter/realtime-highlighter-service'; import { GrowService } from './services/grow/grow'; import { TransitionsService } from './services/transitions'; import { TcpServerService } from './services/api/tcp-server'; @@ -293,4 +295,5 @@ export const AppServices = { RemoteControlService, UrlService, VirtualWebcamService, + RealtimeHighlighterService, }; diff --git a/app/components-react/highlighter/ClipPreviewInfo.tsx b/app/components-react/highlighter/ClipPreviewInfo.tsx index a32cf67a4dfe..5cba8945523a 100644 --- a/app/components-react/highlighter/ClipPreviewInfo.tsx +++ b/app/components-react/highlighter/ClipPreviewInfo.tsx @@ -1,4 +1,4 @@ -import { EGame } from 'services/highlighter/models/ai-highlighter.models'; +import { EGame, IAiClipInfo } from 'services/highlighter/models/ai-highlighter.models'; import { IAiClip } from 'services/highlighter/models/highlighter.models'; import { getConfigByGame, getEventConfig } from 'services/highlighter/models/game-config.models'; import styles from './ClipPreview.m.less'; @@ -15,33 +15,7 @@ export default function ClipPreviewInfo({ return No event data; } - const uniqueInputTypes = new Set(); - if (clip.aiInfo.inputs && Array.isArray(clip.aiInfo.inputs)) { - clip.aiInfo.inputs.forEach(input => { - if (input.type) { - uniqueInputTypes.add(input.type); - } - }); - } - - const eventDisplays = Array.from(uniqueInputTypes).map(type => { - const eventInfo = getEventConfig(game, type); - - if (eventInfo) { - return { - emoji: eventInfo.emoji, - description: eventInfo.description.singular, - type, - }; - } - - return { - emoji: '⚡', - description: type, - type, - }; - }); - + const eventDisplays = getUniqueEmojiConfigFromAiInfo(clip.aiInfo); return (
{ return {event.emoji}; })} - {clip.aiInfo.metadata?.round && ( -
{`Round: ${clip.aiInfo.metadata.round}`}
- )}{' '} + {clip.aiInfo.metadata?.round !== undefined && + clip.aiInfo.metadata?.round !== null && + clip.aiInfo.metadata?.round !== 0 && ( +
{`Round: ${clip.aiInfo.metadata.round}`}
+ )}
); } + +export interface EmojiConfig { + emoji: string; + count: number; + description: string; + type: string; +} + +export function getUniqueEmojiConfigFromAiInfo(aiInfos: IAiClipInfo): EmojiConfig[] { + const typeCounts: Record = {}; + if (aiInfos.inputs && Array.isArray(aiInfos.inputs)) { + aiInfos.inputs.forEach(aiInput => { + if (aiInput.type) { + typeCounts[aiInput.type] = (typeCounts[aiInput.type] || 0) + 1; + } + }); + } + + const uniqueInputTypes = Object.keys(typeCounts); + + const eventDisplays = uniqueInputTypes.map(type => { + const count = typeCounts[type]; + if (aiInfos.metadata?.game) { + const eventInfo = getEventConfig(aiInfos.metadata.game, type); + if (eventInfo) { + return { + emoji: eventInfo.emoji, + description: count > 1 ? eventInfo.description.plural : eventInfo.description.singular, + count, + type, + }; + } + } + return { + emoji: '⚡', + description: type, + count, + type, + }; + }); + return eventDisplays; +} diff --git a/app/components-react/highlighter/RealtimeIndicator.m.less b/app/components-react/highlighter/RealtimeIndicator.m.less new file mode 100644 index 000000000000..34d02e6b3cf7 --- /dev/null +++ b/app/components-react/highlighter/RealtimeIndicator.m.less @@ -0,0 +1,99 @@ +.pulseWrapper { + width: 16px; + height: 16px; + margin-right: 4px; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.pulse { + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background: #f85640; + opacity: 0.3; + top: 0; + left: 0; + animation: pulse 2.5s infinite; +} + +.dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #f85640; + position: relative; + z-index: 1; +} + +@keyframes pulse { + 0% { + transform: scale(0.5); + opacity: 0.5; + } + 50% { + transform: scale(1.4); + opacity: 0; + } + 99% { + transform: scale(1.4); + opacity: 0; + } + 100% { + transform: scale(0.5); + opacity: 0.5; + } +} +.realtime-detection-action { + font-weight: 500; + letter-spacing: 0.2px; + position: relative; + justify-content: space-between; + display: flex; + padding: 4px 8px; + padding-right: 10px; + width: 236px; + align-items: center; + gap: 8px; + border-radius: 48px; + background: #301e24; + color: white; + overflow: hidden; +} + +.realtime-detection-streamcard { + background-color: var(--button); + color: var(--paragraph); + font-weight: 400; + font-size: 16px; + border-radius: 8px; + height: 40px; + width: 100%; +} + +.activity-animation { + transform: translateX(200%); +} + +.background-pulse { + left: -100%; + position: absolute; + width: 200px; + height: 40px; + background: linear-gradient(90deg, #ffffff00, #5d1e1e, #ffffff00); + transition: transform 0.5s ease-in-out; +} + +.emoji { + z-index: 999; + margin-right: 4px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} diff --git a/app/components-react/highlighter/RealtimeIndicator.tsx b/app/components-react/highlighter/RealtimeIndicator.tsx new file mode 100644 index 000000000000..804c408a5537 --- /dev/null +++ b/app/components-react/highlighter/RealtimeIndicator.tsx @@ -0,0 +1,85 @@ +import { Button } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { $t } from 'services/i18n'; +import styles from './RealtimeIndicator.m.less'; +import cx from 'classnames'; +import { EGame } from 'services/highlighter/models/ai-highlighter.models'; +import { GAME_CONFIGS } from 'services/highlighter/models/game-config.models'; +import { TRealtimeFeedEvent } from './realtime-highlights/RealtimeHighlightsFeed'; + +export default function HighlightGenerator({ + eventType, + emitCancel, + location, +}: { + eventType?: TRealtimeFeedEvent; + emitCancel: () => void; + location: 'streamCard' | 'statusBar'; +}) { + const [animateOnce, setAnimateOnce] = useState(false); + const [emoji, setEmoji] = useState(''); + const [description, setDescription] = useState(''); + + // Run animation once when emoji prop changes + useEffect(() => { + if (eventType) { + setEmoji(getEmojiByEventType(eventType)); + setDescription(firstLetterUppercase(getDescriptionByEventType(eventType).singular)); + setAnimateOnce(true); + const timeout = setTimeout(() => setAnimateOnce(false), 2000); + return () => clearTimeout(timeout); + } + }, [eventType]); + + function getEmojiByEventType(eventType: { type: string; game: EGame }) { + return GAME_CONFIGS[eventType.game].inputTypeMap[eventType.type]?.emoji || '🤖'; + } + + function getDescriptionByEventType(eventType: { type: string; game: EGame }) { + return GAME_CONFIGS[eventType.game].inputTypeMap[eventType.type]?.description || 'Highlight'; + } + + function firstLetterUppercase(value: string): string { + return String(value).charAt(0).toUpperCase() + String(value).slice(1); + } + return ( +
+
+ {location === 'statusBar' && ( +
setAnimateOnce(false)} + /> + )} + {animateOnce ? ( +
{emoji}
+ ) : ( +
+
+
+
+ )} +

+ {' '} + {animateOnce ? `${description} detected` : $t('AI detection in progress')} +

+
+ +
+ ); +} diff --git a/app/components-react/highlighter/StreamCard.tsx b/app/components-react/highlighter/StreamCard.tsx index 54b5431b636d..464f3d991763 100644 --- a/app/components-react/highlighter/StreamCard.tsx +++ b/app/components-react/highlighter/StreamCard.tsx @@ -16,6 +16,7 @@ import * as remote from '@electron/remote'; import StreamCardInfo from './StreamCardInfo'; import StreamCardModal, { TModalStreamCard } from './StreamCardModal'; import { supportedGames } from 'services/highlighter/models/game-config.models'; +import RealtimeIndicator from './RealtimeIndicator'; export default function StreamCard({ streamId, @@ -230,7 +231,8 @@ export default function StreamCard({

- {stream.state.type === EAiDetectionState.FINISHED ? ( + {stream.state.type === EAiDetectionState.FINISHED || + stream.state.type === EAiDetectionState.REALTIME_DETECTION_IN_PROGRESS ? ( ) : (
@@ -239,12 +241,19 @@ export default function StreamCard({ { + cancelHighlightGeneration(); + }} emitExportVideo={() => exportVideo(streamId)} emitShowStreamClips={showStreamClips} clipsOfStreamAreLoading={clipsOfStreamAreLoading} emitRestartAiDetection={() => { - HighlighterService.actions.restartAiDetection(stream.path, stream); + if (stream.path) { + HighlighterService.actions.restartAiDetection(stream.path, stream); + } else { + // Can't be restarted because it was a realtime highlight detection + // This scenario shouldn't happen + } }} emitSetView={emitSetView} emitFeedbackForm={() => { @@ -313,6 +322,18 @@ function ActionBar({ emitFeedbackForm(clips.length); }; + if (stream?.state.type === EAiDetectionState.REALTIME_DETECTION_IN_PROGRESS) { + return ( + { + emitCancelHighlightGeneration(); + }} + /> + ); + } + // In Progress if (stream?.state.type === EAiDetectionState.IN_PROGRESS) { return ( diff --git a/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsFeed.m.less b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsFeed.m.less new file mode 100644 index 000000000000..8bf5aa482763 --- /dev/null +++ b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsFeed.m.less @@ -0,0 +1,117 @@ +.event-tooltip { + :global(.ant-tooltip-content) { + max-width: 320px; + } + + :global(.ant-tooltip-inner) { + padding: 12px; + background: #1f1f23; + border: 1px solid #3c3c41; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + } +} + +.event-tooltip-content { + min-width: 200px; + max-width: 300px; +} + +.event-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.event-item { + padding: 10px; + background: #2d2d30; + border-radius: 6px; + border: 1px solid #404040; + transition: all 0.2s ease; + + &:hover { + background: #353538; + border-color: #4a9eff; + box-shadow: 0 2px 8px rgba(74, 158, 255, 0.1); + } +} + +.event-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.event-title { + font-weight: 600; + font-size: 12px; + color: #e4e4e7; +} + +.event-timestamp { + font-size: 10px; + color: #a1a1aa; + opacity: 0.8; +} + +.event-description { + font-size: 11px; + color: #d4d4d8; + margin-bottom: 8px; + line-height: 1.4; +} + +.event-button { + padding: 0; + height: auto; + font-size: 10px; + color: #4a9eff; + transition: color 0.2s ease; + + &:hover { + color: #60a5fa; + text-decoration: underline; + } + + &:focus { + color: #60a5fa; + } +} + +.event-footer { + margin-top: 12px; + padding-top: 10px; + border-top: 1px solid #404040; + display: flex; + justify-content: space-between; + align-items: center; +} + +.more-events-text { + font-size: 10px; + color: #a1a1aa; + opacity: 0.9; +} + +.view-all-button { + height: 26px; + font-size: 10px; + padding: 0 10px; + background: #4a9eff; + border-color: #4a9eff; + border-radius: 4px; + transition: all 0.2s ease; + + &:hover { + background: #60a5fa; + border-color: #60a5fa; + box-shadow: 0 2px 6px rgba(74, 158, 255, 0.3); + } + + &:focus { + background: #60a5fa; + border-color: #60a5fa; + } +} diff --git a/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsFeed.tsx b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsFeed.tsx new file mode 100644 index 000000000000..c326181ffac5 --- /dev/null +++ b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsFeed.tsx @@ -0,0 +1,205 @@ +import React, { useEffect, useState } from 'react'; +import { Tooltip, Button } from 'antd'; +import { TooltipPlacement } from 'antd/lib/tooltip'; +import styles from './RealtimeHighlightsFeed.m.less'; +import { EGame, IHighlight } from 'services/highlighter/models/ai-highlighter.models'; +import { HighlighterService } from 'app-services'; +import { Services } from '../../service-provider'; +import { useVuex } from '../../hooks'; +import { + EHighlighterView, + INewClipData, + TClip, +} from 'services/highlighter/models/highlighter.models'; +import { Subscription } from 'rxjs'; +import RealtimeHighlightsItem from './RealtimeHighlightsItem'; +import { useRealmObject } from '../../hooks/realm'; +import { EMenuItemKey } from 'services/side-nav'; +import Utils from 'services/utils'; +import RealtimeIndicator from '../RealtimeIndicator'; +import { IRealtimeHighlightClipData } from 'services/highlighter/realtime-highlighter-service'; +import { $t } from 'services/i18n/i18n'; + +interface IRealtimeHighlightTooltipProps { + placement?: TooltipPlacement; + trigger?: + | 'hover' + | 'click' + | 'focus' + | 'contextMenu' + | Array<'hover' | 'click' | 'focus' | 'contextMenu'>; + maxEvents?: number; +} + +export type TRealtimeFeedEvent = { + type: string; + game: EGame; +}; + +export default function RealtimeHighlightsTooltip(props: IRealtimeHighlightTooltipProps) { + const { placement, trigger, maxEvents = 3 } = props; + const { HighlighterService, RealtimeHighlighterService, NavigationService } = Services; + const [lastEvent, setLastEvent] = useState(null); + const [showTooltip, setShowTooltip] = useState(undefined); + + const [highlightClips, setHighlightClips] = useState([]); + const [hasMoreEvents, setHasMoreEvents] = useState(false); + const isDevMode = Utils.isDevMode(); + + const currentGame = useRealmObject(RealtimeHighlighterService.ephemeralState).game; + let realtimeHighlightSubscription: Subscription | null = null; + let realtimeEventSubscription: Subscription | null = null; + useEffect(() => { + realtimeHighlightSubscription = RealtimeHighlighterService.highlightsReady.subscribe( + realtimeClipData => { + // This will be called when highlights are ready + const highlights = Object.values(realtimeClipData); + setHighlightClips(prevEvents => { + const updatedEvents = [...prevEvents, ...highlights]; + // Remove excess events from the beginning if we exceed maxEvents + if (updatedEvents.length > maxEvents) { + updatedEvents.splice(0, updatedEvents.length - maxEvents); + if (hasMoreEvents === false) { + setHasMoreEvents(true); + } + } + return updatedEvents; + }); + }, + ); + + realtimeEventSubscription = RealtimeHighlighterService.latestDetectedEvent.subscribe(event => { + if (!event || !event.type) return; + setLastEvent({ type: event.type, game: event.game }); + }); + + // On unmount, unsubscribe from the realtime highlights + return () => { + realtimeEventSubscription?.unsubscribe(); + realtimeHighlightSubscription?.unsubscribe(); + }; + }, []); + + const timeoutRef = React.useRef(null); + + useEffect(() => { + if (highlightClips) { + setShowTooltip(true); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + timeoutRef.current = setTimeout(() => { + setShowTooltip(undefined); + }, 5000); + } + }, [highlightClips]); + + function onViewAll() { + // Navigate to the Highlighter stream view + NavigationService.actions.navigate( + 'Highlighter', + { + view: EHighlighterView.STREAM, + }, + EMenuItemKey.Highlighter, + ); + } + + function onEventItemClick(streamId: string) { + console.log('Open single highlight view for event:', streamId); + // Navigate to specific highlight in stream + NavigationService.actions.navigate( + 'Highlighter', + { + view: EHighlighterView.CLIPS, + id: streamId, + }, + EMenuItemKey.Highlighter, + ); + } + + function getItemOpacity(index: number) { + if (index === highlightClips.length - 1) return 1; + if (index === highlightClips.length - 2) return 0.75; + return 0.5; + } + + const tooltipContent = ( +
+

Clipped highlights

+
+ {hasMoreEvents && ( +
+ +
+ )} + + {highlightClips.length === 0 &&

{$t('Your clipped highlights will appear here')}

} + + {highlightClips && + highlightClips.map((clipData, index) => ( +
+ { + onEventItemClick(clipData.streamId); + }} + latestItem={highlightClips.length - 1 === index} + /> +
+ ))} +
+
+ ); + + return ( + +
{ + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }} + onMouseLeave={() => { + if (showTooltip) { + setShowTooltip(undefined); + } + }} + > + { + HighlighterService.actions.stopRealtimeHighlighter(); + }} + /> +
+
+ ); +} diff --git a/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsIndicator.m.less b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsIndicator.m.less new file mode 100644 index 000000000000..e69a97770835 --- /dev/null +++ b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsIndicator.m.less @@ -0,0 +1,60 @@ +.realtime-indicator { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: rgba(139, 69, 69, 0.8); + border-radius: 20px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: rgba(139, 69, 69, 0.9); + transform: translateY(-1px); + } +} + +.fade-in { + animation: fadeIn 0.4s ease-out forwards; + opacity: 0; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #ff4d4f; + flex-shrink: 0; + animation: pulse 2s infinite; +} + +.status-text { + color: #ffffff; + font-size: 14px; + font-weight: 500; + white-space: nowrap; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} diff --git a/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsIndicator.tsx b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsIndicator.tsx new file mode 100644 index 000000000000..a1a4f5c17419 --- /dev/null +++ b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsIndicator.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import RealtimeHighlightsTooltip from './RealtimeHighlightsFeed'; +import styles from './RealtimeHighlightsIndicator.m.less'; +import RealtimeIndicator from '../RealtimeIndicator'; + +export default function RealtimeHighlightsIndicator() { + return ; +} diff --git a/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsItem.m.less b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsItem.m.less new file mode 100644 index 000000000000..e45004f62f20 --- /dev/null +++ b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsItem.m.less @@ -0,0 +1,49 @@ +.item-wrapper { + padding: 8px; + width: 100%; + display: flex; + background-color: #ffffff10; + gap: 4px; + align-items: center; + flex-direction: column; + border-radius: 4px; +} + +.appear-animation { + animation: fadeIn 0.4s ease-out forwards; + animation-delay: 0.5s; + opacity: 0; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #ff4d4f; + flex-shrink: 0; + animation: pulse 2s infinite; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} diff --git a/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsItem.tsx b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsItem.tsx new file mode 100644 index 000000000000..54f37d36b5b3 --- /dev/null +++ b/app/components-react/highlighter/realtime-highlights/RealtimeHighlightsItem.tsx @@ -0,0 +1,45 @@ +import { Button } from 'antd'; +import React from 'react'; +import { INewClipData } from 'services/highlighter/models/highlighter.models'; +import { getUniqueEmojiConfigFromAiInfo } from '../ClipPreviewInfo'; +import { EGame } from 'services/highlighter/models/ai-highlighter.models'; +import styles from './RealtimeHighlightsItem.m.less'; +import cx from 'classnames'; +import { $t } from 'services/i18n'; + +interface RealtimeHighlightItemProps { + clipData: INewClipData; + onEventItemClick: () => void; + latestItem: boolean; +} + +export default function RealtimeHighlightsItem(props: RealtimeHighlightItemProps) { + const { clipData, onEventItemClick, latestItem } = props; + const emojiDisplayConfig = getUniqueEmojiConfigFromAiInfo(clipData.aiClipInfo); + return ( +
+
+ {emojiDisplayConfig.map((displayConfig, index) => { + return ( +

+ {displayConfig.emoji} {displayConfig.count > 1 ? displayConfig.count : ''}{' '} + {displayConfig.description} +

+ ); + })} +
+ + +
+ ); +} diff --git a/app/components-react/highlighter/utils.ts b/app/components-react/highlighter/utils.ts index 6e53d6adefb2..4f6f77824590 100644 --- a/app/components-react/highlighter/utils.ts +++ b/app/components-react/highlighter/utils.ts @@ -93,13 +93,14 @@ export function aiFilterClips( ): TClip[] { const { rounds, targetDuration, includeAllEvents } = options; + // TODO: this just handles the error for now. Needs to be changed at one point const selectedRounds = rounds.length === 1 && rounds[0] === 0 ? [ ...new Set( clips .filter(clip => clip.source === 'AiClip') - .map(clip => (clip as IAiClip).aiInfo.metadata?.round), + .map(clip => (clip as IAiClip).aiInfo.metadata?.round || 0), ), ] : rounds; diff --git a/app/components-react/pages/Highlighter.tsx b/app/components-react/pages/Highlighter.tsx index 783066511171..42c42375eb46 100644 --- a/app/components-react/pages/Highlighter.tsx +++ b/app/components-react/pages/Highlighter.tsx @@ -12,13 +12,18 @@ import StreamView from 'components-react/highlighter/StreamView'; import ClipsView from 'components-react/highlighter/ClipsView'; import UpdateModal from 'components-react/highlighter/UpdateModal'; import { EAvailableFeatures } from 'services/incremental-rollout'; +import Utils from 'services/utils'; -export default function Highlighter(props: { params?: { view: string } }) { - const { HighlighterService, IncrementalRolloutService, UsageStatisticsService } = Services; +export default function Highlighter(props: { params?: { view: string; id?: string } }) { + const { + HighlighterService, + IncrementalRolloutService, + UsageStatisticsService, + RealtimeHighlighterService, + } = Services; const aiHighlighterFeatureEnabled = IncrementalRolloutService.views.featureIsEnabled( EAvailableFeatures.aiHighlighter, ); - const v = useVuex(() => ({ useAiHighlighter: HighlighterService.views.useAiHighlighter, })); @@ -29,9 +34,23 @@ export default function Highlighter(props: { params?: { view: string } }) { let initialViewState: IViewState; if (props.params?.view) { - const view = - props.params?.view === 'settings' ? EHighlighterView.SETTINGS : EHighlighterView.STREAM; - initialViewState = { view }; + switch (props.params?.view) { + case EHighlighterView.SETTINGS: + initialViewState = { view: EHighlighterView.SETTINGS }; + break; + + case EHighlighterView.STREAM: + initialViewState = { view: EHighlighterView.STREAM }; + break; + + case EHighlighterView.CLIPS: + initialViewState = { view: EHighlighterView.CLIPS, id: props.params.id }; + break; + + default: + initialViewState = { view: EHighlighterView.SETTINGS }; + break; + } } else if (streamAmount > 0 && clipsAmount > 0 && aiHighlighterFeatureEnabled) { initialViewState = { view: EHighlighterView.STREAM }; } else if (clipsAmount > 0) { diff --git a/app/components-react/root/StudioFooter.tsx b/app/components-react/root/StudioFooter.tsx index f28752866efd..c87dad8bd6e9 100644 --- a/app/components-react/root/StudioFooter.tsx +++ b/app/components-react/root/StudioFooter.tsx @@ -12,6 +12,8 @@ import StartStreamingButton from './StartStreamingButton'; import NotificationsArea from './NotificationsArea'; import { Tooltip } from 'antd'; import { confirmAsync } from 'components-react/modals'; +import RealtimeHighlightsIndicator from '../highlighter/realtime-highlights/RealtimeHighlightsIndicator'; +import { useRealmObject } from '../hooks/realm'; export default function StudioFooterComponent() { const { @@ -23,8 +25,12 @@ export default function StudioFooterComponent() { PerformanceService, SettingsService, UserService, + RealtimeHighlighterService, } = Services; + const realtimeHighlighterEnabled = useRealmObject(RealtimeHighlighterService.ephemeralState) + .isRunning; + const { streamingStatus, isLoggedIn, @@ -132,6 +138,7 @@ export default function StudioFooterComponent() {

+ {realtimeHighlighterEnabled && }
{isLoggedIn && }
{recordingModeEnabled && (