11"use client" ;
22
33import { useState , FormEvent } from "react" ;
4+ import clsx from "clsx" ;
45import { askAI } from "@/app/actions/chatActions" ;
56import { StyledMarkdown } from "./markdown" ;
7+ import { useChatHistory , type Message } from "../hooks/useChathistory" ;
8+ import useSWR from "swr" ;
9+ import { getQuestionExample } from "../actions/questionExample" ;
10+ import { getLanguageName } from "../pagesList" ;
611
7- export function ChatForm ( { documentContent } : { documentContent : string } ) {
12+ interface ChatFormProps {
13+ documentContent : string ;
14+ sectionId : string ;
15+ }
16+
17+ export function ChatForm ( { documentContent, sectionId } : ChatFormProps ) {
18+ const [ messages , updateChatHistory ] = useChatHistory ( sectionId ) ;
819 const [ inputValue , setInputValue ] = useState ( "" ) ;
9- const [ response , setResponse ] = useState ( "" ) ;
1020 const [ isLoading , setIsLoading ] = useState ( false ) ;
1121 const [ isFormVisible , setIsFormVisible ] = useState ( false ) ;
1222
23+ const lang = getLanguageName ( sectionId ) ;
24+ const { data : exampleData , error : exampleError } = useSWR (
25+ // 質問フォームを開いたときだけで良い
26+ isFormVisible ? { lang, documentContent } : null ,
27+ getQuestionExample ,
28+ {
29+ // リクエストは古くても構わないので1回でいい
30+ revalidateIfStale : false ,
31+ revalidateOnFocus : false ,
32+ revalidateOnReconnect : false ,
33+ }
34+ ) ;
35+ if ( exampleError ) {
36+ console . error ( "Error getting question example:" , exampleError ) ;
37+ }
38+ // 質問フォームを開くたびにランダムに選び直し、
39+ // exampleData[Math.floor(exampleChoice * exampleData.length)] を採用する
40+ const [ exampleChoice , setExampleChoice ] = useState < number > ( 0 ) ; // 0〜1
41+
1342 const handleSubmit = async ( e : FormEvent < HTMLFormElement > ) => {
1443 e . preventDefault ( ) ;
1544 setIsLoading ( true ) ;
16- setResponse ( "" ) ;
45+
46+ const userMessage : Message = { sender : "user" , text : inputValue } ;
47+ updateChatHistory ( [ userMessage ] ) ;
48+
49+ let userQuestion = inputValue ;
50+ if ( ! userQuestion && exampleData ) {
51+ // 質問が空欄なら、質問例を使用
52+ userQuestion = exampleData [ Math . floor ( exampleChoice * exampleData . length ) ] ;
53+ setInputValue ( userQuestion ) ;
54+ }
1755
1856 const result = await askAI ( {
19- userQuestion : inputValue ,
57+ userQuestion,
2058 documentContent : documentContent ,
2159 } ) ;
2260
2361 if ( result . error ) {
24- setResponse ( `エラー: ${ result . error } ` ) ;
62+ const errorMessage : Message = { sender : "ai" , text : `エラー: ${ result . error } ` , isError : true } ;
63+ updateChatHistory ( [ userMessage , errorMessage ] ) ;
2564 } else {
26- setResponse ( result . response ) ;
65+ const aiMessage : Message = { sender : "ai" , text : result . response } ;
66+ updateChatHistory ( [ userMessage , aiMessage ] ) ;
67+ setInputValue ( "" ) ;
2768 }
2869
2970 setIsLoading ( false ) ;
3071 } ;
72+
73+ const handleClearHistory = ( ) => {
74+ updateChatHistory ( [ ] ) ;
75+ } ;
76+
3177 return (
3278 < >
3379 { isFormVisible && (
3480 < form className = "border border-2 border-secondary shadow-md rounded-lg bg-base-100" style = { { width :"100%" , textAlign :"center" , boxShadow :"-moz-initial" } } onSubmit = { handleSubmit } >
3581 < div className = "input-area" >
3682 < textarea
3783 className = "textarea textarea-ghost textarea-md rounded-lg"
38- placeholder = "質問を入力してください"
39- style = { { width : "100%" , height : "110px" , resize : "none" , outlineStyle : "none" } }
84+ placeholder = {
85+ "質問を入力してください" +
86+ ( exampleData
87+ ? ` (例:「${ exampleData [ Math . floor ( exampleChoice * exampleData . length ) ] } 」)`
88+ : "" )
89+ }
90+ style = { {
91+ width : "100%" ,
92+ height : "110px" ,
93+ resize : "none" ,
94+ outlineStyle : "none" ,
95+ } }
4096 value = { inputValue }
4197 onChange = { ( e ) => setInputValue ( e . target . value ) }
4298 disabled = { isLoading }
@@ -47,8 +103,8 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
47103 < button
48104 className = "btn btn-soft btn-secondary rounded-full"
49105 onClick = { ( ) => setIsFormVisible ( false ) }
106+ type = "button"
50107 >
51-
52108 閉じる
53109 </ button >
54110 </ div >
@@ -68,20 +124,42 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
68124 { ! isFormVisible && (
69125 < button
70126 className = "btn btn-soft btn-secondary rounded-full"
71- onClick = { ( ) => setIsFormVisible ( true ) }
127+ onClick = { ( ) => {
128+ setIsFormVisible ( true ) ;
129+ setExampleChoice ( Math . random ( ) ) ;
130+ } }
72131 >
73132 チャットを開く
74133 </ button >
75134 ) }
76135
77- { response && (
78- < article >
79- < h3 className = "text-lg font-semibold mb-2" > AIの回答</ h3 >
80- < div className = "chat chat-start" >
81- < div className = "chat-bubble bg-secondary-content text-black" style = { { maxWidth : "100%" , wordBreak : "break-word" } } >
82- < div className = "response-container" > < StyledMarkdown content = { response } /> </ div >
83- </ div >
136+ { messages . length > 0 && (
137+ < article className = "mt-4" >
138+ < div className = "flex justify-between items-center mb-2" >
139+ < h3 className = "text-lg font-semibold" > AIとのチャット</ h3 >
140+ < button
141+ onClick = { handleClearHistory }
142+ className = "btn btn-ghost btn-sm text-xs"
143+ aria-label = "チャット履歴を削除"
144+ >
145+ 履歴を削除
146+ </ button >
84147 </ div >
148+ { messages . map ( ( msg , index ) => (
149+ < div key = { index } className = { `chat ${ msg . sender === 'user' ? 'chat-end' : 'chat-start' } ` } >
150+ < div
151+ className = { clsx (
152+ "chat-bubble" ,
153+ { "bg-primary text-primary-content" : msg . sender === 'user' } ,
154+ { "bg-secondary-content text-black" : msg . sender === 'ai' && ! msg . isError } ,
155+ { "chat-bubble-error" : msg . isError }
156+ ) }
157+ style = { { maxWidth : "100%" , wordBreak : "break-word" } }
158+ >
159+ < StyledMarkdown content = { msg . text } />
160+ </ div >
161+ </ div >
162+ ) ) }
85163 </ article >
86164 ) }
87165
@@ -93,4 +171,4 @@ export function ChatForm({ documentContent }: { documentContent: string }) {
93171
94172 </ >
95173 ) ;
96- }
174+ }
0 commit comments