diff --git a/packages/cdk/lambda/deleteChat.ts b/packages/cdk/lambda/deleteChat.ts new file mode 100644 index 00000000..fd8a7a14 --- /dev/null +++ b/packages/cdk/lambda/deleteChat.ts @@ -0,0 +1,32 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { deleteChat } from './repository'; + +export const handler = async ( + event: APIGatewayProxyEvent +): Promise => { + try { + const userId: string = + event.requestContext.authorizer!.claims['cognito:username']; + const chatId = event.pathParameters!.chatId!; + await deleteChat(userId, chatId); + + return { + statusCode: 204, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: '', + }; + } catch (error) { + console.log(error); + return { + statusCode: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ message: 'Internal Server Error' }), + }; + } +}; diff --git a/packages/cdk/lambda/repository.ts b/packages/cdk/lambda/repository.ts index e5b025c7..e24df091 100644 --- a/packages/cdk/lambda/repository.ts +++ b/packages/cdk/lambda/repository.ts @@ -7,6 +7,7 @@ import * as crypto from 'crypto'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { BatchWriteCommand, + DeleteCommand, DynamoDBDocumentClient, PutCommand, QueryCommand, @@ -195,3 +196,39 @@ export const updateFeedback = async ( return res.Attributes as RecordedMessage; }; + +export const deleteChat = async ( + _userId: string, + _chatId: string +): Promise => { + // Chat の削除 + const chatItem = await findChatById(_userId, _chatId); + await dynamoDbDocument.send( + new DeleteCommand({ + TableName: TABLE_NAME, + Key: { + id: chatItem?.id, + createdDate: chatItem?.createdDate, + }, + }) + ); + + // // Message の削除 + const messageItems = await listMessages(_chatId); + await dynamoDbDocument.send( + new BatchWriteCommand({ + RequestItems: { + [TABLE_NAME]: messageItems.map((m) => { + return { + DeleteRequest: { + Key: { + id: m.id, + createdDate: m.createdDate, + }, + }, + }; + }), + }, + }) + ); +}; diff --git a/packages/cdk/lib/construct/api.ts b/packages/cdk/lib/construct/api.ts index 9cc03552..527ff0c3 100644 --- a/packages/cdk/lib/construct/api.ts +++ b/packages/cdk/lib/construct/api.ts @@ -83,6 +83,16 @@ export class Api extends Construct { }); table.grantWriteData(createChatFunction); + const deleteChatFunction = new NodejsFunction(this, 'DeleteChat', { + runtime: Runtime.NODEJS_18_X, + entry: './lambda/deleteChat.ts', + timeout: Duration.minutes(15), + environment: { + TABLE_NAME: table.tableName, + }, + }); + table.grantReadWriteData(deleteChatFunction); + const createMessagesFunction = new NodejsFunction(this, 'CreateMessages', { runtime: Runtime.NODEJS_18_X, entry: './lambda/createMessages.ts', @@ -210,6 +220,13 @@ export class Api extends Construct { commonAuthorizerProps ); + // DELETE: /chats/{chatId} + chatResource.addMethod( + 'DELETE', + new LambdaIntegration(deleteChatFunction), + commonAuthorizerProps + ); + const messagesResource = chatResource.addResource('messages'); // GET: /chats/{chatId}/messages diff --git a/packages/web/src/components/ChatList.tsx b/packages/web/src/components/ChatList.tsx index f296490d..2ef87893 100644 --- a/packages/web/src/components/ChatList.tsx +++ b/packages/web/src/components/ChatList.tsx @@ -1,37 +1,92 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { BaseProps } from '../@types/common'; import useConversation from '../hooks/useConversation'; -import { PiChat } from 'react-icons/pi'; -import { Link, useParams } from 'react-router-dom'; +import { PiChat, PiTrash } from 'react-icons/pi'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import ButtonIcon from './ButtonIcon'; +import DialogConfirmDeleteChat from './DialogConfirmDeleteChat'; +import { Chat } from 'generative-ai-use-cases-jp'; type Props = BaseProps; const ChatList: React.FC = (props) => { - const { conversations, loading } = useConversation(); + const { conversations, loading, deleteConversation } = useConversation(); const { chatId } = useParams(); + const [openDialog, setOpenDialog] = useState(false); + const [targetDelete, setTargetDelete] = useState(); + const navigate = useNavigate(); + + const onDelete = useCallback( + (_chatId: string) => { + deleteConversation(_chatId).then(() => { + setOpenDialog(false); + navigate('/chat'); + }); + }, + [deleteConversation, navigate] + ); return ( -
- {loading && - new Array(10) - .fill('') - .map((_, idx) => ( + <> + { + setOpenDialog(false); + }} + /> +
+ {loading && + new Array(10) + .fill('') + .map((_, idx) => ( +
+ ))} + {conversations.map((chat) => { + const _chatId = chat.chatId.split('#')[1]; + return (
- ))} - {conversations.map((chat) => ( - - -
{chat.title}
- - ))} -
+ key={_chatId} + className={`hover:bg-aws-sky group flex w-full items-center rounded ${ + chatId === _chatId && 'bg-aws-sky' + } + ${props.className}`}> + +
+ +
+
+ {chat.title} +
+
+ + {chatId === _chatId && ( +
+ { + setOpenDialog(true); + setTargetDelete(chat); + }}> + + +
+ )} +
+ ); + })} + + ); }; diff --git a/packages/web/src/components/DialogConfirmDeleteChat.tsx b/packages/web/src/components/DialogConfirmDeleteChat.tsx new file mode 100644 index 00000000..e693854c --- /dev/null +++ b/packages/web/src/components/DialogConfirmDeleteChat.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { BaseProps } from '../@types/common'; +import Button from './Button'; +import ModalDialog from './ModalDialog'; +import { Chat } from 'generative-ai-use-cases-jp'; + +type Props = BaseProps & { + isOpen: boolean; + target?: Chat; + onDelete: (chatId: string) => void; + onClose: () => void; +}; + +const DialogConfirmDeleteChat: React.FC = (props) => { + return ( + +
+ チャット + 「{props.target?.title}」 + を削除しますか? +
+ +
+ + +
+
+ ); +}; + +export default DialogConfirmDeleteChat; diff --git a/packages/web/src/components/ModalDialog.tsx b/packages/web/src/components/ModalDialog.tsx new file mode 100644 index 00000000..00f1c834 --- /dev/null +++ b/packages/web/src/components/ModalDialog.tsx @@ -0,0 +1,66 @@ +import { Dialog, Transition } from '@headlessui/react'; +import { Fragment, useCallback } from 'react'; +import { BaseProps } from '../@types/common'; + +type Props = BaseProps & { + isOpen: boolean; + title: string; + children: React.ReactNode; + onClose?: () => void; +}; + +const ModalDialog: React.FC = (props) => { + const onClose = useCallback(() => { + if (props.onClose) { + props.onClose(); + } + }, [props]); + + return ( + <> + + onClose()}> + +
+ + +
+
+ + + + {props.title} + + +
+
+ {props.children} +
+
+
+
+
+
+
+
+ + ); +}; + +export default ModalDialog; diff --git a/packages/web/src/hooks/useChatApi.ts b/packages/web/src/hooks/useChatApi.ts index 03eb3cd4..42ef0355 100644 --- a/packages/web/src/hooks/useChatApi.ts +++ b/packages/web/src/hooks/useChatApi.ts @@ -37,6 +37,9 @@ const useChatApi = () => { const res = await http.post(`chats/${chatId}/messages`, req); return res.data; }, + deleteChat: async (chatId: string) => { + return http.delete(`chats/${chatId}`); + }, listChats: () => { return http.get('chats'); }, diff --git a/packages/web/src/hooks/useConversation.ts b/packages/web/src/hooks/useConversation.ts index c099aed7..5fd9c76d 100644 --- a/packages/web/src/hooks/useConversation.ts +++ b/packages/web/src/hooks/useConversation.ts @@ -1,13 +1,19 @@ import useChatApi from './useChatApi'; const useConversation = () => { - const { listChats } = useChatApi(); + const { listChats, deleteChat } = useChatApi(); const { data, isLoading, mutate } = listChats(); + const deleteConversation = (chatId: string) => { + return deleteChat(chatId).then(() => { + mutate(); + }); + }; return { loading: isLoading, conversations: data ? data.chats : [], mutate, + deleteConversation, }; };