@@ -7,11 +7,11 @@ import { useState, FormEvent, useEffect } from "react";
77// QuestionExampleParams,
88// } from "../actions/questionExample";
99// import { getLanguageName } from "../pagesList";
10- import { DynamicMarkdownSection } from "./pageContent" ;
1110import { useEmbedContext } from "@/terminal/embedContext" ;
12- import { askAI } from "@/actions/chatActions" ;
13- import { PagePath } from "@/lib/docs" ;
11+ import { DynamicMarkdownSection , PagePath } from "@/lib/docs" ;
1412import { useRouter } from "next/navigation" ;
13+ import { ChatStreamEvent } from "@/api/chat/route" ;
14+ import { useStreamingChatContext } from "@/(docs)/streamingChatContext" ;
1515
1616interface ChatFormProps {
1717 path : PagePath ;
@@ -28,6 +28,7 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
2828 const { files, replOutputs, execResults } = useEmbedContext ( ) ;
2929
3030 const router = useRouter ( ) ;
31+ const streamingChatContext = useStreamingChatContext ( ) ;
3132
3233 const exampleData = sectionContent
3334 . filter ( ( s ) => s . inView )
@@ -46,42 +47,108 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
4647 } , [ exampleChoice ] ) ;
4748
4849 const handleSubmit = async ( e : FormEvent < HTMLFormElement > ) => {
50+
4951 let userQuestion = inputValue ;
5052 if ( ! userQuestion && exampleData . length > 0 && exampleChoice ) {
5153 // 質問が空欄なら、質問例を使用
5254 userQuestion =
5355 exampleData [ Math . floor ( exampleChoice * exampleData . length ) ] ;
5456 setInputValue ( userQuestion ) ;
5557 }
56- if ( userQuestion ) {
57- e . preventDefault ( ) ;
58- setIsLoading ( true ) ;
59- setErrorMessage ( null ) ; // Clear previous error message
60-
61- const result = await askAI ( {
62- path,
63- userQuestion,
64- sectionContent,
65- replOutputs,
66- files,
67- execResults,
68- } ) ;
58+ if ( ! userQuestion ) {
59+ return ;
60+ }
6961
62+ e . preventDefault ( ) ;
63+ setIsLoading ( true ) ;
64+ setErrorMessage ( null ) ; // Clear previous error message
7065
71- if ( result . error !== null ) {
72- setErrorMessage ( result . error ) ;
73- console . log ( result . error ) ;
74- } else {
75- document . getElementById ( result . chat . sectionId ) ?. scrollIntoView ( {
76- behavior : "smooth" ,
77- } ) ;
78- router . push ( `/chat/${ result . chat . chatId } ` , { scroll : false } ) ;
79- router . refresh ( ) ;
80- setInputValue ( "" ) ;
81- close ( ) ;
82- }
66+ let response : Response ;
67+ try {
68+ response = await fetch ( "/api/chat" , {
69+ method : "POST" ,
70+ headers : { "Content-Type" : "application/json" } ,
71+ body : JSON . stringify ( {
72+ path,
73+ userQuestion,
74+ sectionContent,
75+ replOutputs,
76+ files,
77+ execResults,
78+ } ) ,
79+ } ) ;
80+ } catch {
81+ setErrorMessage ( "AIへの接続に失敗しました" ) ;
82+ setIsLoading ( false ) ;
83+ return ;
84+ }
85+
86+ if ( ! response . ok ) {
87+ setErrorMessage ( `エラーが発生しました (${ response . status } )` ) ;
8388 setIsLoading ( false ) ;
89+ return ;
8490 }
91+
92+ const reader = response . body ! . getReader ( ) ;
93+ const decoder = new TextDecoder ( ) ;
94+ let buffer = "" ;
95+ let navigated = false ;
96+
97+ // ストリームを非同期で読み続ける(ナビゲーション後もバックグラウンドで継続)
98+ void ( async ( ) => {
99+ try {
100+ while ( true ) {
101+ const result = await reader . read ( ) ;
102+ const { done, value } = result ;
103+ if ( done ) break ;
104+
105+ buffer += decoder . decode ( value , { stream : true } ) ;
106+ const lines = buffer . split ( "\n" ) ;
107+ buffer = lines . pop ( ) ?? "" ;
108+
109+ for ( const line of lines ) {
110+ if ( ! line . trim ( ) ) continue ;
111+ try {
112+ const event = JSON . parse ( line ) as ChatStreamEvent ;
113+
114+ if ( event . type === "chat" ) {
115+ streamingChatContext . startStreaming ( event . chatId ) ;
116+ document . getElementById ( event . sectionId ) ?. scrollIntoView ( {
117+ behavior : "smooth" ,
118+ } ) ;
119+ router . push ( `/chat/${ event . chatId } ` , { scroll : false } ) ;
120+ router . refresh ( ) ;
121+ navigated = true ;
122+ setIsLoading ( false ) ;
123+ setInputValue ( "" ) ;
124+ close ( ) ;
125+ } else if ( event . type === "chunk" ) {
126+ streamingChatContext . appendChunk ( event . text ) ;
127+ } else if ( event . type === "done" ) {
128+ streamingChatContext . finishStreaming ( ) ;
129+ router . refresh ( ) ;
130+ } else if ( event . type === "error" ) {
131+ if ( ! navigated ) {
132+ setErrorMessage ( event . message ) ;
133+ setIsLoading ( false ) ;
134+ }
135+ streamingChatContext . finishStreaming ( ) ;
136+ }
137+ } catch {
138+ // ignore JSON parse errors
139+ }
140+ }
141+ }
142+ } catch ( err ) {
143+ console . error ( "Stream reading failed:" , err ) ;
144+ // ナビゲーション後のエラーはストリーミングを終了してローディングを止める
145+ if ( ! navigated ) {
146+ setErrorMessage ( String ( err ) ) ;
147+ setIsLoading ( false ) ;
148+ }
149+ streamingChatContext . finishStreaming ( ) ;
150+ }
151+ } ) ( ) ;
85152 } ;
86153
87154 return (
0 commit comments