AIdentity

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

1_chat.tsx (7482B)


      1 "use client";
      2 import React, {
      3   KeyboardEvent,
      4   useCallback,
      5   useEffect,
      6   useRef,
      7   useState,
      8 } from "react";
      9 import { StageProps } from "../ctrl/page.tsx";
     10 import init, { chat } from "../../rust-wasm/pkg/rust_wasm.js";
     11 const useAudioPlayback = (
     12   initialAudioId: number, // 1
     13   audioSources: Record<number, string>,
     14   onAudioEnd: () => void,
     15 ) => {
     16   const audioRef = useRef<HTMLAudioElement | null>(null);
     17   const initialAudioUrl = audioSources[initialAudioId] || "";
     18 
     19   const stop = useCallback(() => {
     20     if (audioRef.current) {
     21       audioRef.current.pause();
     22       audioRef.current.currentTime = 0;
     23     }
     24   }, []);
     25 
     26   useEffect(() => {
     27     if (!initialAudioUrl) {
     28       console.error("Initial audio URL is missing.");
     29       return;
     30     }
     31 
     32     // 既存の音源を停止
     33     stop();
     34 
     35     const audio = new Audio(initialAudioUrl);
     36     audioRef.current = audio;
     37     audio.loop = false; // ループはしない
     38 
     39     // 再生終了時のハンドラ: 無条件で onAudioEnd を実行
     40     const handleEnded = () => {
     41       console.log("初期音源の再生が終了しました。onAudioEndを実行します。");
     42       stop();
     43       onAudioEnd(); // 完了コールバックを実行
     44     };
     45 
     46     audio.addEventListener("ended", handleEnded);
     47 
     48     audio.play().catch((e) => {
     49       console.log(
     50         "Audio playback failed initially. Waiting for user interaction.",
     51         e,
     52       );
     53     });
     54 
     55     // クリーンアップ関数
     56     return () => {
     57       audio.removeEventListener("ended", handleEnded);
     58       audio.pause();
     59     };
     60   }, [initialAudioUrl, onAudioEnd, stop]);
     61 
     62   return { currentAudioId: initialAudioId };
     63 };
     64 // ------------------------------------------------------------------
     65 // 以下、ChatPage およびその他のコンポーネント
     66 // ------------------------------------------------------------------
     67 const FirstChat: React.FC<StageProps> = ({ onComplete }) => {
     68   return <ChatPage onComplete={onComplete} />;
     69 };
     70 export default FirstChat;
     71 const AUDIO_SOURCES: Record<number, string> = {
     72   1: "/audio/001.flac", // 再生される音源はこれのみ
     73 };
     74 interface Message {
     75   id: number;
     76   text: string;
     77   sender: "user" | "ai";
     78 }
     79 const initialMessages: Message[] = [
     80   { id: 1, text: "そろそろどうするか決めないとだよ?", sender: "ai" },
     81   { id: 2, text: "はやく話して。", sender: "ai" },
     82 ];
     83 const MessageBubble: React.FC<{ message: Message }> = ({ message }) => {
     84   const isUser = message.sender === "user";
     85 
     86   const bubbleStyle: React.CSSProperties = {
     87     padding: "10px 15px",
     88     borderRadius: "15px",
     89     maxWidth: "70%",
     90     wordBreak: "break-word",
     91     fontSize: "16px",
     92     backgroundColor: isUser ? "#3b82f6" : "#e5e7eb", // blue-500 or gray-200
     93     color: isUser ? "white" : "#1f2937", // white or gray-800
     94     marginLeft: isUser ? "auto" : "0",
     95     marginRight: isUser ? "0" : "auto",
     96   };
     97 
     98   const containerStyle: React.CSSProperties = {
     99     display: "flex",
    100     marginBottom: "10px",
    101     justifyContent: isUser ? "flex-end" : "flex-start",
    102   };
    103 
    104   return (
    105     <div style={containerStyle}>
    106       <div style={bubbleStyle}>{message.text}</div>
    107     </div>
    108   );
    109 };
    110 const MessageInput: React.FC<{ onSend: (text: string) => void }> = ({
    111   onSend,
    112 }) => {
    113   const [input, setInput] = useState("");
    114 
    115   const handleSend = () => {
    116     if (input.trim() === "") return;
    117     onSend(input);
    118     setInput("");
    119   };
    120 
    121   const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
    122     if (e.key === "Enter") {
    123       e.preventDefault();
    124       handleSend();
    125     }
    126   };
    127 
    128   const inputStyle: React.CSSProperties = {
    129     flexGrow: 1,
    130     padding: "12px",
    131     border: "1px solid #d1d5db", // gray-300
    132     borderRadius: "8px",
    133     marginRight: "10px",
    134     fontSize: "16px",
    135     outline: "none",
    136   };
    137 
    138   const buttonStyle: React.CSSProperties = {
    139     backgroundColor: "#3b82f6", // blue-500
    140     color: "white",
    141     border: "none",
    142     padding: "12px 20px",
    143     borderRadius: "8px",
    144     cursor: "pointer",
    145     fontWeight: "bold",
    146   };
    147 
    148   return (
    149     <div
    150       style={{
    151         padding: "15px",
    152         backgroundColor: "#f9fafb",
    153         display: "flex",
    154         alignItems: "center",
    155       }}
    156     >
    157       <input
    158         type="text"
    159         style={inputStyle}
    160         placeholder="メッセージを入力してください..."
    161         value={input}
    162         onChange={(e) => setInput(e.target.value)}
    163         onKeyDown={handleKeyPress}
    164       />
    165       <button
    166         type="submit"
    167         style={buttonStyle}
    168         onClick={handleSend}
    169         disabled={input.trim() === ""}
    170       >
    171         送信
    172       </button>
    173     </div>
    174   );
    175 };
    176 function ChatPage({ onComplete }: StageProps) {
    177   const [messages, setMessages] = useState<Message[]>(initialMessages);
    178   const [dict, setDict] = useState<Uint8Array | undefined>(undefined);
    179   const messagesEndRef = useRef<HTMLDivElement>(null);
    180   const dictPath = "/system.dic.zst";
    181 
    182   // for audio: 最初の音源を再生し、終了したら onComplete を呼ぶ
    183   const { currentAudioId } = useAudioPlayback(
    184     1, // initialAudioId (最初の音源ID)
    185     AUDIO_SOURCES,
    186     onComplete,
    187   );
    188 
    189   useEffect(() => {
    190     messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
    191   }, [messages]);
    192 
    193   useEffect(() => {
    194     const loadData = async () => {
    195       const loadedDict = await LoadDict(dictPath);
    196       setDict(loadedDict);
    197     };
    198     loadData();
    199   }, []);
    200 
    201   useEffect(() => {
    202     init();
    203   }, []);
    204 
    205   const handleSendMessage = useCallback(
    206     async (text: string) => {
    207       if (text.trim() === "") return;
    208 
    209       const newUserMessage: Message = {
    210         id: Date.now(),
    211         text,
    212         sender: "user",
    213       };
    214       setMessages((prev) => [...prev, newUserMessage]);
    215 
    216       let ans =
    217         dict == undefined ? "もっとマシなことを言いなさい。" : chat(dict, text);
    218       if (messages[messages.length - 1].text === ans) {
    219         ans = "は?";
    220       }
    221       const aiResponse: Message = {
    222         id: Date.now() + 1,
    223         text: ans,
    224         sender: "ai",
    225       };
    226       setMessages((prev) => [...prev, aiResponse]);
    227     },
    228     [dict, messages],
    229   );
    230 
    231   const pageContainerStyle: React.CSSProperties = {
    232     display: "flex",
    233     flexDirection: "column",
    234     height: "100vh",
    235     maxWidth: "800px",
    236     margin: "0 auto",
    237     backgroundColor: "white",
    238     boxShadow:
    239       "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
    240   };
    241 
    242   const headerStyle: React.CSSProperties = {
    243     padding: "15px",
    244     backgroundColor: "#3b82f6",
    245     color: "white",
    246     textAlign: "center",
    247     fontWeight: "bold",
    248     fontSize: "20px",
    249   };
    250 
    251   const messageListStyle: React.CSSProperties = {
    252     flexGrow: 1,
    253     padding: "15px",
    254     overflowY: "auto",
    255   };
    256 
    257   return (
    258     <div style={pageContainerStyle}>
    259       <div style={headerStyle}>チャット</div>
    260 
    261       <div style={messageListStyle}>
    262         {messages.map((msg) => (
    263           <MessageBubble key={msg.id} message={msg} />
    264         ))}
    265         <div ref={messagesEndRef} />
    266       </div>
    267 
    268       <MessageInput onSend={handleSendMessage} />
    269     </div>
    270   );
    271 }
    272 async function LoadDict(dictPath: string): Promise<Uint8Array> {
    273   try {
    274     const response = await fetch(dictPath);
    275     if (!response.ok) {
    276       throw new Error(`fail to fetch file: ${response.statusText}`);
    277     }
    278     const arrayBuffer = await response.arrayBuffer();
    279     const byte = new Uint8Array(arrayBuffer);
    280     return byte;
    281   } catch (error) {
    282     console.log("error occur", error);
    283   }
    284   return new Uint8Array(0);
    285 }