Skip to content

Commit

Permalink
Merge pull request #361 from boostcampwm-2024/feat/#58/ai
Browse files Browse the repository at this point in the history
[Feat] AI 요약 클라이언트 연결
  • Loading branch information
simeunseo authored Dec 4, 2024
2 parents 322d967 + 8c5fc67 commit 59f4d22
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 30 deletions.
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 @@ -147,7 +147,11 @@ const ControlBar = ({ isOwner, onTicleEnd }: ControlBarProps) => {
/>
)}
{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 @@ import { useMediasoupState } from '@/contexts/mediasoup/context';
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);
});
}, []);

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>;

0 comments on commit 59f4d22

Please sign in to comment.