11"use client" ;
22
3- import { useState , FormEvent } from "react" ;
4- import clsx from "clsx" ;
3+ import { useState , FormEvent , useEffect } from "react" ;
54import { askAI } from "@/app/actions/chatActions" ;
6- import { StyledMarkdown } from "./markdown" ;
7- import { useChatHistory , type Message } from "../hooks/useChathistory" ;
85import useSWR from "swr" ;
9- import { getQuestionExample } from "../actions/questionExample" ;
6+ import {
7+ getQuestionExample ,
8+ QuestionExampleParams ,
9+ } from "../actions/questionExample" ;
1010import { getLanguageName } from "../pagesList" ;
11- import { ReplCommand , ReplOutput } from "../terminal/repl" ;
11+ import { DynamicMarkdownSection } from "./pageContent" ;
12+ import { useEmbedContext } from "../terminal/embedContext" ;
13+ import { ChatMessage , useChatHistoryContext } from "./chatHistory" ;
1214
1315interface ChatFormProps {
16+ docs_id : string ;
1417 documentContent : string ;
15- sectionId : string ;
16- replOutputs : ReplCommand [ ] ;
17- fileContents : Array < {
18- name : string ;
19- content : string ;
20- } > ;
21- execResults : Record < string , ReplOutput [ ] > ;
18+ sectionContent : DynamicMarkdownSection [ ] ;
19+ close : ( ) => void ;
2220}
2321
2422export function ChatForm ( {
23+ docs_id,
2524 documentContent,
26- sectionId,
27- replOutputs,
28- fileContents,
29- execResults,
25+ sectionContent,
26+ close,
3027} : ChatFormProps ) {
31- const [ messages , updateChatHistory ] = useChatHistory ( sectionId ) ;
28+ // const [messages, updateChatHistory] = useChatHistory(sectionId);
3229 const [ inputValue , setInputValue ] = useState ( "" ) ;
3330 const [ isLoading , setIsLoading ] = useState ( false ) ;
34- const [ isFormVisible , setIsFormVisible ] = useState ( false ) ;
3531
36- const lang = getLanguageName ( sectionId ) ;
32+ const { addChat } = useChatHistoryContext ( ) ;
33+
34+ const lang = getLanguageName ( docs_id ) ;
35+
36+ const { files, replOutputs, execResults } = useEmbedContext ( ) ;
37+
38+ const documentContentInView = sectionContent
39+ . filter ( ( s ) => s . inView )
40+ . map ( ( s ) => s . rawContent )
41+ . join ( "\n\n" ) ;
3742 const { data : exampleData , error : exampleError } = useSWR (
3843 // 質問フォームを開いたときだけで良い
39- isFormVisible ? { lang, documentContent } : null ,
44+ {
45+ lang,
46+ documentContent : documentContentInView ,
47+ } satisfies QuestionExampleParams ,
4048 getQuestionExample ,
4149 {
4250 // リクエストは古くても構わないので1回でいい
@@ -51,13 +59,17 @@ export function ChatForm({
5159 // 質問フォームを開くたびにランダムに選び直し、
5260 // exampleData[Math.floor(exampleChoice * exampleData.length)] を採用する
5361 const [ exampleChoice , setExampleChoice ] = useState < number > ( 0 ) ; // 0〜1
62+ useEffect ( ( ) => {
63+ if ( exampleChoice === 0 ) {
64+ setExampleChoice ( Math . random ( ) ) ;
65+ }
66+ } , [ exampleChoice ] ) ;
5467
5568 const handleSubmit = async ( e : FormEvent < HTMLFormElement > ) => {
5669 e . preventDefault ( ) ;
5770 setIsLoading ( true ) ;
5871
59- const userMessage : Message = { sender : "user" , text : inputValue } ;
60- updateChatHistory ( [ userMessage ] ) ;
72+ const userMessage : ChatMessage = { sender : "user" , text : inputValue } ;
6173
6274 let userQuestion = inputValue ;
6375 if ( ! userQuestion && exampleData ) {
@@ -69,148 +81,89 @@ export function ChatForm({
6981
7082 const result = await askAI ( {
7183 userQuestion,
72- documentContent : documentContent ,
84+ documentContent,
85+ sectionContent,
7386 replOutputs,
74- fileContents ,
87+ files ,
7588 execResults,
7689 } ) ;
7790
7891 if ( result . error ) {
79- const errorMessage : Message = {
80- sender : "ai " ,
92+ const errorMessage : ChatMessage = {
93+ sender : "error " ,
8194 text : `エラー: ${ result . error } ` ,
82- isError : true ,
8395 } ;
84- updateChatHistory ( [ userMessage , errorMessage ] ) ;
96+ console . log ( result . error ) ;
97+ // TODO: ユーザーに表示
8598 } else {
86- const aiMessage : Message = { sender : "ai" , text : result . response } ;
87- updateChatHistory ( [ userMessage , aiMessage ] ) ;
99+ const aiMessage : ChatMessage = { sender : "ai" , text : result . response } ;
100+ const chatId = addChat ( result . targetSectionId , [ userMessage , aiMessage ] ) ;
101+ // TODO: chatIdが指す対象の回答にフォーカス
88102 setInputValue ( "" ) ;
103+ close ( ) ;
89104 }
90105
91106 setIsLoading ( false ) ;
92107 } ;
93108
94- const handleClearHistory = ( ) => {
95- updateChatHistory ( [ ] ) ;
96- } ;
97-
98109 return (
99- < >
100- { isFormVisible && (
101- < form
102- className = "border border-2 border-secondary shadow-md rounded-lg bg-base-100"
110+ < form
111+ className = "border border-2 border-secondary shadow-lg rounded-lg bg-base-100"
112+ style = { {
113+ width : "100%" ,
114+ textAlign : "center" ,
115+ } }
116+ onSubmit = { handleSubmit }
117+ >
118+ < div className = "input-area" >
119+ < textarea
120+ className = "textarea textarea-ghost textarea-md rounded-lg"
121+ placeholder = {
122+ "質問を入力してください" +
123+ ( exampleData
124+ ? ` (例:「${ exampleData [ Math . floor ( exampleChoice * exampleData . length ) ] } 」)`
125+ : "" )
126+ }
103127 style = { {
104128 width : "100%" ,
105- textAlign : "center" ,
106- boxShadow : "-moz-initial" ,
129+ height : "110px" ,
130+ resize : "none" ,
131+ outlineStyle : "none" ,
107132 } }
108- onSubmit = { handleSubmit }
109- >
110- < div className = "input-area" >
111- < textarea
112- className = "textarea textarea-ghost textarea-md rounded-lg"
113- placeholder = {
114- "質問を入力してください" +
115- ( exampleData
116- ? ` (例:「${ exampleData [ Math . floor ( exampleChoice * exampleData . length ) ] } 」)`
117- : "" )
118- }
119- style = { {
120- width : "100%" ,
121- height : "110px" ,
122- resize : "none" ,
123- outlineStyle : "none" ,
124- } }
125- value = { inputValue }
126- onChange = { ( e ) => setInputValue ( e . target . value ) }
127- disabled = { isLoading }
128- > </ textarea >
129- </ div >
130- < div
131- className = "controls"
132- style = { {
133- margin : "10px" ,
134- display : "flex" ,
135- alignItems : "center" ,
136- justifyContent : "space-between" ,
137- } }
133+ value = { inputValue }
134+ onChange = { ( e ) => setInputValue ( e . target . value ) }
135+ disabled = { isLoading }
136+ > </ textarea >
137+ </ div >
138+ < div
139+ className = "controls"
140+ style = { {
141+ margin : "10px" ,
142+ display : "flex" ,
143+ alignItems : "center" ,
144+ justifyContent : "space-between" ,
145+ } }
146+ >
147+ < div className = "left-icons" >
148+ < button
149+ className = "btn btn-soft btn-secondary rounded-full"
150+ onClick = { close }
151+ type = "button"
138152 >
139- < div className = "left-icons" >
140- < button
141- className = "btn btn-soft btn-secondary rounded-full"
142- onClick = { ( ) => setIsFormVisible ( false ) }
143- type = "button"
144- >
145- 閉じる
146- </ button >
147- </ div >
148- < div className = "right-controls" >
149- < button
150- type = "submit"
151- className = "btn btn-soft btn-circle btn-accent border-2 border-accent rounded-full"
152- title = "送信"
153- disabled = { isLoading }
154- >
155- < span className = "icon" > ➤</ span >
156- </ button >
157- </ div >
158- </ div >
159- </ form >
160- ) }
161- { ! isFormVisible && (
162- < button
163- className = "btn btn-soft btn-secondary rounded-full"
164- onClick = { ( ) => {
165- setIsFormVisible ( true ) ;
166- setExampleChoice ( Math . random ( ) ) ;
167- } }
168- >
169- チャットを開く
170- </ button >
171- ) }
172-
173- { messages . length > 0 && (
174- < article className = "mt-4" >
175- < div className = "flex justify-between items-center mb-2" >
176- < h3 className = "text-lg font-semibold" > AIとのチャット</ h3 >
177- < button
178- onClick = { handleClearHistory }
179- className = "btn btn-ghost btn-sm text-xs"
180- aria-label = "チャット履歴を削除"
181- >
182- 履歴を削除
183- </ button >
184- </ div >
185- { messages . map ( ( msg , index ) => (
186- < div
187- key = { index }
188- className = { `chat ${ msg . sender === "user" ? "chat-end" : "chat-start" } ` }
189- >
190- < div
191- className = { clsx (
192- "chat-bubble" ,
193- { "bg-primary text-primary-content" : msg . sender === "user" } ,
194- {
195- "bg-secondary-content dark:bg-neutral text-black dark:text-white" :
196- msg . sender === "ai" && ! msg . isError ,
197- } ,
198- { "chat-bubble-error" : msg . isError }
199- ) }
200- style = { { maxWidth : "100%" , wordBreak : "break-word" } }
201- >
202- < StyledMarkdown content = { msg . text } />
203- </ div >
204- </ div >
205- ) ) }
206- </ article >
207- ) }
208-
209- { isLoading && (
210- < div className = "mt-2 text-l text-gray-500 animate-pulse" >
211- AIが考え中です…
153+ 閉じる
154+ </ button >
155+ </ div >
156+ < div className = "right-controls" >
157+ < button
158+ type = "submit"
159+ className = "btn btn-soft btn-circle btn-accent border-2 border-accent rounded-full"
160+ title = "送信"
161+ disabled = { isLoading }
162+ >
163+ < span className = "icon" > ➤</ span >
164+ </ button >
212165 </ div >
213- ) }
214- </ >
166+ </ div >
167+ </ form >
215168 ) ;
216169}
0 commit comments