diff --git a/apps/web/src/api/dashboard.ts b/apps/web/src/api/dashboard.ts index 17aeca4d..b8ebdfbf 100644 --- a/apps/web/src/api/dashboard.ts +++ b/apps/web/src/api/dashboard.ts @@ -1,4 +1,6 @@ import { + DashboardAiSummaryResponse, + DashboardAiSummaryResponseSchema, DashboardApplicantsResponse, DashboardApplicantsResponseSchema, DashboardListResponse, @@ -26,6 +28,14 @@ const getApplicantsTicle = async (ticleId: string) => { }); }; +const getAiSummary = async (ticleId: string) => { + return request({ + method: 'GET', + url: `/stream/summary/${ticleId}`, + schema: DashboardAiSummaryResponseSchema, + }); +}; + const startTicle = async (ticleId: string) => { const { data } = await axiosInstance.post(`/dashboard/${ticleId}/start`); @@ -38,4 +48,4 @@ const joinTicle = async (ticleId: string) => { return data; }; -export { getDashboardTicleList, startTicle, joinTicle, getApplicantsTicle }; +export { getDashboardTicleList, startTicle, joinTicle, getApplicantsTicle, getAiSummary }; diff --git a/apps/web/src/assets/icons/ai-summary.svg b/apps/web/src/assets/icons/ai-summary.svg new file mode 100644 index 00000000..52f16db8 --- /dev/null +++ b/apps/web/src/assets/icons/ai-summary.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/components/dashboard/AiSummaryDialog.tsx b/apps/web/src/components/dashboard/AiSummaryDialog.tsx new file mode 100644 index 00000000..34bd5a65 --- /dev/null +++ b/apps/web/src/components/dashboard/AiSummaryDialog.tsx @@ -0,0 +1,35 @@ +import { Dialog } from '@/components/common/Dialog'; +import { useAiSummary } from '@/hooks/api/dashboard'; + +import Loading from '../common/Loading'; + +interface AiSummaryDialogProps { + isOpen: boolean; + onClose: () => void; + ticleId: string; +} + +function AiSummaryDialog({ isOpen, onClose, ticleId }: AiSummaryDialogProps) { + const { data } = useAiSummary(ticleId); + + return ( + + AI 음성 요약 + + + {!data || data?.summary.length === 0 ? ( +
+ + + AI 요약을 처리중이에요. + +
+ ) : ( +

{data?.summary[0]}

+ )} +
+
+ ); +} + +export default AiSummaryDialog; diff --git a/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx b/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx index 0b0499e9..7371fe80 100644 --- a/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx +++ b/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx @@ -1,9 +1,13 @@ import { Link, useNavigate } from '@tanstack/react-router'; import { MouseEvent } from 'react'; +import AiSummaryIc from '@/assets/icons/ai-summary.svg?react'; import Button from '@/components/common/Button'; +import useModal from '@/hooks/useModal'; import { formatDateTimeRange } from '@/utils/date'; +import AiSummaryDialog from '../AiSummaryDialog'; + interface TicleInfoCardProps { ticleId: number; speakerName: string; @@ -21,6 +25,7 @@ function TicleInfoCard({ endTime, status, }: TicleInfoCardProps) { + const { isOpen, onOpen, onClose } = useModal(); const { dateStr, timeRangeStr } = formatDateTimeRange(startTime, endTime); const navigate = useNavigate(); @@ -29,6 +34,11 @@ function TicleInfoCard({ navigate({ to: `/live/${ticleId}` }); }; + const handleAiSummaryDialogOpen = (e: MouseEvent) => { + e.preventDefault(); + onOpen(); + }; + return (
@@ -48,17 +58,33 @@ function TicleInfoCard({ {`${dateStr} ${timeRangeStr}`}
- +
+ {status === 'closed' && ( + + )} + +
+ {isOpen && ( + + )} ); diff --git a/apps/web/src/components/dashboard/open/TicleInfoCard.tsx b/apps/web/src/components/dashboard/open/TicleInfoCard.tsx index c23bd7c6..402fa6a8 100644 --- a/apps/web/src/components/dashboard/open/TicleInfoCard.tsx +++ b/apps/web/src/components/dashboard/open/TicleInfoCard.tsx @@ -1,6 +1,7 @@ import { Link, useNavigate } from '@tanstack/react-router'; import { MouseEvent } from 'react'; +import AiSummaryIc from '@/assets/icons/ai-summary.svg?react'; import PersonFilledIc from '@/assets/icons/person-filled.svg?react'; import Button from '@/components/common/Button'; import { useApplicantsTicle, useStartTicle } from '@/hooks/api/dashboard'; @@ -8,6 +9,7 @@ import useModal from '@/hooks/useModal'; import { formatDateTimeRange } from '@/utils/date'; import ApplicantsDialog from './ApplicantsDialog'; +import AiSummaryDialog from '../AiSummaryDialog'; interface TicleInfoCardProps { ticleId: number; @@ -18,7 +20,17 @@ interface TicleInfoCardProps { } function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: TicleInfoCardProps) { - const { isOpen, onOpen, onClose } = useModal(); + const { + isOpen: isApplicantsDialogOpen, + onOpen: onApplicantsDialogOpen, + onClose: onApplicantsDialogClose, + } = useModal(); + + const { + isOpen: isAiSummaryDialogOpen, + onOpen: onAiSummaryDialogOpen, + onClose: onAiSummaryDialogClose, + } = useModal(); const { data: applicantsData } = useApplicantsTicle(ticleId.toString()); const { mutate: ticleStartMutate } = useStartTicle(); @@ -36,7 +48,12 @@ function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: Ticl const handleApplicantsDialogOpen = (e: MouseEvent) => { e.preventDefault(); - onOpen(); + onApplicantsDialogOpen(); + }; + + const handleAiSummaryDialogOpen = (e: MouseEvent) => { + e.preventDefault(); + onAiSummaryDialogOpen(); }; return ( @@ -55,6 +72,17 @@ function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: Ticl
+ {status === 'closed' && ( + + )}
- {isOpen && ( - + {isApplicantsDialogOpen && ( + + )} + {isAiSummaryDialogOpen && ( + )} diff --git a/apps/web/src/components/live/ControlBar/index.tsx b/apps/web/src/components/live/ControlBar/index.tsx index e6994d38..7607a698 100644 --- a/apps/web/src/components/live/ControlBar/index.tsx +++ b/apps/web/src/components/live/ControlBar/index.tsx @@ -147,7 +147,11 @@ const ControlBar = ({ isOwner, onTicleEnd }: ControlBarProps) => { /> )} {isOpenSettingModal && ( - + )} ); diff --git a/apps/web/src/components/live/SettingDialog/AiSummary.tsx b/apps/web/src/components/live/SettingDialog/AiSummary.tsx index 2e860f1a..13bfaa2d 100644 --- a/apps/web/src/components/live/SettingDialog/AiSummary.tsx +++ b/apps/web/src/components/live/SettingDialog/AiSummary.tsx @@ -1,5 +1,5 @@ import { useParams } from '@tanstack/react-router'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { SOCKET_EVENTS } from '@repo/mediasoup'; import Button from '@/components/common/Button'; @@ -8,15 +8,23 @@ import { useMediasoupState } from '@/contexts/mediasoup/context'; function AiSummary() { const [isRecording, setIsRecording] = useState(false); const { socketRef } = useMediasoupState(); + const socket = socketRef.current; const { ticleId: roomId } = useParams({ from: '/_authenticated/live/$ticleId' }); const handleRecordStart = () => { - const socket = socketRef.current; if (!socket) return; socket.emit(SOCKET_EVENTS.startRecord, { roomId }); setIsRecording(true); }; + useEffect(() => { + if (!socket) return; + socket.emit(SOCKET_EVENTS.getIsRecording, { roomId }, (res) => { + const { isRecording } = res; + setIsRecording(isRecording); + }); + }, []); + return (
diff --git a/apps/web/src/components/live/SettingDialog/index.tsx b/apps/web/src/components/live/SettingDialog/index.tsx index 48f71da8..5ff23b29 100644 --- a/apps/web/src/components/live/SettingDialog/index.tsx +++ b/apps/web/src/components/live/SettingDialog/index.tsx @@ -23,10 +23,12 @@ const listVariants = cva( const SIDEBAR_ITEMS = [ { + onlyForOwner: false, title: '오디오 및 비디오', Component: SelectMedia, }, { + onlyForOwner: true, title: 'AI 음성 요약', Component: AiSummary, }, @@ -35,9 +37,10 @@ const SIDEBAR_ITEMS = [ interface SettingDialogProps { isOpen: boolean; onClose: () => void; + isOwner: boolean; } -function SettingDialog({ isOpen, onClose }: SettingDialogProps) { +function SettingDialog({ isOpen, onClose, isOwner }: SettingDialogProps) { const [activeIndex, setActiveIndex] = useState(0); const Component = SIDEBAR_ITEMS[activeIndex]?.Component; @@ -48,15 +51,18 @@ function SettingDialog({ isOpen, onClose }: SettingDialogProps) {
    - {SIDEBAR_ITEMS.map((item, index) => ( -
  • setActiveIndex(index)} - > - {item.title} -
  • - ))} + {SIDEBAR_ITEMS.map( + (item, index) => + (item.onlyForOwner ? isOwner : true) && ( +
  • setActiveIndex(index)} + > + {item.title} +
  • + ) + )}
{Component && }
diff --git a/apps/web/src/hooks/api/dashboard.ts b/apps/web/src/hooks/api/dashboard.ts index 7cccd1b2..ade5fbaf 100644 --- a/apps/web/src/hooks/api/dashboard.ts +++ b/apps/web/src/hooks/api/dashboard.ts @@ -1,7 +1,13 @@ import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; import { GetDashboardListQueryType } from '@repo/types'; -import { getDashboardTicleList, getApplicantsTicle, startTicle, joinTicle } from '@/api/dashboard'; +import { + getDashboardTicleList, + getApplicantsTicle, + startTicle, + joinTicle, + getAiSummary, +} from '@/api/dashboard'; export const useDashboardTicleList = (params: GetDashboardListQueryType) => { return useInfiniteQuery({ @@ -28,6 +34,14 @@ export const useApplicantsTicle = (ticleId: string) => { }); }; +export const useAiSummary = (ticleId: string) => { + return useQuery({ + queryKey: ['aiSummary', ticleId], + queryFn: () => getAiSummary(ticleId), + enabled: !!ticleId, + }); +}; + export const useStartTicle = () => { const queryClient = useQueryClient(); diff --git a/packages/types/src/dashboard/getDashboardList.ts b/packages/types/src/dashboard/getDashboardList.ts index 90b10c67..23363e3c 100644 --- a/packages/types/src/dashboard/getDashboardList.ts +++ b/packages/types/src/dashboard/getDashboardList.ts @@ -61,3 +61,9 @@ export const DashboardApplicantsResponseSchema = z.array( ); export type DashboardApplicantsResponse = z.infer; + +export const DashboardAiSummaryResponseSchema = z.object({ + summary: z.array(z.string()), +}); + +export type DashboardAiSummaryResponse = z.infer;