11"use client" ;
22
3- import { Fragment , useEffect , useRef , useState } from "react" ;
3+ import { Fragment , useCallback , useEffect , useRef , useState } from "react" ;
44import { ChatForm } from "./chatForm" ;
5- import { Heading , StyledMarkdown } from ". /markdown" ;
5+ import { StyledMarkdown } from "@/markdown /markdown" ;
66import { useChatHistoryContext } from "./chatHistory" ;
77import { useSidebarMdContext } from "@/sidebar" ;
88import clsx from "clsx" ;
@@ -13,11 +13,23 @@ import {
1313 PageEntry ,
1414 PagePath ,
1515} from "@/lib/docs" ;
16+ import { ReplacedRange } from "@/markdown/multiHighlight" ;
17+ import { Heading } from "@/markdown/heading" ;
1618
17- // MarkdownSectionに追加で、ユーザーが今そのセクションを読んでいるかどうか、などの動的な情報を持たせる
18- export type DynamicMarkdownSection = MarkdownSection & {
19+ /**
20+ * MarkdownSectionに追加で、動的な情報を持たせる
21+ */
22+ export interface DynamicMarkdownSection extends MarkdownSection {
23+ /**
24+ * ユーザーが今そのセクションを読んでいるかどうか
25+ */
1926 inView : boolean ;
20- } ;
27+ /**
28+ * チャットの会話を元にAIが書き換えた後の内容
29+ */
30+ replacedContent : string ;
31+ replacedRange : ReplacedRange [ ] ;
32+ }
2133
2234interface PageContentProps {
2335 splitMdContent : MarkdownSection [ ] ;
@@ -31,25 +43,84 @@ export function PageContent(props: PageContentProps) {
3143 const { setSidebarMdContent } = useSidebarMdContext ( ) ;
3244 const { splitMdContent, pageEntry, path } = props ;
3345
46+ const { chatHistories } = useChatHistoryContext ( ) ;
47+
48+ const initDynamicMdContent = useCallback ( ( ) => {
49+ const newContent : DynamicMarkdownSection [ ] = splitMdContent . map (
50+ ( section ) => ( {
51+ ...section ,
52+ inView : false ,
53+ replacedContent : section . rawContent ,
54+ replacedRange : [ ] ,
55+ } )
56+ ) ;
57+ const chatDiffs = chatHistories . map ( ( chat ) => chat . diff ) . flat ( ) ;
58+ chatDiffs . sort ( ( a , b ) => a . createdAt . getTime ( ) - b . createdAt . getTime ( ) ) ;
59+ for ( const diff of chatDiffs ) {
60+ const targetSection = newContent . find ( ( s ) => s . id === diff . sectionId ) ;
61+ if ( targetSection ) {
62+ const startIndex = targetSection . replacedContent . indexOf ( diff . search ) ;
63+ if ( startIndex !== - 1 ) {
64+ const endIndex = startIndex + diff . search . length ;
65+ const replaceLen = diff . replace . length ;
66+ const diffLen = replaceLen - diff . search . length ; // 文字列長の増減分
67+
68+ // 1. 文字列の置換
69+ targetSection . replacedContent =
70+ targetSection . replacedContent . slice ( 0 , startIndex ) +
71+ diff . replace +
72+ targetSection . replacedContent . slice ( endIndex ) ;
73+
74+ // 2. 既存のハイライト範囲のズレを補正(今回の置換箇所より後ろにあるものをシフト)
75+ targetSection . replacedRange = targetSection . replacedRange . map ( ( h ) => {
76+ if ( h . start >= endIndex ) {
77+ // 完全に後ろにある場合は単純にシフト
78+ return {
79+ start : h . start + diffLen ,
80+ end : h . end + diffLen ,
81+ id : h . id ,
82+ } ;
83+ }
84+ if ( h . end >= endIndex ) {
85+ return { start : h . start , end : h . end + diffLen , id : h . id } ;
86+ }
87+ return h ;
88+ } ) ;
89+
90+ // 3. 今回の置換箇所を新たなハイライト範囲として追加
91+ targetSection . replacedRange . push ( {
92+ start : startIndex ,
93+ end : startIndex + replaceLen ,
94+ id : diff . chatId ,
95+ } ) ;
96+ } else {
97+ // TODO: md5ハッシュを参照し過去バージョンのドキュメントへ適用を試みる
98+ console . error (
99+ `Failed to apply diff: search string "${ diff . search } " not found in section ${ targetSection . id } `
100+ ) ;
101+ }
102+ } else {
103+ console . error (
104+ `Failed to apply diff: section with id "${ diff . sectionId } " not found`
105+ ) ;
106+ }
107+ }
108+
109+ return newContent ;
110+ } , [ splitMdContent , chatHistories ] ) ;
111+
34112 // SSR用のローカルstate
35113 const [ dynamicMdContent , setDynamicMdContent ] = useState <
36114 DynamicMarkdownSection [ ]
37- > (
38- splitMdContent . map ( ( section ) => ( {
39- ...section ,
40- inView : false ,
41- } ) )
42- ) ;
115+ > ( ( ) => initDynamicMdContent ( ) ) ;
43116
44117 useEffect ( ( ) => {
45- // props.splitMdContentが変わったときにローカルstateとcontextの両方を更新
46- const newContent = splitMdContent . map ( ( section ) => ( {
47- ...section ,
48- inView : false ,
49- } ) ) ;
118+ // props.splitMdContentが変わったとき, チャットのdiffが変わった時に
119+ // ローカルstateとcontextの両方を更新
120+ const newContent = initDynamicMdContent ( ) ;
50121 setDynamicMdContent ( newContent ) ;
51122 setSidebarMdContent ( path , newContent ) ;
52- } , [ splitMdContent , path , setSidebarMdContent ] ) ;
123+ } , [ initDynamicMdContent , path , setSidebarMdContent ] ) ;
53124
54125 const sectionRefs = useRef < Array < HTMLDivElement | null > > ( [ ] ) ;
55126 // sectionRefsの長さをsplitMdContentに合わせる
@@ -87,8 +158,6 @@ export function PageContent(props: PageContentProps) {
87158
88159 const [ isFormVisible , setIsFormVisible ] = useState ( false ) ;
89160
90- const { chatHistories } = useChatHistoryContext ( ) ;
91-
92161 return (
93162 < div
94163 className = "p-4 mx-auto max-w-full grid"
@@ -110,7 +179,10 @@ export function PageContent(props: PageContentProps) {
110179 } }
111180 >
112181 { /* ドキュメントのコンテンツ */ }
113- < StyledMarkdown content = { section . rawContent } />
182+ < StyledMarkdown
183+ content = { section . replacedContent }
184+ replacedRange = { section . replacedRange }
185+ />
114186 </ div >
115187 < div >
116188 { /* 右側に表示するチャット履歴欄 */ }
0 commit comments