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

ai-cat-prototype で作成した必要なファイルを移植 #16

Merged
merged 3 commits into from May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 15 additions & 1 deletion next.config.js
@@ -1,4 +1,18 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
// TODO 一時的に追加、エンドユーザーのアバター画像はどうやって設定するか考える
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'lgtm-images.lgtmeow.com',
},
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
},
],
},
}

module.exports = nextConfig
54 changes: 54 additions & 0 deletions src/app/_components/Footer/Footer.tsx
@@ -0,0 +1,54 @@
import Link from 'next/link';
import type { JSX } from 'react';

export const Footer = (): JSX.Element => {
return (
<footer className="m-4 rounded-lg bg-yellow-200 shadow dark:bg-gray-900">
<div className="mx-auto w-full max-w-screen-xl p-4 md:py-8">
<div className="sm:flex sm:items-center sm:justify-between">
<Link href="/" className="mb-4 flex items-center sm:mb-0">
<span className="self-center whitespace-nowrap text-2xl font-semibold dark:text-white">
AI Cat(仮)
</span>
</Link>
<ul className="mb-6 flex flex-wrap items-center text-sm font-medium text-gray-500 dark:text-gray-400 sm:mb-0">
<li>
<Link
href="/"
prefetch={false}
className="mr-4 hover:underline md:mr-6"
>
Top
</Link>
</li>
<li>
<Link
href="/terms"
prefetch={false}
className="mr-4 hover:underline md:mr-6"
>
Terms of Use
</Link>
</li>
<li>
<Link
href="/privacy"
prefetch={false}
className="mr-4 hover:underline md:mr-6"
>
Privacy Policy
</Link>
</li>
</ul>
</div>
<hr className="my-6 border-amber-200 dark:border-gray-700 sm:mx-auto lg:my-8" />
<span className="block text-sm text-gray-500 dark:text-gray-400 sm:text-center">
Copyright (c){' '}
<a href="https://github.com/nekochans" className="hover:underline">
nekochans
</a>
</span>
</div>
</footer>
);
};
1 change: 1 addition & 0 deletions src/app/_components/Footer/index.ts
@@ -0,0 +1 @@
export { Footer } from './Footer';
1 change: 1 addition & 0 deletions src/app/_components/index.ts
@@ -0,0 +1 @@
export { Footer } from './Footer';
33 changes: 33 additions & 0 deletions src/app/api/cats/route.ts
@@ -0,0 +1,33 @@
import { NextResponse } from 'next/server';

type RequestBody = {
catName: string;
message: string;
};

type ResponseBody = {
message: string;
};

export const runtime = 'edge';

export async function POST(request: Request): Promise<NextResponse> {
const requestBody = (await request.json()) as RequestBody;

const response = await fetch(
`${String(process.env.API_BASE_URL)}/cats/${requestBody.catName}/messages`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${String(
process.env.API_BASIC_AUTH_CREDENTIALS
)}`,
},
body: JSON.stringify({ message: requestBody.message }),
}
);
const responseBody = (await response.json()) as ResponseBody;

return NextResponse.json(responseBody, { status: 201 });
}
29 changes: 29 additions & 0 deletions src/app/chat/_components/ChatContent/CatChatMessage.tsx
@@ -0,0 +1,29 @@
import Image from 'next/image';
import type { JSX } from 'react';

type Props = {
message: string;
avatarUrl: string;
name: string;
};
export const CatChatMessage = ({ message, avatarUrl, name }: Props): JSX.Element => {
return (
<div className="flex items-end">
<div className="order-2 mx-2 flex max-w-xs flex-col items-start space-y-2 text-xs">
<div>
<span className="inline-block rounded-lg rounded-bl-none bg-white px-4 py-2">
{message}
</span>
</div>
</div>
<Image
src={avatarUrl}
// TODO width, heightの指定方法をどうするか後で考える
width={330}
height={400}
alt={name}
className="h-10 w-10 rounded-full sm:h-16 sm:w-16"
/>
</div>
);
};
28 changes: 28 additions & 0 deletions src/app/chat/_components/ChatContent/CatLoadingMessage.tsx
@@ -0,0 +1,28 @@
import Image from 'next/image';
import type { JSX } from 'react';

type Props = {
avatarUrl: string;
name: string;
};
export const CatLoadingMessage = ({ avatarUrl, name }: Props): JSX.Element => {
return (
<div className="flex items-end">
<div className="order-2 mx-2 flex max-w-xs flex-col items-start space-y-2 text-xs">
<div>
<span className="inline-block animate-pulse rounded-lg rounded-bl-none bg-white px-4 py-2">
{name}が入力中・・・🐱
</span>
</div>
</div>
<Image
src={avatarUrl}
// TODO width, heightの指定方法をどうするか後で考える
width={330}
height={400}
alt={name}
className="h-10 w-10 rounded-full sm:h-16 sm:w-16"
/>
</div>
);
};
139 changes: 139 additions & 0 deletions src/app/chat/_components/ChatContent/ChatContent.tsx
@@ -0,0 +1,139 @@
'use client';

import { type ChatMessages, ChatMessagesList } from './ChatMessagesList';
import {
type FormEvent,
type KeyboardEvent,
type JSX,
useRef,
useState,
} from 'react';

type ResponseBody = {
message: string;
};

type Props = {
initChatMessages: ChatMessages;
};

export const ChatContent = ({ initChatMessages }: Props): JSX.Element => {
const [isLoading, setIsLoading] = useState(false);

const [chatMessages, setChatMessages] =
useState<ChatMessages>(initChatMessages);

const ref = useRef<HTMLTextAreaElement>(null);

const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (isLoading) {
return;
}

if (ref.current?.value != null) {
const message = ref.current.value;

ref.current.value = '';

const newUserMessage = {
role: 'user',
name: 'User',
message,
avatarUrl: 'https://avatars.githubusercontent.com/u/11032365?s=96&v=4',
} as const;
const newChatMessages = [...chatMessages, ...[newUserMessage]];

setChatMessages(newChatMessages);

setIsLoading(true);

try {
const response = await fetch(`/api/cats`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ catName: 'moko', message }),
});
const body = (await response.json()) as ResponseBody;

const newCatMessage = {
role: 'cat',
name: 'もこちゃん',
message: body.message,
avatarUrl:
'https://lgtm-images.lgtmeow.com/2022/03/23/10/9738095a-f426-48e4-be8d-93f933c42917.webp',
} as const;

const newCatReplyContainedChatMessage = [
...newChatMessages,
...[newCatMessage],
];
setChatMessages(newCatReplyContainedChatMessage);
} catch (error) {
// TODO 後でちゃんとしたエラー処理をする
console.error(error);
} finally {
setIsLoading(false);
}
}
};

const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
if (isLoading) {
return;
}

if (event.shiftKey && event.key === 'Enter') {
const submitEvent = new Event('submit', {
bubbles: true,
cancelable: true,
});
event.currentTarget.form?.dispatchEvent(submitEvent);
event.preventDefault();
}
};

const submitButtonBgColor = isLoading ? 'bg-orange-300' : 'bg-orange-500';

const submitButtonHoverColor = isLoading
? 'hover:bg-orange-200'
: 'hover:bg-orange-400';

return (
<>
<ChatMessagesList chatMessages={chatMessages} isLoading={isLoading} />
<div className="mb-2 border-t-2 border-amber-200 bg-yellow-100 px-4 pt-4 sm:mb-0">
<form
id="send-message"
method="post"
action=""
onSubmit={handleSubmit}
aria-label="send to message"
>
<div className="relative flex">
<textarea
id="message-input"
name="message-input"
placeholder="Type your message here. Press Enter + Shift to send."
className="w-full rounded-md py-3 pl-4 text-gray-600 placeholder:text-gray-600 focus:outline-none focus:placeholder:text-gray-400"
ref={ref}
onKeyDown={handleKeyDown}
/>
</div>
<div className="mt-1 flex flex-row-reverse">
<button
type="submit"
className={`${submitButtonBgColor} ${submitButtonHoverColor} rounded-md px-4 py-3.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600`}
disabled={isLoading}
>
Send
</button>
</div>
</form>
</div>
</>
);
};
17 changes: 17 additions & 0 deletions src/app/chat/_components/ChatContent/ChatContentLayout.tsx
@@ -0,0 +1,17 @@
import { ChatHeader } from './ChatHeader';
import { Footer } from '@/app/_components';
import type { JSX, ReactNode } from 'react';

type Props = {
children: ReactNode;
};

export const ChatContentLayout = ({ children }: Props): JSX.Element => {
return (
<main className="flex h-screen flex-1 flex-col justify-between bg-yellow-100 p-2 sm:p-6">
<ChatHeader />
{children}
<Footer />
</main>
);
};
30 changes: 30 additions & 0 deletions src/app/chat/_components/ChatContent/ChatHeader.tsx
@@ -0,0 +1,30 @@
import Image from 'next/image';
import type { JSX } from 'react';

export const ChatHeader = (): JSX.Element => {
return (
<div className="flex justify-between border-b-2 border-amber-200 bg-yellow-200 py-3 sm:items-center">
<div className="relative flex items-center space-x-4">
<div className="relative">
<Image
src="https://lgtm-images.lgtmeow.com/2022/03/23/10/9738095a-f426-48e4-be8d-93f933c42917.webp"
// TODO width, heightの指定方法をどうするか後で考える
width={330}
height={400}
alt="もこちゃん"
className="h-10 w-10 rounded-full sm:h-16 sm:w-16"
/>
</div>
<div className="flex flex-col leading-tight">
<div className="mt-1 flex items-center text-2xl">
<span className="mr-3 text-gray-700">もこちゃん</span>
</div>
<span className="text-lg text-gray-600">チンチラシルバー</span>
</div>
</div>
<div className="flex items-center space-x-2">
{/* Buttonとかを並べるエリア */}
</div>
</div>
);
};