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>
  )
}