Skip to content

Commit c18dab0

Browse files
authored
Merge pull request #224 from ut-code/utcodelearn-term
ut.code(); LearnのremarkTermを実装し、キーワードのポップアップ・リンク
2 parents 2fc8b20 + 3427d9a commit c18dab0

30 files changed

Lines changed: 1250 additions & 273 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
# documentation
77
/public/docs/**/sections.*
8+
/public/docs/**/termDefinitions.*
89
/public/docs/languages.*
910
/public/docs/revisions-dev.yml
1011
/public/docs/commits-dev.yml

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ Cloudflare Worker のビルドログとステータス表示が見れますが
101101
- このセクションに関連する、想定される質問例。
102102
# question: 自体を省略すると、GitHub ActionがGeminiを呼び出して質問例を生成しpushします。
103103
# 質問例が不要な場合は question: [] にしてください。
104+
term:
105+
- キーワード
106+
- きーわーど
107+
# 他のページで [[キーワード]] と書いた場合に、このセクションの内容がポップアップで表示される。
104108
```
105109
* コード例はそれが配置されているセクションの内容と関連するようにする。
106110
````md
@@ -185,7 +189,38 @@ Cloudflare Worker のビルドログとステータス表示が見れますが
185189
````
186190
- 練習問題のファイル名は不都合がなければ `practice(章番号)_(問題番号).拡張子` で統一。空でもよいのでファイルコードブロックとexecコードブロックを置く
187191

188-
## markdown仕様
192+
### Alert
193+
194+
GitHubのalert記法の、`NOTE` `TIP` `WARNING` `CAUTION` が使えます。
195+
`IMPORTANT` はwarningに置き換えられます。
196+
197+
```
198+
> [!NOTE]
199+
> Highlights information that users should take into account, even when skimming.
200+
```
201+
202+
### 内部リンク
203+
204+
```
205+
[[用語]]
206+
```
207+
と書くと、`term: - 用語` が定義されている別のセクションへのリンクになり、ポップアップでその内容も表示されます。
208+
209+
markdown書式も利用可能です。 `[[ほげ**ふが**]]` は `term: - ほげふが` へのリンクになり、表示は「<ins>ほげ**ふが**</ins>」になります。ただし `**[[ふが]]**` はリンクの色が優先されるのに対し `[[**ふが**]]` は強調の色が優先されます。
210+
211+
```
212+
[[./1-foo]]
213+
[[./1]]
214+
```
215+
と書くと、同じ言語のページID `1-foo` またはindex `1` (ページのindexはindex.ymlの記述順で0-based) へのリンクになり、表示は「<ins>第1章</ins>」になります
216+
217+
```
218+
[[./prev]]
219+
[[./next]]
220+
```
221+
と書くと、同じ言語の前のページ・次のページへのリンクになり、表示は「<ins>第1章</ins>」(実際の章番号)になります
222+
223+
### コード実行環境仕様
189224
190225
実行環境の説明は ./packages/runtime/README.md を参照
191226

app/(docs)/@chat/chat/[chatId]/chatArea.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { ChatAreaStateUpdater } from "@/(docs)/chatAreaState";
44
import { useStreamingChatContext } from "@/(docs)/streamingChatContext";
55
import { deleteChatAction } from "@/actions/deleteChat";
66
import { ChatWithMessages } from "@/lib/chatHistory";
7-
import { LanguageEntry, MarkdownSection, PageEntry } from "@/lib/docs";
7+
import { LangId, MarkdownSection, PageSlug } from "@/lib/docs";
88
import { Heading } from "@/markdown/heading";
99
import { StyledMarkdown } from "@/markdown/markdown";
10+
import { usePagesListForLang } from "@/pagesListContext";
1011
import clsx from "clsx";
1112
import Link from "next/link";
1213
import { useRouter } from "next/navigation";
@@ -70,12 +71,15 @@ export function ChatAreaContainer(props: {
7071
interface Props {
7172
chatId: string;
7273
chatData: ChatWithMessages;
73-
targetLang: LanguageEntry | undefined;
74-
targetPage: PageEntry | undefined;
74+
langId: LangId;
75+
pageSlug: PageSlug;
7576
targetSection: MarkdownSection | undefined;
7677
}
7778
export function ChatAreaContent(props: Props) {
78-
const { chatId, chatData, targetLang, targetPage, targetSection } = props;
79+
const { chatId, chatData, langId, pageSlug, targetSection } = props;
80+
81+
const langEntry = usePagesListForLang(langId);
82+
const pageEntry = langEntry?.pages.find((p) => p.slug === pageSlug);
7983

8084
const messagesAndDiffs = [
8185
...chatData.messages.map((msg) => ({ type: "message" as const, ...msg })),
@@ -97,13 +101,13 @@ export function ChatAreaContent(props: Props) {
97101
<div className="flex-none breadcrumbs text-sm">
98102
<ul className="flex-wrap">
99103
<li>
100-
<Link href={`/${targetLang?.id}/${targetLang?.pages[0].slug}`}>
101-
{targetLang?.name}
104+
<Link href={`/${langId}/${langEntry?.pages[0].slug}`}>
105+
{langEntry?.name}
102106
</Link>
103107
</li>
104108
<li>
105109
<Link href={`/${chatData.section.pagePath}`}>
106-
{targetPage?.index}. {targetPage?.name}
110+
{pageEntry?.index}. {pageEntry?.name}
107111
</Link>
108112
</li>
109113
<li>

app/(docs)/@chat/chat/[chatId]/page.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import {
44
getChatOne,
55
initContext,
66
} from "@/lib/chatHistory";
7-
import { getMarkdownSections, getPagesList } from "@/lib/docs";
7+
import {
8+
getMarkdownSections,
9+
LangId,
10+
PageSlug,
11+
} from "@/lib/docs";
812
import { ChatAreaContainer, ChatAreaContent } from "./chatArea";
913
import { cacheLife, cacheTag } from "next/cache";
1014
import { isCloudflare } from "@/lib/detectCloudflare";
@@ -29,26 +33,20 @@ export default async function ChatPage({
2933
);
3034
}
3135

32-
const pagesList = await getPagesList();
33-
const targetLang = pagesList.find(
34-
(lang) => lang.id === chatData.section.pagePath.split("/")[0]
35-
);
36-
const targetPage = targetLang?.pages.find(
37-
(page) => page.slug === chatData.section.pagePath.split("/")[1]
38-
);
39-
const sections =
40-
targetLang && targetPage
41-
? await getMarkdownSections(targetLang.id, targetPage.slug)
42-
: [];
36+
const [langId, pageSlug] = chatData.section.pagePath.split("/") as [
37+
LangId,
38+
PageSlug,
39+
];
40+
const sections = await getMarkdownSections(langId, pageSlug);
4341
const targetSection = sections.find((sec) => sec.id === chatData.sectionId);
4442

4543
return (
4644
<ChatAreaContainer chatId={chatId}>
4745
<ChatAreaContent
4846
chatId={chatId}
4947
chatData={chatData}
50-
targetLang={targetLang}
51-
targetPage={targetPage}
48+
langId={langId}
49+
pageSlug={pageSlug}
5250
targetSection={targetSection}
5351
/>
5452
</ChatAreaContainer>

app/(docs)/@docs/[lang]/[pageId]/page.tsx

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
} from "@/lib/chatHistory";
1010
import {
1111
getMarkdownSections,
12-
getPagesList,
12+
getPagesListForLang,
13+
getTermDefinitions,
1314
LangId,
1415
PagePath,
1516
PageSlug,
@@ -18,15 +19,15 @@ import { cacheLife, cacheTag } from "next/cache";
1819
import { isCloudflare } from "@/lib/detectCloudflare";
1920
import { DocsAutoRedirect } from "./autoRedirect";
2021
import { dateReviver } from "@/lib/dateReviver";
22+
import { TermDefinitionProvider } from "@/markdown/term";
2123

2224
export async function generateMetadata({
2325
params,
2426
}: {
2527
params: Promise<{ lang: LangId; pageId: PageSlug }>;
2628
}): Promise<Metadata> {
2729
const { lang, pageId } = await params;
28-
const pagesList = await getPagesList();
29-
const langEntry = pagesList.find((l) => l.id === lang);
30+
const langEntry = await getPagesListForLang(lang);
3031
const pageEntry = langEntry?.pages.find((p) => p.slug === pageId);
3132
if (!langEntry || !pageEntry) notFound();
3233

@@ -45,15 +46,6 @@ export default async function Page({
4546
params: Promise<{ lang: LangId; pageId: PageSlug }>;
4647
}) {
4748
const { lang, pageId } = await params;
48-
const pagesList = await getPagesList();
49-
const langEntry = pagesList.find((l) => l.id === lang);
50-
const pageEntryIndex =
51-
langEntry?.pages.findIndex((p) => p.slug === pageId) ?? -1;
52-
const pageEntry = langEntry?.pages[pageEntryIndex];
53-
if (!langEntry || !pageEntry) notFound();
54-
55-
const prevPage = langEntry.pages[pageEntryIndex - 1];
56-
const nextPage = langEntry.pages[pageEntryIndex + 1];
5749

5850
// server componentなのでuseMemoいらない
5951
const path = { lang: lang, page: pageId };
@@ -62,17 +54,23 @@ export default async function Page({
6254
const context = await initContext();
6355
const chatHistories = await getChatFromCache(path, context.userId);
6456

57+
const termDefinitions = await getTermDefinitions(lang);
58+
6559
return (
6660
<>
67-
<PageContent
68-
chatHistories={chatHistories}
69-
splitMdContent={sections}
70-
langEntry={langEntry}
71-
pageEntry={pageEntry}
72-
prevPage={prevPage}
73-
nextPage={nextPage}
74-
path={path}
75-
/>
61+
<TermDefinitionProvider
62+
termDefinitions={termDefinitions}
63+
lang={lang}
64+
page={pageId}
65+
>
66+
<PageContent
67+
chatHistories={chatHistories}
68+
splitMdContent={sections}
69+
langId={lang}
70+
pageSlug={pageId}
71+
path={path}
72+
/>
73+
</TermDefinitionProvider>
7674
<DocsAutoRedirect path={path} />
7775
</>
7876
);

app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,38 @@ import clsx from "clsx";
88
import { PageTransition } from "./pageTransition";
99
import {
1010
DynamicMarkdownSection,
11-
LanguageEntry,
11+
LangId,
1212
MarkdownSection,
13-
PageEntry,
1413
PagePath,
14+
PageSlug,
1515
SectionId,
1616
} from "@/lib/docs";
1717
import { Heading } from "@/markdown/heading";
1818
import Link from "next/link";
1919
import { useChatId } from "@/(docs)/chatAreaState";
2020
import { ChatWithMessages } from "@/lib/chatHistory";
21+
import { usePagesListForLang } from "@/pagesListContext";
2122

2223
interface PageContentProps {
2324
splitMdContent: MarkdownSection[];
24-
langEntry: LanguageEntry;
25-
pageEntry: PageEntry;
26-
prevPage?: PageEntry;
27-
nextPage?: PageEntry;
25+
langId: LangId;
26+
pageSlug: PageSlug;
2827
path: PagePath;
2928
chatHistories: ChatWithMessages[];
3029
}
3130
export function PageContent(props: PageContentProps) {
3231
const { setSidebarMdContent } = useSidebarMdContext();
33-
const { splitMdContent, pageEntry, path, chatHistories } = props;
32+
const { splitMdContent, langId, pageSlug, path, chatHistories } = props;
33+
34+
const langEntry = usePagesListForLang(langId);
35+
const pageEntryIndex =
36+
langEntry?.pages.findIndex((p) => p.slug === pageSlug) ?? -1;
37+
const pageEntry = langEntry?.pages[pageEntryIndex];
38+
const prevPage = langEntry?.pages[pageEntryIndex - 1];
39+
const nextPage = langEntry?.pages[pageEntryIndex + 1];
3440

3541
const [sectionInView, setSectionInView] = useState<boolean[]>([]);
36-
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
42+
const sectionRefs = useRef<Array<HTMLElement | null>>([]);
3743
useEffect(() => {
3844
const handleScroll = () => {
3945
setSectionInView((sectionInView) => {
@@ -143,12 +149,12 @@ export function PageContent(props: PageContentProps) {
143149
}}
144150
>
145151
<Heading className="max-w-docs" level={1}>
146-
{pageEntry.index}章: {pageEntry.title}
152+
{pageEntry?.index}章: {pageEntry?.title}
147153
</Heading>
148154
<div />
149155
{dynamicMdContent.map((section, index) => (
150156
<Fragment key={section.id}>
151-
<div
157+
<section
152158
className="min-w-1/2 max-w-docs text-justify"
153159
id={section.id} // 目次からaタグで飛ぶために必要
154160
ref={(el) => {
@@ -159,8 +165,9 @@ export function PageContent(props: PageContentProps) {
159165
<StyledMarkdown
160166
content={section.replacedContent}
161167
replacedRange={section.replacedRange}
168+
interactive
162169
/>
163-
</div>
170+
</section>
164171
<div>
165172
<ChatListForSection
166173
sectionId={section.id}
@@ -172,8 +179,8 @@ export function PageContent(props: PageContentProps) {
172179
))}
173180
<PageTransition
174181
lang={path.lang}
175-
prevPage={props.prevPage}
176-
nextPage={props.nextPage}
182+
prevPage={prevPage}
183+
nextPage={nextPage}
177184
/>
178185
<div />
179186
</div>
@@ -183,7 +190,7 @@ export function PageContent(props: PageContentProps) {
183190
<div className="fixed bottom-4 right-4 left-4 has-sidebar:left-[calc(var(--container-sidebar)+1rem)] z-40">
184191
<ChatForm
185192
path={path}
186-
langName={props.langEntry.name}
193+
langName={langEntry?.name ?? ""}
187194
sectionContent={dynamicMdContent}
188195
close={() => setIsFormVisible(false)}
189196
/>

app/api/chat/route.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import {
1010
DynamicMarkdownSectionSchema,
1111
getMarkdownSections,
12-
getPagesList,
12+
getPagesListForLang,
1313
introSectionId,
1414
PagePathSchema,
1515
SectionId,
@@ -57,8 +57,7 @@ export async function POST(request: NextRequest) {
5757
execResults,
5858
} = parseResult.data;
5959

60-
const pagesList = await getPagesList();
61-
const langEntry = pagesList.find((lang) => lang.id === path.lang);
60+
const langEntry = await getPagesListForLang(path.lang);
6261
const langName = langEntry?.name ?? path.lang;
6362
let targetPath = path;
6463
let targetSectionContent = sectionContent;

0 commit comments

Comments
 (0)