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 }