commit dcb1aac809dfab7792e980544d7e7cc632de7156
parent a19ed143d651e73ad987ec9aea7359ab8ea6bc83
Author: minerva-jupiter <ryouturn@gmail.com>
Date: Sun, 19 Oct 2025 02:47:08 +0900
add chat ui
Diffstat:
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