Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] AI 요약 클라이언트 연결 #361

Merged
merged 9 commits into from
Dec 4, 2024
12 changes: 11 additions & 1 deletion apps/web/src/api/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
DashboardAiSummaryResponse,
DashboardAiSummaryResponseSchema,
DashboardApplicantsResponse,
DashboardApplicantsResponseSchema,
DashboardListResponse,
Expand Down Expand Up @@ -26,6 +28,14 @@ const getApplicantsTicle = async (ticleId: string) => {
});
};

const getAiSummary = async (ticleId: string) => {
return request<DashboardAiSummaryResponse>({
method: 'GET',
url: `/stream/summary/${ticleId}`,
schema: DashboardAiSummaryResponseSchema,
});
};

const startTicle = async (ticleId: string) => {
const { data } = await axiosInstance.post(`/dashboard/${ticleId}/start`);

Expand All @@ -38,4 +48,4 @@ const joinTicle = async (ticleId: string) => {
return data;
};

export { getDashboardTicleList, startTicle, joinTicle, getApplicantsTicle };
export { getDashboardTicleList, startTicle, joinTicle, getApplicantsTicle, getAiSummary };
5 changes: 5 additions & 0 deletions apps/web/src/assets/icons/ai-summary.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions apps/web/src/components/dashboard/AiSummaryDialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog.Root isOpen={isOpen} onClose={onClose} className="h-[30rem] w-[35rem]">
<Dialog.Title align="center">AI 음성 요약</Dialog.Title>
<Dialog.Close onClose={onClose} />
<Dialog.Content className="custom-scrollbar overflow-y-scroll">
{!data || data?.summary.length === 0 ? (
<div className="flex h-[20rem] w-full flex-col items-center justify-center gap-10">
<Loading color="primary" />
<span className="whitespace-pre text-center text-title1 text-primary">
AI 요약을 처리중이에요.
</span>
</div>
) : (
<p className="whitespace-pre text-body1">{data?.summary[0]}</p>
)}
</Dialog.Content>
</Dialog.Root>
);
}

export default AiSummaryDialog;
48 changes: 37 additions & 11 deletions apps/web/src/components/dashboard/apply/TicleInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,6 +25,7 @@ function TicleInfoCard({
endTime,
status,
}: TicleInfoCardProps) {
const { isOpen, onOpen, onClose } = useModal();
const { dateStr, timeRangeStr } = formatDateTimeRange(startTime, endTime);
const navigate = useNavigate();

Expand All @@ -29,6 +34,11 @@ function TicleInfoCard({
navigate({ to: `/live/${ticleId}` });
};

const handleAiSummaryDialogOpen = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onOpen();
};

return (
<Link to={`/ticle/${ticleId}`}>
<div className="flex items-center justify-between rounded-lg border border-main bg-white p-6 shadow-normal">
Expand All @@ -48,17 +58,33 @@ function TicleInfoCard({
<span className="text-body1 text-main">{`${dateStr} ${timeRangeStr}`}</span>
</div>
</div>
<Button
disabled={status === 'closed' || status === 'open'}
onClick={handleTicleParticipate}
className="w-36"
>
{status === 'closed'
? '종료된 티클'
: status === 'inProgress'
? '티클 참여하기'
: '티클 시작 전'}
</Button>
<div className="flex gap-9">
{status === 'closed' && (
<button
className="flex items-center gap-2 rounded-md p-2.5 hover:bg-teritary"
onClick={handleAiSummaryDialogOpen}
>
<span className="text-title2 text-primary">AI 음성 요약</span>
<div>
<AiSummaryIc className="fill-primary" />
</div>
</button>
)}
<Button
disabled={status === 'closed' || status === 'open'}
onClick={handleTicleParticipate}
className="w-36"
>
{status === 'closed'
? '종료된 티클'
: status === 'inProgress'
? '티클 참여하기'
: '티클 시작 전'}
</Button>
</div>
{isOpen && (
<AiSummaryDialog onClose={onClose} isOpen={isOpen} ticleId={ticleId.toString()} />
)}
</div>
</Link>
);
Expand Down
47 changes: 43 additions & 4 deletions apps/web/src/components/dashboard/open/TicleInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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';
import useModal from '@/hooks/useModal';
import { formatDateTimeRange } from '@/utils/date';

import ApplicantsDialog from './ApplicantsDialog';
import AiSummaryDialog from '../AiSummaryDialog';

interface TicleInfoCardProps {
ticleId: number;
Expand All @@ -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();
Expand All @@ -36,7 +48,12 @@ function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: Ticl

const handleApplicantsDialogOpen = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onOpen();
onApplicantsDialogOpen();
};

const handleAiSummaryDialogOpen = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onAiSummaryDialogOpen();
};

return (
Expand All @@ -55,6 +72,17 @@ function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: Ticl
</div>
</div>
<div className="flex gap-9">
{status === 'closed' && (
<button
className="flex items-center gap-2 rounded-md p-2.5 hover:bg-teritary"
onClick={handleAiSummaryDialogOpen}
>
<span className="text-title2 text-primary">AI 음성 요약</span>
<div>
<AiSummaryIc className="fill-primary" />
</div>
</button>
)}
<button
className="flex items-center gap-2 rounded-md p-2.5 hover:bg-teritary"
onClick={handleApplicantsDialogOpen}
Expand All @@ -77,8 +105,19 @@ function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: Ticl
: '티클 시작하기'}
</Button>
</div>
{isOpen && (
<ApplicantsDialog onClose={onClose} isOpen={isOpen} applicants={applicantsData} />
{isApplicantsDialogOpen && (
<ApplicantsDialog
onClose={onApplicantsDialogClose}
isOpen={isApplicantsDialogOpen}
applicants={applicantsData}
/>
)}
{isAiSummaryDialogOpen && (
<AiSummaryDialog
onClose={onAiSummaryDialogClose}
isOpen={isAiSummaryDialogOpen}
ticleId={ticleId.toString()}
/>
)}
</div>
</Link>
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/live/ControlBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
closeStream('screen');
} else {
startScreenStream();
}

Check warning on line 62 in apps/web/src/components/live/ControlBar/index.tsx

View workflow job for this annotation

GitHub Actions / check

'_' is defined but never used
} catch (_) {
closeStream('screen');
}
Expand Down Expand Up @@ -147,7 +147,11 @@
/>
)}
{isOpenSettingModal && (
<SettingDialog isOpen={isOpenSettingModal} onClose={onCloseSettingModal} />
<SettingDialog
isOpen={isOpenSettingModal}
onClose={onCloseSettingModal}
isOwner={isOwner}
/>
)}
</>
);
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/components/live/SettingDialog/AiSummary.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -8,15 +8,23 @@
function AiSummary() {
const [isRecording, setIsRecording] = useState<boolean>(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);
});
}, []);

Check warning on line 26 in apps/web/src/components/live/SettingDialog/AiSummary.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has missing dependencies: 'roomId' and 'socket'. Either include them or remove the dependency array

return (
<div className="flex flex-col gap-4">
<span className="text-body1">
Expand Down
26 changes: 16 additions & 10 deletions apps/web/src/components/live/SettingDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ const listVariants = cva(

const SIDEBAR_ITEMS = [
{
onlyForOwner: false,
title: '오디오 및 비디오',
Component: SelectMedia,
},
{
onlyForOwner: true,
title: 'AI 음성 요약',
Component: AiSummary,
},
Expand All @@ -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;
Expand All @@ -48,15 +51,18 @@ function SettingDialog({ isOpen, onClose }: SettingDialogProps) {
<Dialog.Close onClose={onClose} />
<Dialog.Content className="flex h-full flex-1 items-center justify-center gap-x-4">
<ul className="flex h-full basis-32 flex-col items-start justify-start gap-y-2">
{SIDEBAR_ITEMS.map((item, index) => (
<li
key={index}
className={listVariants({ active: activeIndex === index })}
onClick={() => setActiveIndex(index)}
>
{item.title}
</li>
))}
{SIDEBAR_ITEMS.map(
(item, index) =>
(item.onlyForOwner ? isOwner : true) && (
<li
key={index}
className={listVariants({ active: activeIndex === index })}
onClick={() => setActiveIndex(index)}
>
{item.title}
</li>
)
)}
</ul>
<div className="h-full flex-1">{Component && <Component />}</div>
</Dialog.Content>
Expand Down
16 changes: 15 additions & 1 deletion apps/web/src/hooks/api/dashboard.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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();

Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/dashboard/getDashboardList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,9 @@ export const DashboardApplicantsResponseSchema = z.array(
);

export type DashboardApplicantsResponse = z.infer<typeof DashboardApplicantsResponseSchema>;

export const DashboardAiSummaryResponseSchema = z.object({
summary: z.array(z.string()),
});

export type DashboardAiSummaryResponse = z.infer<typeof DashboardAiSummaryResponseSchema>;
Loading