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

削除の追加 #69

Merged
merged 2 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/cdk/lambda/deleteChat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { deleteChat } from './repository';

export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
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' }),
};
}
};
39 changes: 39 additions & 0 deletions packages/cdk/lambda/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as crypto from 'crypto';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
BatchWriteCommand,
DeleteCommand,
DynamoDBDocumentClient,
PutCommand,
QueryCommand,
Expand Down Expand Up @@ -195,3 +196,41 @@ export const updateFeedback = async (

return res.Attributes as RecordedMessage;
};

export const deleteChat = async (
_userId: string,
_chatId: string
): Promise<void> => {
const chatItem = await findChatById(_userId, _chatId);
console.log(chatItem);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

消し忘れ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

消し忘れだったので、削除しました。


// Chat の削除
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,
},
},
};
}),
},
})
);
};
17 changes: 17 additions & 0 deletions packages/cdk/lib/construct/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
103 changes: 79 additions & 24 deletions packages/web/src/components/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -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> = (props) => {
const { conversations, loading } = useConversation();
const { conversations, loading, deleteConversation } = useConversation();
const { chatId } = useParams();
const [openDialog, setOpenDialog] = useState(false);
const [targetDelete, setTargetDelete] = useState<Chat>();
const navigate = useNavigate();

const onDelete = useCallback(
(_chatId: string) => {
deleteConversation(_chatId).then(() => {
setOpenDialog(false);
navigate('/chat');
});
},
[deleteConversation, navigate]
);

return (
<div className={`${props.className ?? ''} flex flex-col gap-1`}>
{loading &&
new Array(10)
.fill('')
.map((_, idx) => (
<>
<DialogConfirmDeleteChat
isOpen={openDialog}
target={targetDelete}
onDelete={onDelete}
onClose={() => {
setOpenDialog(false);
}}
/>
<div
className={`${
props.className ?? ''
} flex flex-col items-start gap-1 overflow-x-hidden`}>
{loading &&
new Array(10)
.fill('')
.map((_, idx) => (
<div
key={idx}
className="bg-aws-sky/20 h-8 w-full animate-pulse rounded"></div>
))}
{conversations.map((chat) => {
const _chatId = chat.chatId.split('#')[1];
return (
<div
key={idx}
className="bg-aws-sky/20 h-8 w-full animate-pulse rounded"></div>
))}
{conversations.map((chat) => (
<Link
key={chat.chatId}
className={`hover:bg-aws-sky flex h-8 items-center rounded p-2
${chatId === chat.chatId.split('#')[1] && 'bg-aws-sky'}
${props.className}`}
to={`/chat/${chat.chatId.split('#')[1]}`}>
<PiChat className="mr-2 " />
<div className="truncate">{chat.title}</div>
</Link>
))}
</div>
key={_chatId}
className={`hover:bg-aws-sky group flex w-full items-center rounded ${
chatId === _chatId && 'bg-aws-sky'
}
${props.className}`}>
<Link
className={`flex h-8 w-full items-center justify-start p-2`}
to={`/chat/${_chatId}`}>
<div className="mr-2 ">
<PiChat />
</div>
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis break-all">
{chat.title}
<div
className={`group-hover:from-aws-sky group-hover:to-aws-sky/40 absolute inset-y-0 right-0 w-8 bg-gradient-to-l
${chatId === _chatId ? 'from-aws-sky' : 'from-aws-squid-ink'}
`}></div>
</div>
</Link>
{chatId === _chatId && (
<div className="-ml-2 flex pr-2">
<ButtonIcon
onClick={() => {
setOpenDialog(true);
setTargetDelete(chat);
}}>
<PiTrash />
</ButtonIcon>
</div>
)}
</div>
);
})}
</div>
</>
);
};

Expand Down
39 changes: 39 additions & 0 deletions packages/web/src/components/DialogConfirmDeleteChat.tsx
Original file line number Diff line number Diff line change
@@ -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> = (props) => {
return (
<ModalDialog {...props} title="削除確認">
<div>
チャット
<span className="font-bold">「{props.target?.title}」</span>
を削除しますか?
</div>

<div className="mt-4 flex justify-end gap-2">
<Button outlined onClick={props.onClose} className="p-2">
Cancel
</Button>
<Button
onClick={() => {
props.onDelete(props.target?.chatId.split('#')[1] ?? '');
}}
className="bg-red-500 p-2 text-white">
削除
</Button>
</div>
</ModalDialog>
);
};

export default DialogConfirmDeleteChat;
69 changes: 69 additions & 0 deletions packages/web/src/components/ModalDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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> = (props) => {
const onClose = useCallback(() => {
if (props.onClose) {
props.onClose();
}
}, [props]);

return (
<>
<Transition appear show={props.isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={() => onClose()}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/30" />
</Transition.Child>

<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-aws-font-color border-b pb-2"
>
{props.title}
</Dialog.Title>

<div className="mt-3">
<p className="text-sm text-aws-font-color/70">
{props.children}
</p>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
);
};

export default ModalDialog;
3 changes: 3 additions & 0 deletions packages/web/src/hooks/useChatApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>(`chats/${chatId}`);
},
listChats: () => {
return http.get<ListChatsResponse>('chats');
},
Expand Down
8 changes: 7 additions & 1 deletion packages/web/src/hooks/useConversation.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};

Expand Down