Skip to content

Commit 32dc358

Browse files
committed
id周りをbranded typeにする
1 parent 5c579b5 commit 32dc358

File tree

8 files changed

+126
-96
lines changed

8 files changed

+126
-96
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +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-
langName: string;
17+
path: PagePath;
1718
sectionContent: DynamicMarkdownSection[];
1819
close: () => void;
1920
}
2021

21-
export function ChatForm({ langName, sectionContent, close }: ChatFormProps) {
22+
export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
2223
// const [messages, updateChatHistory] = useChatHistory(sectionId);
2324
const [inputValue, setInputValue] = useState("");
2425
const [isLoading, setIsLoading] = useState(false);
@@ -74,7 +75,7 @@ export function ChatForm({ langName, sectionContent, close }: ChatFormProps) {
7475
// }
7576

7677
const result = await askAI({
77-
langName,
78+
path,
7879
userQuestion,
7980
sectionContent,
8081
replOutputs,

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-
lang={lang}
58-
pageId={pageId}
59-
langName={langEntry.name}
60+
path={path}
6061
/>
6162
</ChatHistoryProvider>
6263
);

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +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-
langName: string;
25+
path: PagePath;
2326
}
2427
export function PageContent(props: PageContentProps) {
2528
const { setSidebarMdContent } = useSidebarMdContext();
29+
const { splitMdContent, langEntry, pageEntry, path } = props;
2630

2731
// SSR用のローカルstate
2832
const [dynamicMdContent, setDynamicMdContent] = useState<
2933
DynamicMarkdownSection[]
3034
>(
31-
props.splitMdContent.map((section) => ({
35+
splitMdContent.map((section) => ({
3236
...section,
3337
inView: false,
3438
}))
3539
);
3640

3741
useEffect(() => {
3842
// props.splitMdContentが変わったときにローカルstateとcontextの両方を更新
39-
const newContent = props.splitMdContent.map((section) => ({
43+
const newContent = splitMdContent.map((section) => ({
4044
...section,
4145
inView: false,
4246
}));
4347
setDynamicMdContent(newContent);
44-
setSidebarMdContent(props.lang, props.pageId, newContent);
45-
}, [props.splitMdContent, props.lang, props.pageId, setSidebarMdContent]);
48+
setSidebarMdContent(path, newContent);
49+
}, [splitMdContent, path, setSidebarMdContent]);
4650

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

7074
// ローカルstateとcontextの両方を更新
7175
setDynamicMdContent(updateContent);
72-
setSidebarMdContent(props.lang, props.pageId, updateContent);
76+
setSidebarMdContent(path, updateContent);
7377
};
7478
window.addEventListener("scroll", handleScroll);
7579
handleScroll();
7680
return () => {
7781
window.removeEventListener("scroll", handleScroll);
7882
};
79-
}, [setSidebarMdContent, props.lang, props.pageId]);
83+
}, [setSidebarMdContent, path]);
8084

8185
const [isFormVisible, setIsFormVisible] = useState(false);
8286

@@ -144,7 +148,7 @@ export function PageContent(props: PageContentProps) {
144148
// replがz-10を使用することからそれの上にするためz-20
145149
<div className="fixed bottom-4 right-4 left-4 lg:left-84 z-20">
146150
<ChatForm
147-
langName={props.langName}
151+
path={path}
148152
sectionContent={dynamicMdContent}
149153
close={() => setIsFormVisible(false)}
150154
/>

app/actions/chatActions.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { generateContent } from "./gemini";
55
import { DynamicMarkdownSection } from "../[lang]/[pageId]/pageContent";
66
import { ReplCommand, ReplOutput } from "../terminal/repl";
77
import { addChat, ChatWithMessages } from "@/lib/chatHistory";
8+
import { getPagesList, introSectionId, PagePath, SectionId } from "@/lib/docs";
89

910
type ChatResult =
1011
| {
@@ -17,7 +18,7 @@ type ChatResult =
1718
};
1819

1920
type ChatParams = {
20-
langName: string;
21+
path: PagePath;
2122
userQuestion: string;
2223
sectionContent: DynamicMarkdownSection[];
2324
replOutputs: Record<string, ReplCommand[]>;
@@ -36,14 +37,17 @@ export async function askAI(params: ChatParams): Promise<ChatResult> {
3637
// }
3738

3839
const {
39-
langName,
40+
path,
4041
userQuestion,
4142
sectionContent,
4243
replOutputs,
4344
files,
4445
execResults,
4546
} = params;
4647

48+
const pagesList = await getPagesList();
49+
const langName = pagesList.find((lang) => lang.id === path.lang)?.name;
50+
4751
const prompt: string[] = [];
4852

4953
prompt.push(`あなたは${langName}言語のチュートリアルの講師をしています。`);
@@ -138,7 +142,7 @@ export async function askAI(params: ChatParams): Promise<ChatResult> {
138142
prompt.push(
139143
" - ユーザーの質問がドキュメントのどのセクションとも直接的に関連しない場合は空白でも良いです。"
140144
);
141-
prompt.push("- 2行目は水平線 --- を出力してください。")
145+
prompt.push("- 2行目は水平線 --- を出力してください。");
142146
prompt.push(
143147
"- それ以降の行に、ドキュメントの内容に基づいて、ユーザーに伝える回答をMarkdown形式で記述してください。"
144148
);
@@ -150,7 +154,9 @@ export async function askAI(params: ChatParams): Promise<ChatResult> {
150154
" - 回答内でコードブロックを使用する際は ```言語名 としてください。" +
151155
"ドキュメント内では ```言語名-repl や ```言語名:ファイル名 、 ```言語名-exec:ファイル名 などが登場しますが、ユーザーへの回答ではこれらの記法は使用しないでください。"
152156
);
153-
prompt.push(" - 水平線(---)はシステムが区切りとして認識するので、ユーザーへの回答中に水平線を使用することはできません。");
157+
prompt.push(
158+
" - 水平線(---)はシステムが区切りとして認識するので、ユーザーへの回答中に水平線を使用することはできません。"
159+
);
154160
console.log(prompt);
155161

156162
try {
@@ -159,7 +165,10 @@ export async function askAI(params: ChatParams): Promise<ChatResult> {
159165
if (!text) {
160166
throw new Error("AIからの応答が空でした");
161167
}
162-
const targetSectionId = text.split(/-{3,}/)[0].trim();
168+
let targetSectionId = text.split(/-{3,}/)[0].trim() as SectionId;
169+
if (!targetSectionId) {
170+
targetSectionId = introSectionId(path);
171+
}
163172
const responseMessage = text.split(/-{3,}/)[1].trim();
164173
const newChat = await addChat(targetSectionId, [
165174
{ role: "user", content: userQuestion },

app/lib/chatHistory.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Auth } from "better-auth";
99
import { revalidateTag, unstable_cacheLife } from "next/cache";
1010
import { isCloudflare } from "./detectCloudflare";
1111
import { unstable_cacheTag } from "next/cache";
12+
import { PagePath, SectionId } from "./docs";
1213

1314
export interface CreateChatMessage {
1415
role: "user" | "ai" | "error";
@@ -17,6 +18,9 @@ export interface CreateChatMessage {
1718

1819
// cacheに使うキーで、実際のURLではない
1920
const CACHE_KEY_BASE = "https://my-code.utcode.net/chatHistory";
21+
function cacheKeyForPage(path: PagePath, userId: string) {
22+
return `${CACHE_KEY_BASE}/getChat?path=${path.lang}/${path.page}&userId=${userId}`;
23+
}
2024

2125
interface Context {
2226
drizzle: Awaited<ReturnType<typeof getDrizzle>>;
@@ -50,7 +54,7 @@ export async function initContext(ctx?: Partial<Context>): Promise<Context> {
5054
}
5155

5256
export async function addChat(
53-
sectionId: string,
57+
sectionId: SectionId,
5458
messages: CreateChatMessage[],
5559
context?: Partial<Context>
5660
) {
@@ -77,15 +81,13 @@ export async function addChat(
7781
)
7882
.returning();
7983

80-
revalidateTag(`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`);
84+
revalidateTag(cacheKeyForPage({}, userId));
8185
if (isCloudflare()) {
8286
const cache = await caches.open("chatHistory");
8387
console.log(
84-
`deleting cache for chatHistory/getChat for user ${userId} and docs ${docsId}`
85-
);
86-
await cache.delete(
87-
`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`
88+
`deleting cache for chatHistory/getChat for user ${userId} and docs ${lang}/${page}`
8889
);
90+
await cache.delete(cacheKeyForPage({}, userId));
8991
}
9092

9193
return {
@@ -97,7 +99,7 @@ export async function addChat(
9799
export type ChatWithMessages = Awaited<ReturnType<typeof addChat>>;
98100

99101
export async function getChat(
100-
docsId: string,
102+
path: PagePath,
101103
context?: Partial<Context>
102104
): Promise<ChatWithMessages[]> {
103105
const { drizzle, userId } = await initContext(context);
@@ -118,35 +120,30 @@ export async function getChat(
118120
if (isCloudflare()) {
119121
const cache = await caches.open("chatHistory");
120122
await cache.put(
121-
`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`,
123+
cacheKeyForPage(path, userId),
122124
new Response(JSON.stringify(chats), {
123125
headers: { "Cache-Control": "max-age=86400, s-maxage=86400" },
124126
})
125127
);
126128
}
127129
return chats;
128130
}
129-
export async function getChatFromCache(docsId: string, context: Context) {
131+
export async function getChatFromCache(path: PagePath, context: Context) {
130132
"use cache";
131133
unstable_cacheLife("days");
132134

133135
// cacheされる関数の中でheader()にはアクセスできない。
134136
// なので外でinitContext()を呼んだものを引数に渡す必要がある。
135137
// しかし、drizzleオブジェクトは外から渡せないのでgetChatの中で改めてinitContext()を呼んでdrizzleだけ再初期化している
136138
const { auth, userId } = context;
137-
unstable_cacheTag(
138-
`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`
139-
);
140-
141139
if (!userId) {
142140
return [];
143141
}
142+
unstable_cacheTag(cacheKeyForPage(path, userId));
144143

145144
if (isCloudflare()) {
146145
const cache = await caches.open("chatHistory");
147-
const cachedResponse = await cache.match(
148-
`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`
149-
);
146+
const cachedResponse = await cache.match(cacheKeyForPage(path, userId));
150147
if (cachedResponse) {
151148
console.log("Cache hit for chatHistory/getChat");
152149
const data = (await cachedResponse.json()) as ChatWithMessages[];
@@ -155,7 +152,7 @@ export async function getChatFromCache(docsId: string, context: Context) {
155152
console.log("Cache miss for chatHistory/getChat");
156153
}
157154
}
158-
return await getChat(docsId, { auth, userId });
155+
return await getChat(path, { auth, userId });
159156
}
160157

161158
export async function migrateChatUser(oldUserId: string, newUserId: string) {

0 commit comments

Comments
 (0)