AIdentity

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

commit dcb1aac809dfab7792e980544d7e7cc632de7156
parent a19ed143d651e73ad987ec9aea7359ab8ea6bc83
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Sun, 19 Oct 2025 02:47:08 +0900

add chat ui

Diffstat:
Aapp/components/1_chat.tsx | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/components/2_draw.tsx | 16++++++++++++++++
Aapp/components/error.tsx | 16++++++++++++++++
Mapp/ctrl/page.tsx | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Dpublic/file.svg | 2--
Dpublic/globe.svg | 2--
Dpublic/next.svg | 2--
Apublic/system.dic | 0
Dpublic/vercel.svg | 2--
Dpublic/window.svg | 2--
10 files changed, 293 insertions(+), 12 deletions(-)

diff --git a/app/components/1_chat.tsx b/app/components/1_chat.tsx @@ -0,0 +1,214 @@ +'use client'; + +import React, { useState, useRef, useEffect, KeyboardEvent, useCallback } from 'react'; +import { StageProps } from '../ctrl/page.tsx'; +import init, { chat } from '../../rust-wasm/pkg/rust_wasm.js'; + +const FirstChat: React.FC<StageProps> = ({ onComplete }) => { + return( + <ChatPage/> + ) +}; + + +export default FirstChat; + +interface Message { + id: number; + text: string; + sender: 'user' | 'ai'; +} + +const initialMessages: Message[] = [ + { id: 1, text: 'そろそろどうするか決めないとだよ?', sender: 'ai' }, + { id: 2, text: 'はやく話して。', sender:'ai'}, +]; + +const MessageBubble: React.FC<{ message: Message }> = ({ message }) => { + const isUser = message.sender === 'user'; + + const bubbleStyle: React.CSSProperties = { + padding: '10px 15px', + borderRadius: '15px', + maxWidth: '70%', + wordBreak: 'break-word', + fontSize: '16px', + backgroundColor: isUser ? '#3b82f6' : '#e5e7eb', // blue-500 or gray-200 + color: isUser ? 'white' : '#1f2937', // white or gray-800 + marginLeft: isUser ? 'auto' : '0', + marginRight: isUser ? '0' : 'auto', + }; + + const containerStyle: React.CSSProperties = { + display: 'flex', + marginBottom: '10px', + justifyContent: isUser ? 'flex-end' : 'flex-start', + }; + + return ( + <div style={containerStyle}> + <div style={bubbleStyle}> + {message.text} + </div> + </div> + ); +}; + +const MessageInput: React.FC<{ onSend: (text: string) => void }> = ({ onSend }) => { + const [input, setInput] = useState(''); + + const handleSend = () => { + if (input.trim() === '') return; + onSend(input); + setInput(''); + }; + + const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSend(); + } + }; + + const inputStyle: React.CSSProperties = { + flexGrow: 1, + padding: '12px', + border: '1px solid #d1d5db', // gray-300 + borderRadius: '8px', + marginRight: '10px', + fontSize: '16px', + outline: 'none', + }; + + const buttonStyle: React.CSSProperties = { + backgroundColor: '#3b82f6', // blue-500 + color: 'white', + border: 'none', + padding: '12px 20px', + borderRadius: '8px', + cursor: 'pointer', + fontWeight: 'bold', + }; + + return ( + <div style={{ padding: '15px', backgroundColor: '#f9fafb', display: 'flex', alignItems: 'center' }}> + <input + type="text" + style={inputStyle} + placeholder="メッセージを入力してください..." + value={input} + onChange={(e) => setInput(e.target.value)} + onKeyDown={handleKeyPress} + /> + <button + type='submit' + style={buttonStyle} + onClick={handleSend} + disabled={input.trim() === ''} + > + 送信 + </button> + </div> + ); +}; + + + +function ChatPage() { + const [messages, setMessages] = useState<Message[]>(initialMessages); + const [ dict, setDict ] = useState<Uint8Array|undefined>(undefined); + const messagesEndRef = useRef<HTMLDivElement>(null); + const dictPath = '/system.dic'; + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + useEffect(() => { + const loadData = async () => { + const loadedDict = await LoadDict(dictPath); + setDict(loadedDict); + } + loadData(); + }, []); + + useEffect(() => { + init(); + }, []); + + const handleSendMessage = useCallback((text: string) => { + console.log(dict); + if (text.trim() === '') return; + + const newUserMessage: Message = { + id: Date.now(), + text, + sender: 'user', + }; + setMessages((prev) => [...prev, newUserMessage]); + const ans = dict == undefined ? 'もっとまともなことを言いなさい。' : chat(dict, text); + const aiResponse: Message = { + id: Date.now() + 1, + text: ans, + sender: 'ai', + }; + setMessages((prev) => [...prev, aiResponse]); + }, [dict]); + + const pageContainerStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + height: '100vh', // 全画面の高さ + maxWidth: '800px', // 最大幅を制限して中央に寄せる + margin: '0 auto', + backgroundColor: 'white', + boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', + }; + + const headerStyle: React.CSSProperties = { + padding: '15px', + backgroundColor: '#3b82f6', // blue-500 + color: 'white', + textAlign: 'center', + fontWeight: 'bold', + fontSize: '20px', + }; + + const messageListStyle: React.CSSProperties = { + flexGrow: 1, // 残りのスペースをすべて占める + padding: '15px', + overflowY: 'auto', // スクロール可能にする + }; + + return ( + <div style={pageContainerStyle}> + <div style={headerStyle}> + チャット + </div> + + <div style={messageListStyle}> + {messages.map((msg) => ( + <MessageBubble key={msg.id} message={msg} /> + ))} + <div ref={messagesEndRef} /> + </div> + + <MessageInput onSend={handleSendMessage} /> + </div> + ); +} + +async function LoadDict(dictPath: string): Promise<Uint8Array> { + try{ + const response = await fetch(dictPath); + if(!response.ok){ + throw new Error(`fail to fetch file: ${response.statusText}`); + } + const arrayBuffer = await response.arrayBuffer(); + const byte = new Uint8Array(arrayBuffer); + return byte; + } catch (error) { + console.log("error occur",error); + } + return new Uint8Array(0); +} diff --git a/app/components/2_draw.tsx b/app/components/2_draw.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { StageProps } from '../ctrl/page.tsx'; + +const SecondDraw: React.FC<StageProps> = ({ onComplete }) => { + return( + <div> + <h1> + this is SecondDraw + </h1> + <button type='button' onClick={onComplete}>onComplete</button> + </div> + ) +}; + + +export default SecondDraw; diff --git a/app/components/error.tsx b/app/components/error.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { StageProps } from '../ctrl/page.tsx'; + +const Error: React.FC<StageProps> = ({ onComplete }) => { + return( + <div> + <h1> + Error + </h1> + <button type='button' onClick={onComplete}>onComplete</button> + </div> + ) +}; + + +export default Error; diff --git a/app/ctrl/page.tsx b/app/ctrl/page.tsx @@ -1,19 +1,57 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useCallback } from 'react'; +import dynamic from 'next/dynamic'; + +export interface StageProps { + onComplete: () => void; +} + +const Loading = () => <p>loading...</p>; + +//component dynamic import handler +const FirstChat = dynamic<StageProps>(() => import('../components/1_chat.tsx'), { + loading: Loading, +}); +const SecondDraw = dynamic<StageProps>(() => import('../components/2_draw.tsx'), { + loading: Loading, +}); +const Error = dynamic<StageProps>(() => import('../components/error.tsx'), { + loading: Loading, +}); export default function Chat() { const pageRef = useRef<HTMLDivElement>(null); const [isFullscreen, setIsFullscreen] = useState(false); const [isInitialCheckDone, setIsInitialCheckDone] = useState(false); + const [ stage, setStage ] = useState(1); + const handleStageComplete = useCallback(() => { + setStage(stage + 1); + }, []); + let StageComponent: React.ReactNode; + switch (stage) { + case 1: + StageComponent = <FirstChat onComplete={handleStageComplete}/>; + break; + case 2: + StageComponent = <SecondDraw onComplete={handleStageComplete}/>; + break; + default: + StageComponent = <Error onComplete={handleStageComplete}/>; + } const checkFullscreen = () => { const isTargetFullscreen = document.fullscreenElement === pageRef.current; setIsFullscreen(isTargetFullscreen); }; +// const checkStage = () => { +// const stage; +// }; + useEffect(() => { + // for fullscreen const element = pageRef.current; if (element && element.requestFullscreen) { element.requestFullscreen() @@ -29,12 +67,19 @@ export default function Chat() { }else{ setIsFullscreen(false); setIsInitialCheckDone(true); + setStage(1); } document.addEventListener('fullscreenchange', checkFullscreen); return () => { document.removeEventListener('fullscreenchange', checkFullscreen); }; + + //for stage +// document.addEventListener('stagechange', checkStage); +// return () => { +// document.removeEventListener('stagechange', checkStage); +// }; }, []); const enterFullScreen = () => { @@ -59,7 +104,7 @@ export default function Chat() { <button type='button' onClick={enterFullScreen} style={{fontSize:'xxx-large'}}>フルスクリーンにする</button> </article> ):( - <h1>you are full screened</h1> + StageComponent ) } </div> diff --git a/public/file.svg b/public/file.svg @@ -1 +0,0 @@ -<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>- \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>- \ No newline at end of file diff --git a/public/next.svg b/public/next.svg @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>- \ No newline at end of file diff --git a/public/system.dic b/public/system.dic Binary files differ. diff --git a/public/vercel.svg b/public/vercel.svg @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>- \ No newline at end of file diff --git a/public/window.svg b/public/window.svg @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>- \ No newline at end of file