Skip to content

Commit c4a1b5b

Browse files
authored
Merge pull request #183 from ut-code/chat-with-sectionid
チャットがセクションidを返すようにする
2 parents 29d78a1 + 087c580 commit c4a1b5b

File tree

23 files changed

+1044
-228
lines changed

23 files changed

+1044
-228
lines changed

.github/workflows/node.js.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ jobs:
3333
- run: npm ci
3434
- run: npm run tsc
3535

36+
check-docs:
37+
runs-on: ubuntu-latest
38+
strategy:
39+
matrix:
40+
node-version: [22.x]
41+
steps:
42+
- uses: actions/checkout@v4
43+
- uses: actions/setup-node@v4
44+
with:
45+
node-version: ${{ matrix.node-version }}
46+
cache: 'npm'
47+
- run: npm ci
48+
- run: npm run checkDocs
49+
3650
test-js-eval:
3751
runs-on: ubuntu-latest
3852
strategy:

.gitignore

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
/.open-next
44
/cloudflare-env.d.ts
55

6-
# generated docs section file lists (regenerated by npm run generateSections)
7-
/public/docs/**/sections.yml
8-
9-
# generated languages list (regenerated by npm run generateLanguages)
10-
/public/docs/languages.yml
6+
# generated docs section file lists (regenerated by npm run generateDocsMeta)
7+
/public/docs/**/sections.json
8+
/public/docs/languages.json
119

1210
# dependencies
1311
/node_modules

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ COPY --from=dependencies /app/node_modules ./node_modules
2121
# Copy application source code
2222
COPY . .
2323

24-
ENV NODE_ENV=production
24+
# Stop if documentation has any change that is not reflected to revisions.yml and database.
25+
RUN npx tsx ./scripts/checkDocs.ts --check-diff
2526

2627
# Next.js collects completely anonymous telemetry data about general usage.
2728
# Learn more here: https://nextjs.org/telemetry

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ npm run lint
5454
```
5555
でコードをチェックします。出てくるwarningやerrorはできるだけ直しましょう。
5656

57+
### データベースのスキーマ
58+
5759
* データベースのスキーマ(./app/schema/hoge.ts)を編集した場合、 `npx drizzle-kit generate` でmigrationファイルを作成し、 `npx drizzle-kit migrate` でデータベースに反映します。
5860
* また、mainにマージする際に本番環境のデータベースにもmigrateをする必要があります
5961
* スキーマのファイルを追加した場合は app/lib/drizzle.ts でimportを追加する必要があります(たぶん)
@@ -72,6 +74,11 @@ Cloudflare Worker のビルドログとステータス表示が見れますが
7274

7375
## ドキュメント
7476

77+
```bash
78+
npm run checkDocs
79+
```
80+
でドキュメントの読み込み時にエラーにならないか確認できます (index.ymlの間違いなど)
81+
7582
* ドキュメントはセクション(見出し)ごとにわけ、 public/docs/言語id/ページid/並び替え用連番-セクション名.md に置く。
7683
* ページはディレクトリの名前によらず 言語id/index.yml に書かれている順で表示される。
7784
* セクションはセクションIDによらずファイル名順で表示される。
@@ -111,6 +118,7 @@ Cloudflare Worker のビルドログとステータス表示が見れますが
111118
* REPLのコード例は1セクションに最大1つまで。
112119
* コードエディターとコード実行ブロックはいくつでも置けます。
113120
* ページ0以外の各ページの最後はレベル2見出し「この章のまとめ」と、レベル3見出し「練習問題n」を置く
121+
* ドキュメントに変更を加えたものをmainブランチにpushした際、public/docs/revisions.ymlが更新されます。基本的には手動でこのファイルを編集する必要はありません。
114122

115123
### ベースとなるドキュメントの作り方
116124

@@ -141,18 +149,17 @@ Cloudflare Worker のビルドログとステータス表示が見れますが
141149
- Canvasを使われた場合はやり直す。(Canvasはファイル名付きコードブロックで壊れる)
142150
- 太字がなぜか `**キーワード**` の代わりに `\*\*キーワード\*\*` となっている場合がある。 `\*\*` → `**` の置き換えで対応
143151
- 見出しの前に `-----` (水平線)が入る場合がある。my.code();は水平線の表示に対応しているが、消す方向で統一
144-
- `言語名-repl` にはページ内で一意なIDを追加する (例: `言語名-repl:1`)
145152
- REPLの出力部分に書かれたコメントは消えるので修正する
146153
- ダメな例
147154
````
148-
```js-repl:1
155+
```js-repl
149156
> console.log("Hello")
150157
Hello // 文字列を表示する
151158
```
152159
````
153160
- 以下のようにすればok
154161
````
155-
```js-repl:1
162+
```js-repl
156163
> console.log("Hello") // 文字列を表示する
157164
Hello
158165
@@ -162,7 +169,6 @@ Cloudflare Worker のビルドログとステータス表示が見れますが
162169
```
163170
````
164171
- 練習問題のファイル名は不都合がなければ `practice(章番号)_(問題番号).拡張子` で統一。空でもよいのでファイルコードブロックとexecコードブロックを置く
165-
- 1章にはたぶん練習問題要らない。
166172

167173
## markdown仕様
168174

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,15 @@ import { DynamicMarkdownSection } from "./pageContent";
1111
import { useEmbedContext } from "@/terminal/embedContext";
1212
import { useChatHistoryContext } from "./chatHistory";
1313
import { askAI } from "@/actions/chatActions";
14+
import { PagePath } from "@/lib/docs";
1415

1516
interface ChatFormProps {
16-
docs_id: string;
17-
documentContent: string;
17+
path: PagePath;
1818
sectionContent: DynamicMarkdownSection[];
1919
close: () => void;
2020
}
2121

22-
export function ChatForm({
23-
docs_id,
24-
documentContent,
25-
sectionContent,
26-
close,
27-
}: ChatFormProps) {
22+
export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
2823
// const [messages, updateChatHistory] = useChatHistory(sectionId);
2924
const [inputValue, setInputValue] = useState("");
3025
const [isLoading, setIsLoading] = useState(false);
@@ -80,9 +75,8 @@ export function ChatForm({
8075
// }
8176

8277
const result = await askAI({
78+
path,
8379
userQuestion,
84-
docsId: docs_id,
85-
documentContent,
8680
sectionContent,
8781
replOutputs,
8882
files,
@@ -94,7 +88,9 @@ export function ChatForm({
9488
console.log(result.error);
9589
} else {
9690
addChat(result.chat);
97-
// TODO: chatIdが指す対象の回答にフォーカス
91+
document.getElementById(result.chat.sectionId)?.scrollIntoView({
92+
behavior: "smooth",
93+
});
9894
setInputValue("");
9995
close();
10096
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { ChatWithMessages, getChat } from "@/lib/chatHistory";
4+
import { PagePath } from "@/lib/docs";
45
import {
56
createContext,
67
ReactNode,
@@ -28,11 +29,11 @@ export function useChatHistoryContext() {
2829

2930
export function ChatHistoryProvider({
3031
children,
31-
docs_id,
32+
path,
3233
initialChatHistories,
3334
}: {
3435
children: ReactNode;
35-
docs_id: string;
36+
path: PagePath;
3637
initialChatHistories: ChatWithMessages[];
3738
}) {
3839
const [chatHistories, setChatHistories] =
@@ -43,7 +44,7 @@ export function ChatHistoryProvider({
4344
}, [initialChatHistories]);
4445
// その後、クライアント側で最新のchatHistoriesを改めて取得して更新する
4546
const { data: fetchedChatHistories } = useSWR<ChatWithMessages[]>(
46-
docs_id,
47+
path,
4748
getChat,
4849
{
4950
// リクエストは古くても構わないので1回でいい

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ import { notFound } from "next/navigation";
33
import { PageContent } from "./pageContent";
44
import { ChatHistoryProvider } from "./chatHistory";
55
import { getChatFromCache, initContext } from "@/lib/chatHistory";
6-
import { getMarkdownSections, getPagesList } from "@/lib/docs";
6+
import {
7+
getMarkdownSections,
8+
getPagesList,
9+
LangId,
10+
PageSlug,
11+
} from "@/lib/docs";
712

813
export async function generateMetadata({
914
params,
1015
}: {
11-
params: Promise<{ lang: string; pageId: string }>;
16+
params: Promise<{ lang: LangId; pageId: PageSlug }>;
1217
}): Promise<Metadata> {
1318
const { lang, pageId } = await params;
1419
const pagesList = await getPagesList();
@@ -28,35 +33,31 @@ export async function generateMetadata({
2833
export default async function Page({
2934
params,
3035
}: {
31-
params: Promise<{ lang: string; pageId: string }>;
36+
params: Promise<{ lang: LangId; pageId: PageSlug }>;
3237
}) {
3338
const { lang, pageId } = await params;
3439
const pagesList = await getPagesList();
3540
const langEntry = pagesList.find((l) => l.id === lang);
3641
const pageEntry = langEntry?.pages.find((p) => p.slug === pageId);
3742
if (!langEntry || !pageEntry) notFound();
3843

39-
const docsId = `${lang}/${pageId}`;
44+
// server componentなのでuseMemoいらない
45+
const path = { lang: lang, page: pageId };
4046
const sections = await getMarkdownSections(lang, pageId);
4147

42-
// AI用のドキュメント全文(rawContentを結合)
43-
const documentContent = sections.map((s) => s.rawContent).join("\n");
44-
4548
const context = await initContext();
46-
const initialChatHistories = await getChatFromCache(docsId, context);
49+
const initialChatHistories = await getChatFromCache(path, context);
4750

4851
return (
4952
<ChatHistoryProvider
5053
initialChatHistories={initialChatHistories}
51-
docs_id={docsId}
54+
path={path}
5255
>
5356
<PageContent
54-
documentContent={documentContent}
5557
splitMdContent={sections}
58+
langEntry={langEntry}
5659
pageEntry={pageEntry}
57-
docs_id={docsId}
58-
lang={lang}
59-
pageId={pageId}
60+
path={path}
6061
/>
6162
</ChatHistoryProvider>
6263
);

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

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,47 @@ import { Heading, StyledMarkdown } from "./markdown";
66
import { useChatHistoryContext } from "./chatHistory";
77
import { useSidebarMdContext } from "@/sidebar";
88
import clsx from "clsx";
9-
import { MarkdownSection, PageEntry } from "@/lib/docs";
9+
import {
10+
LanguageEntry,
11+
MarkdownSection,
12+
PageEntry,
13+
PagePath,
14+
} from "@/lib/docs";
1015

1116
// MarkdownSectionに追加で、ユーザーが今そのセクションを読んでいるかどうか、などの動的な情報を持たせる
1217
export type DynamicMarkdownSection = MarkdownSection & {
1318
inView: boolean;
1419
};
1520

1621
interface PageContentProps {
17-
documentContent: string;
1822
splitMdContent: MarkdownSection[];
23+
langEntry: LanguageEntry;
1924
pageEntry: PageEntry;
20-
lang: string;
21-
pageId: string;
22-
// TODO: チャット周りのid管理をsectionIdに移行し、docs_idパラメータを削除
23-
docs_id: string;
25+
path: PagePath;
2426
}
2527
export function PageContent(props: PageContentProps) {
2628
const { setSidebarMdContent } = useSidebarMdContext();
29+
const { splitMdContent, pageEntry, path } = props;
2730

2831
// SSR用のローカルstate
2932
const [dynamicMdContent, setDynamicMdContent] = useState<
3033
DynamicMarkdownSection[]
3134
>(
32-
props.splitMdContent.map((section) => ({
35+
splitMdContent.map((section) => ({
3336
...section,
3437
inView: false,
3538
}))
3639
);
3740

3841
useEffect(() => {
3942
// props.splitMdContentが変わったときにローカルstateとcontextの両方を更新
40-
const newContent = props.splitMdContent.map((section) => ({
43+
const newContent = splitMdContent.map((section) => ({
4144
...section,
4245
inView: false,
4346
}));
4447
setDynamicMdContent(newContent);
45-
setSidebarMdContent(props.lang, props.pageId, newContent);
46-
}, [props.splitMdContent, props.lang, props.pageId, setSidebarMdContent]);
48+
setSidebarMdContent(path, newContent);
49+
}, [splitMdContent, path, setSidebarMdContent]);
4750

4851
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
4952
// sectionRefsの長さをsplitMdContentに合わせる
@@ -70,14 +73,14 @@ export function PageContent(props: PageContentProps) {
7073

7174
// ローカルstateとcontextの両方を更新
7275
setDynamicMdContent(updateContent);
73-
setSidebarMdContent(props.lang, props.pageId, updateContent);
76+
setSidebarMdContent(path, updateContent);
7477
};
7578
window.addEventListener("scroll", handleScroll);
7679
handleScroll();
7780
return () => {
7881
window.removeEventListener("scroll", handleScroll);
7982
};
80-
}, [setSidebarMdContent, props.lang, props.pageId]);
83+
}, [setSidebarMdContent, path]);
8184

8285
const [isFormVisible, setIsFormVisible] = useState(false);
8386

@@ -91,7 +94,7 @@ export function PageContent(props: PageContentProps) {
9194
}}
9295
>
9396
<Heading level={1}>
94-
{props.pageEntry.index}章: {props.pageEntry.title}
97+
{pageEntry.index}章: {pageEntry.title}
9598
</Heading>
9699
<div />
97100
{dynamicMdContent.map((section, index) => (
@@ -104,17 +107,18 @@ export function PageContent(props: PageContentProps) {
104107
}}
105108
>
106109
{/* ドキュメントのコンテンツ */}
107-
<StyledMarkdown
108-
content={section.rawContent.replace(
109-
/-repl\s*\n/,
110-
`-repl:${section.id}\n`
111-
)}
112-
/>
110+
<StyledMarkdown content={section.rawContent} />
113111
</div>
114112
<div>
115113
{/* 右側に表示するチャット履歴欄 */}
116114
{chatHistories
117-
.filter((c) => c.sectionId === section.id)
115+
.filter(
116+
(c) =>
117+
c.sectionId === section.id ||
118+
// 対象のセクションが存在しないものは、introセクション(index=0)にフォールバックする
119+
(index === 0 &&
120+
dynamicMdContent.every((sec) => c.sectionId !== sec.id))
121+
)
118122
.map(({ chatId, messages }) => (
119123
<div
120124
key={chatId}
@@ -150,9 +154,8 @@ export function PageContent(props: PageContentProps) {
150154
// replがz-10を使用することからそれの上にするためz-20
151155
<div className="fixed bottom-4 right-4 left-4 lg:left-84 z-20">
152156
<ChatForm
153-
documentContent={props.documentContent}
157+
path={path}
154158
sectionContent={dynamicMdContent}
155-
docs_id={props.docs_id}
156159
close={() => setIsFormVisible(false)}
157160
/>
158161
</div>

0 commit comments

Comments
 (0)