gemini-chat

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

page.tsx (5293B)


      1 "use client";
      2 import React, { useState, useRef, useEffect } from "react";
      3 import { Send, Trash2 } from "lucide-react";
      4 import styles from "./page.module.css";
      5 
      6 export default function Page() {
      7   const GEMINI_API_KEY = process.env.NEXT_PUBLIC_GEMINI_API_KEY;
      8   const SYSTEM_INSTRUCTION = `
      9 あなたは、プロフェッショナルで温かみのある心理カウンセラーです。
     10 以下のガイドラインに従って、ユーザーに寄り添ってください:
     11 
     12 - 傾聴と共感: ユーザーの感情や状況を否定せず、まずは「それはお辛かったですね」「そう思われるのも無理はありません」と受け止めてください。
     13 - 積極的問いかけ: 解決策を急がず、「その時、どのようなお気持ちでしたか?」のように、ユーザーが自分自身の内面を探索できるような問いかけを行ってください。
     14 - 安全な場所の提供: ユーザーが何を話しても安全であると感じられるよう、穏やかで丁寧な言葉遣い(敬語)を徹底してください。
     15 - リフレーミング: ユーザーが自分を責めている場合、別の視点や、その中にあるポジティブな側面を優しく提示してください。
     16 `; // 固定systemInstruction
     17 
     18   const [input, setInput] = useState("");
     19   const [messages, setMessages] = useState<{ role: string; text: string }[]>(
     20     [],
     21   );
     22   const [loading, setLoading] = useState(false);
     23   const [error, setError] = useState<string | null>(null);
     24   const listRef = useRef<HTMLDivElement | null>(null);
     25 
     26   useEffect(() => {
     27     listRef.current?.scrollTo({
     28       top: listRef.current.scrollHeight,
     29       behavior: "smooth",
     30     });
     31   }, [messages]);
     32 
     33   async function sendMessage() {
     34     if (!input.trim()) return;
     35     if (!GEMINI_API_KEY) {
     36       setError("環境変数 NEXT_PUBLIC_GEMINI_API_KEY が設定されていません。");
     37       return;
     38     }
     39 
     40     setError(null);
     41     const userMsg = { role: "user", text: input };
     42     const newMessages = [...messages, userMsg];
     43     setMessages(newMessages);
     44     setInput("");
     45     setLoading(true);
     46 
     47     try {
     48       // 過去の履歴をGemini APIの形式(user / model)に変換
     49       const history = messages.map((m) => ({
     50         role: m.role === "user" ? "user" : "model",
     51         parts: [{ text: m.text }],
     52       }));
     53 
     54       const payload: any = {
     55         systemInstruction: {
     56           parts: [{ text: SYSTEM_INSTRUCTION }],
     57         },
     58         contents: [
     59           ...history,
     60           {
     61             role: "user",
     62             parts: [{ text: userMsg.text }],
     63           },
     64         ],
     65       };
     66 
     67       const res = await fetch(
     68         "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent",
     69         {
     70           method: "POST",
     71           headers: {
     72             "Content-Type": "application/json",
     73             "x-goog-api-key": GEMINI_API_KEY,
     74           } as Record<string, string>,
     75           body: JSON.stringify(payload),
     76         },
     77       );
     78 
     79       if (!res.ok) {
     80         const text = await res.text();
     81         throw new Error(`HTTP ${res.status} - ${text}`);
     82       }
     83 
     84       const data = await res.json();
     85       const assistantText =
     86         data?.candidates?.[0]?.content?.parts?.[0]?.text ??
     87         JSON.stringify(data, null, 2);
     88 
     89       setMessages((m) => [...m, { role: "assistant", text: assistantText }]);
     90     } catch (e: any) {
     91       setError(`Error: ${e.message}`);
     92     } finally {
     93       setLoading(false);
     94     }
     95   }
     96 
     97   function clearConversation() {
     98     setMessages([]);
     99   }
    100 
    101   return (
    102     <div className={styles.chatContainer}>
    103       <header className={styles.chatHeader}>
    104         <h1 className={styles.chatTitle}>Gemini Chat</h1>
    105         <button onClick={clearConversation} className={styles.chatClear}>
    106           <Trash2 size={16} /> クリア
    107         </button>
    108       </header>
    109 
    110       <main ref={listRef} className={styles.chatMessages}>
    111         {messages.length === 0 && (
    112           <div className={styles.chatEmpty}>まだ会話がありません。</div>
    113         )}
    114         {messages.map((m, i) => (
    115           <div
    116             key={i}
    117             className={`${styles.messageRow} ${m.role === "user" ? styles.user : styles.assistant}`}
    118           >
    119             <div
    120               className={`${styles.messageBubble} ${m.role === "user" ? styles.user : styles.assistant}`}
    121             >
    122               {m.text}
    123             </div>
    124           </div>
    125         ))}
    126       </main>
    127 
    128       {error && <div className={styles.chatError}>{error}</div>}
    129 
    130       <footer className={styles.chatFooter}>
    131         <div className={styles.inputBox}>
    132           <textarea
    133             value={input}
    134             onChange={(e) => setInput(e.target.value)}
    135             className={styles.messageInput}
    136             placeholder="メッセージを入力..."
    137             rows={2}
    138             onKeyDown={(e) => {
    139               if (e.key === "Enter" && !e.shiftKey) {
    140                 e.preventDefault();
    141                 sendMessage();
    142               }
    143             }}
    144           />
    145           <button
    146             onClick={sendMessage}
    147             disabled={loading}
    148             className={styles.sendButton}
    149           >
    150             {loading ? "..." : <Send size={18} />}
    151           </button>
    152         </div>
    153       </footer>
    154     </div>
    155   );
    156 }