Open7
チャットアプリ
code
import { useState } from "react"
import { Send } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
type Message = {
id: number
content: string
sender: "user" | "bot"
}
export default function ChatComponent() {
const [messages, setMessages] = useState<Message[]>([
{ id: 1, content: "こんにちは!どのようなご用件でしょうか?", sender: "bot" },
])
const [input, setInput] = useState("")
const handleSend = () => {
if (input.trim()) {
const newMessage: Message = { id: messages.length + 1, content: input, sender: "user" }
setMessages([...messages, newMessage])
setInput("")
// ボットの応答をシミュレート
setTimeout(() => {
const botResponse: Message = { id: messages.length + 2, content: "ありがとうございます。どのようにお手伝いできますか?", sender: "bot" }
setMessages(prev => [...prev, botResponse])
}, 1000)
}
}
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>チャット</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] pr-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"} mb-4`}
>
{message.sender === "bot" && (
<Avatar className="mr-2">
<AvatarImage src="/placeholder.svg?height=40&width=40" alt="Bot" />
<AvatarFallback>Bot</AvatarFallback>
</Avatar>
)}
<div
className={`p-2 rounded-lg ${
message.sender === "user" ? "bg-primary text-primary-foreground" : "bg-secondary"
}`}
>
{message.content}
</div>
</div>
))}
</ScrollArea>
</CardContent>
<CardFooter>
<form
onSubmit={(e) => {
e.preventDefault()
handleSend()
}}
className="flex w-full items-center space-x-2"
>
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="メッセージを入力..."
className="flex-grow"
/>
<Button type="submit" size="icon">
<Send className="h-4 w-4" />
<span className="sr-only">送信</span>
</Button>
</form>
</CardFooter>
</Card>
)
}
code
import { useState, useRef, useEffect } from "react"
import { Send } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
type Message = {
id: number
content: string
sender: "user" | "bot"
}
export default function ChatComponent() {
const [messages, setMessages] = useState<Message[]>([
{ id: 1, content: "こんにちは!どのようなご用件でしょうか?", sender: "bot" },
])
const [input, setInput] = useState("")
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(scrollToBottom, [messages])
const handleSend = () => {
if (input.trim()) {
const newMessage: Message = { id: messages.length + 1, content: input, sender: "user" }
setMessages([...messages, newMessage])
setInput("")
// ボットの応答をシミュレート
setTimeout(() => {
const botResponse: Message = { id: messages.length + 2, content: "ありがとうございます。どのようにお手伝いできますか?", sender: "bot" }
setMessages(prev => [...prev, botResponse])
}, 1000)
}
}
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>チャット</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] pr-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"} mb-4`}
>
{message.sender === "bot" && (
<Avatar className="mr-2">
<AvatarImage src="/placeholder.svg?height=40&width=40" alt="Bot" />
<AvatarFallback>Bot</AvatarFallback>
</Avatar>
)}
<div
className={`p-2 rounded-lg ${
message.sender === "user" ? "bg-primary text-primary-foreground" : "bg-secondary"
}`}
>
{message.content}
</div>
</div>
))}
<div ref={messagesEndRef} />
</ScrollArea>
</CardContent>
<CardFooter>
<form
onSubmit={(e) => {
e.preventDefault()
handleSend()
}}
className="flex w-full items-center space-x-2"
>
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="メッセージを入力..."
className="flex-grow"
/>
<Button type="submit" size="icon">
<Send className="h-4 w-4" />
<span className="sr-only">送信</span>
</Button>
</form>
</CardFooter>
</Card>
)
}
Commit Messages
# ==== Commit Messages ====
# ==== Commit Messages(Template) ====
# :emoji: #Issue番号 変更内容
# 例) :+1: #438 コメント追加
# 👍 #438 コメント追加
# ==== Prefix ====
# :fix: バグ修正
# :hotfix: クリティカルなバグ修正
# :add: 新規機能・新規ファイル追加
# :feat: feature
# :update: バグではない機能修正
# :change: 仕様変更による機能修正
# :docs: ドキュメントのみ修正
# :disable: 無効化
# :remove(delete): ファイル削除、コードの一部を取り除く
# :rename: ファイル名の変更
# :upgrade: バージョンアップ
# :revert: 修正取り消し
# :style: 空白、セミコロン、行、コーディングフォーマットなどの修正
# :refactor(clean,improve): リファクタリング
# :test: テスト追加や間違っていたテストの修正
# :chore: ビルドツールやライブラリで自動生成されたものをコミットするとき
# ==== Emojis ====
# 🐛 :bug: バグ修正
# 👍 :+1: 機能改善
# ✨ :sparkles: 部分的な機能追加
# 🎨 :art: デザイン変更のみ
# 💢 :anger: コンフリクト
# 🚧 :construction: WIP
# 📝 :memo: 文言修正
# ♻️ :recycle: リファクタリング
# 🔥 :fire: 不要な機能・使われなくなった機能の削除
# 💚 :green_heart: テストやCIの修正・改善
# 👕 :shirt: Lintエラーの修正やコードスタイルの修正
# 🚀 :rocket: パフォーマンス改善
# 🆙 :up: 依存パッケージなどのアップデート
# 👮 :cop: セキュリティ関連の改善
# ⚙ :gear: config変更
# 📚 :books: ドキュメント
code
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { ScrollArea } from "@/components/ui/scroll-area"
type Chat = {
id: string
name: string
lastMessage: string
avatar: string
}
type ChatListProps = {
chats: Chat[]
onSelectChat: (chatId: string) => void
selectedChatId: string | null
}
export function ChatList({ chats, onSelectChat, selectedChatId }: ChatListProps) {
return (
<ScrollArea className="h-[calc(100vh-4rem)] w-full">
{chats.map((chat) => (
<div
key={chat.id}
className={`flex items-center space-x-4 p-4 hover:bg-accent cursor-pointer ${
selectedChatId === chat.id ? 'bg-accent' : ''
}`}
onClick={() => onSelectChat(chat.id)}
>
<Avatar>
<AvatarImage src={chat.avatar} alt={chat.name} />
<AvatarFallback>{chat.name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">{chat.name}</p>
<p className="text-sm text-muted-foreground">{chat.lastMessage}</p>
</div>
</div>
))}
</ScrollArea>
)
}
chat-list.tsx
chat-list.tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { ScrollArea } from "@/components/ui/scroll-area"
type Chat = {
id: string
name: string
lastMessage: string
avatar: string
}
type ChatListProps = {
chats: Chat[]
onSelectChat: (chatId: string) => void
selectedChatId: string | null
}
export function ChatList({ chats, onSelectChat, selectedChatId }: ChatListProps) {
return (
<ScrollArea className="h-[calc(100vh-4rem)] w-full">
<nav aria-label="チャット一覧">
<ul className="space-y-2">
{chats.map((chat) => (
<li key={chat.id}>
<button
className={`w-full flex items-center space-x-4 p-4 hover:bg-accent cursor-pointer ${
selectedChatId === chat.id ? 'bg-accent' : ''
}`}
onClick={() => onSelectChat(chat.id)}
aria-selected={selectedChatId === chat.id}
>
<Avatar>
<AvatarImage src={chat.avatar} alt="" />
<AvatarFallback>{chat.name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-1 text-left">
<p className="text-sm font-medium leading-none">{chat.name}</p>
<p className="text-sm text-muted-foreground">{chat.lastMessage}</p>
</div>
</button>
</li>
))}
</ul>
</nav>
</ScrollArea>
)
}
chat-component.tsx
import { useState, useRef, useEffect } from "react"
import { Send } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
type Message = {
id: number
content: string
sender: "user" | "bot"
}
type ChatComponentProps = {
chatId: string
chatName: string
chatAvatar: string
}
export function ChatComponent({ chatId, chatName, chatAvatar }: ChatComponentProps) {
const [messages, setMessages] = useState<Message[]>([
{ id: 1, content: `こんにちは!${chatName}さん、どのようなご用件でしょうか?`, sender: "bot" },
])
const [input, setInput] = useState("")
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(scrollToBottom, [messages])
const handleSend = () => {
if (input.trim()) {
const newMessage: Message = { id: messages.length + 1, content: input, sender: "user" }
setMessages([...messages, newMessage])
setInput("")
setTimeout(() => {
const botResponse: Message = { id: messages.length + 2, content: `${chatName}さん、ありがとうございます。どのようにお手伝いできますか?`, sender: "bot" }
setMessages(prev => [...prev, botResponse])
}, 1000)
}
}
return (
<Card className="w-full h-full flex flex-col">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Avatar>
<AvatarImage src={chatAvatar} alt="" />
<AvatarFallback>{chatName.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<span>{chatName}</span>
</CardTitle>
</CardHeader>
<CardContent className="flex-grow overflow-hidden">
<ScrollArea className="h-full pr-4">
<div role="log" aria-label="チャットメッセージ">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"} mb-4`}
>
{message.sender === "bot" && (
<Avatar className="mr-2">
<AvatarImage src={chatAvatar} alt="" />
<AvatarFallback>{chatName.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
)}
<div
className={`p-2 rounded-lg ${
message.sender === "user" ? "bg-primary text-primary-foreground" : "bg-secondary"
}`}
>
{message.content}
</div>
</div>
))}
</div>
<div ref={messagesEndRef} />
</ScrollArea>
</CardContent>
<CardFooter>
<form
onSubmit={(e) => {
e.preventDefault()
handleSend()
}}
className="flex w-full items-center space-x-2"
>
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="メッセージを入力..."
className="flex-grow"
aria-label="メッセージ入力"
/>
<Button type="submit" size="icon" aria-label="メッセージを送信">
<Send className="h-4 w-4" />
<span className="sr-only">送信</span>
</Button>
</form>
</CardFooter>
</Card>
)
}
chat-app.tsx
chat-app.tsx
import { useState } from "react"
import { ChatList } from "./chat-list"
import { ChatComponent } from "./chat-component"
type Chat = {
id: string
name: string
lastMessage: string
avatar: string
}
const mockChats: Chat[] = [
{ id: "1", name: "Alice", lastMessage: "こんにちは!", avatar: "/placeholder.svg?height=40&width=40" },
{ id: "2", name: "Bob", lastMessage: "お元気ですか?", avatar: "/placeholder.svg?height=40&width=40" },
{ id: "3", name: "Charlie", lastMessage: "明日の予定は?", avatar: "/placeholder.svg?height=40&width=40" },
]
export default function ChatApp() {
const [selectedChatId, setSelectedChatId] = useState<string | null>(null)
const selectedChat = mockChats.find(chat => chat.id === selectedChatId)
return (
<div className="flex h-screen">
<aside className="w-1/3 border-r" aria-label="チャット一覧">
<ChatList
chats={mockChats}
onSelectChat={setSelectedChatId}
selectedChatId={selectedChatId}
/>
</aside>
<main className="w-2/3">
{selectedChat ? (
<ChatComponent
chatId={selectedChat.id}
chatName={selectedChat.name}
chatAvatar={selectedChat.avatar}
/>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground">
チャットを選択してください
</div>
)}
</main>
</div>
)
}
chat-component.tsx
chat-component.tsx
import { useState, useRef, useEffect } from "react"
import { Send } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
type Message = {
id: number
content: string
sender: "user" | "bot"
}
type ChatComponentProps = {
chatId: string
chatName: string
chatAvatar: string
messages: Message[]
onSendMessage: (content: string) => void
}
export function ChatComponent({ chatId, chatName, chatAvatar, messages, onSendMessage }: ChatComponentProps) {
const [input, setInput] = useState("")
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(scrollToBottom, [messages])
const handleSend = () => {
if (input.trim()) {
onSendMessage(input)
setInput("")
}
}
return (
<Card className="w-full h-full flex flex-col">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Avatar>
<AvatarImage src={chatAvatar} alt="" />
<AvatarFallback>{chatName.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<span>{chatName}</span>
</CardTitle>
</CardHeader>
<CardContent className="flex-grow overflow-hidden">
<ScrollArea className="h-full pr-4">
<div role="log" aria-label="チャットメッセージ">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"} mb-4`}
>
{message.sender === "bot" && (
<Avatar className="mr-2">
<AvatarImage src={chatAvatar} alt="" />
<AvatarFallback>{chatName.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
)}
<div
className={`p-2 rounded-lg ${
message.sender === "user" ? "bg-primary text-primary-foreground" : "bg-secondary"
}`}
>
{message.content}
</div>
</div>
))}
</div>
<div ref={messagesEndRef} />
</ScrollArea>
</CardContent>
<CardFooter>
<form
onSubmit={(e) => {
e.preventDefault()
handleSend()
}}
className="flex w-full items-center space-x-2"
>
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="メッセージを入力..."
className="flex-grow"
aria-label="メッセージ入力"
/>
<Button type="submit" size="icon" aria-label="メッセージを送信">
<Send className="h-4 w-4" />
<span className="sr-only">送信</span>
</Button>
</form>
</CardFooter>
</Card>
)
}
chat-app.tsx
chat-app.tsx
import { useState } from "react"
import { ChatList } from "./chat-list"
import { ChatComponent } from "./chat-component"
type Message = {
id: number
content: string
sender: "user" | "bot"
}
type Chat = {
id: string
name: string
lastMessage: string
avatar: string
messages: Message[]
}
const mockChats: Chat[] = [
{
id: "1",
name: "Alice",
lastMessage: "こんにちは!",
avatar: "/placeholder.svg?height=40&width=40",
messages: [{ id: 1, content: "こんにちは!Aliceさん、どのようなご用件でしょうか?", sender: "bot" }]
},
{
id: "2",
name: "Bob",
lastMessage: "お元気ですか?",
avatar: "/placeholder.svg?height=40&width=40",
messages: [{ id: 1, content: "こんにちは!Bobさん、どのようなご用件でしょうか?", sender: "bot" }]
},
{
id: "3",
name: "Charlie",
lastMessage: "明日の予定は?",
avatar: "/placeholder.svg?height=40&width=40",
messages: [{ id: 1, content: "こんにちは!Charlieさん、どのようなご用件でしょうか?", sender: "bot" }]
},
]
export default function ChatApp() {
const [chats, setChats] = useState<Chat[]>(mockChats)
const [selectedChatId, setSelectedChatId] = useState<string | null>(null)
const selectedChat = chats.find(chat => chat.id === selectedChatId)
const onSendMessage = (chatId: string, content: string) => {
setChats(prevChats => {
const updatedChats = prevChats.map(chat => {
if (chat.id === chatId) {
const newUserMessage = { id: chat.messages.length + 1, content, sender: "user" as const }
const newBotMessage = { id: chat.messages.length + 2, content: `${chat.name}さん、ありがとうございます。どのようにお手伝いできますか?`, sender: "bot" as const }
return {
...chat,
messages: [...chat.messages, newUserMessage, newBotMessage],
lastMessage: content
}
}
return chat
})
return updatedChats
})
}
return (
<div className="flex h-screen">
<aside className="w-1/3 border-r" aria-label="チャット一覧">
<ChatList
chats={chats}
onSelectChat={setSelectedChatId}
selectedChatId={selectedChatId}
/>
</aside>
<main className="w-2/3">
{selectedChat ? (
<ChatComponent
chatId={selectedChat.id}
chatName={selectedChat.name}
chatAvatar={selectedChat.avatar}
messages={selectedChat.messages}
onSendMessage={(content) => onSendMessage(selectedChat.id, content)}
/>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground">
チャットを選択してください
</div>
)}
</main>
</div>
)
}
chat-app.tsx
chat-app.tsx
import { useState } from "react"
import { ChatList } from "./chat-list"
import { ChatComponent } from "./chat-component"
type Message = {
id: number
content: string
sender: "user" | "bot"
}
type Chat = {
id: string
name: string
lastMessage: string
avatar: string
messages: Message[]
}
const mockChats: Chat[] = [
{
id: "1",
name: "Alice",
lastMessage: "こんにちは!",
avatar: "/placeholder.svg?height=40&width=40",
messages: [{ id: 1, content: "こんにちは!Aliceさん、どのようなご用件でしょうか?", sender: "bot" }]
},
{
id: "2",
name: "Bob",
lastMessage: "お元気ですか?",
avatar: "/placeholder.svg?height=40&width=40",
messages: [{ id: 1, content: "こんにちは!Bobさん、どのようなご用件でしょうか?", sender: "bot" }]
},
{
id: "3",
name: "Charlie",
lastMessage: "明日の予定は?",
avatar: "/placeholder.svg?height=40&width=40",
messages: [{ id: 1, content: "こんにちは!Charlieさん、どのようなご用件でしょうか?", sender: "bot" }]
},
]
export default function ChatApp() {
const [chats, setChats] = useState<Chat[]>(mockChats)
const [selectedChatId, setSelectedChatId] = useState<string | null>(null)
const selectedChat = chats.find(chat => chat.id === selectedChatId)
const onSendMessage = (chatId: string, content: string) => {
setChats(prevChats => {
const updatedChats = prevChats.map(chat => {
if (chat.id === chatId) {
const newUserMessage = { id: chat.messages.length + 1, content, sender: "user" as const }
return {
...chat,
messages: [...chat.messages, newUserMessage],
lastMessage: content // ユーザーのメッセージを一時的にlastMessageとして設定
}
}
return chat
})
return updatedChats
})
// ボットの返信を非同期で処理
setTimeout(() => {
setChats(prevChats => {
const updatedChats = prevChats.map(chat => {
if (chat.id === chatId) {
const botResponse = `${chat.name}さん、ありがとうございます。どのようにお手伝いできますか?`
const newBotMessage = { id: chat.messages.length + 1, content: botResponse, sender: "bot" as const }
return {
...chat,
messages: [...chat.messages, newBotMessage],
lastMessage: botResponse // ボットの返信をlastMessageとして設定
}
}
return chat
})
return updatedChats
})
}, 1000) // 1秒後にボットが返信
}
return (
<div className="flex h-screen">
<aside className="w-1/3 border-r" aria-label="チャット一覧">
<ChatList
chats={chats}
onSelectChat={setSelectedChatId}
selectedChatId={selectedChatId}
/>
</aside>
<main className="w-2/3">
{selectedChat ? (
<ChatComponent
chatId={selectedChat.id}
chatName={selectedChat.name}
chatAvatar={selectedChat.avatar}
messages={selectedChat.messages}
onSendMessage={(content) => onSendMessage(selectedChat.id, content)}
/>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground">
チャットを選択してください
</div>
)}
</main>
</div>
)
}