Skip to content

Commit 74268e3

Browse files
authored
Merge pull request #190 from ut-code/ai-diff
AIがドキュメントのdiffとして回答を出力する
2 parents 2a5fcea + 4397e60 commit 74268e3

File tree

17 files changed

+1069
-201
lines changed

17 files changed

+1069
-201
lines changed

app/[lang]/[pageId]/markdown.tsx

Lines changed: 0 additions & 166 deletions
This file was deleted.

app/[lang]/[pageId]/pageContent.tsx

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"use client";
22

3-
import { Fragment, useEffect, useRef, useState } from "react";
3+
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
44
import { ChatForm } from "./chatForm";
5-
import { Heading, StyledMarkdown } from "./markdown";
5+
import { StyledMarkdown } from "@/markdown/markdown";
66
import { useChatHistoryContext } from "./chatHistory";
77
import { useSidebarMdContext } from "@/sidebar";
88
import 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

2234
interface 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

Comments
 (0)