Skip to content

Commit f8dbb41

Browse files
authored
Merge pull request #101 from ut-code/update-description
トップページの説明を更新, メタデータ設定, テーマカラー変更
2 parents 59ce490 + 250590a commit f8dbb41

26 files changed

+863
-299
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ export function ChatForm({
104104

105105
return (
106106
<form
107-
className="border border-2 border-secondary shadow-lg rounded-lg bg-base-100"
107+
className="border border-2 border-secondary shadow-lg rounded-box bg-base-100"
108108
style={{
109109
width: "100%",
110110
textAlign: "center",
111111
}}
112112
onSubmit={handleSubmit}
113113
>
114114
<textarea
115-
className="textarea textarea-ghost textarea-md rounded-lg"
115+
className="textarea textarea-ghost textarea-md rounded-box"
116116
placeholder={
117117
"質問を入力してください" +
118118
(exampleData
@@ -138,7 +138,7 @@ export function ChatForm({
138138
}}
139139
>
140140
<button
141-
className="btn btn-soft btn-secondary rounded-full"
141+
className="btn btn-soft btn-primary rounded-full"
142142
onClick={close}
143143
type="button"
144144
>
@@ -158,7 +158,7 @@ export function ChatForm({
158158
)}
159159
<button
160160
type="submit"
161-
className="btn btn-soft btn-circle btn-accent border-2 border-accent rounded-full"
161+
className="btn btn-soft btn-circle btn-secondary"
162162
title="送信"
163163
disabled={isLoading}
164164
>

app/[docs_id]/markdown.tsx

Lines changed: 37 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import Markdown, { Components } from "react-markdown";
1+
import Markdown, { Components, ExtraProps } from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import { EditorComponent, getAceLang } from "../terminal/editor";
44
import { ExecFile } from "../terminal/exec";
5-
import { useChangeTheme } from "./themeToggle";
6-
import {
7-
tomorrow,
8-
atomOneDark,
9-
} from "react-syntax-highlighter/dist/esm/styles/hljs";
10-
import { ReactNode } from "react";
5+
import { JSX, ReactNode } from "react";
116
import { getRuntimeLang } from "@/terminal/runtime";
127
import { ReplTerminal } from "@/terminal/repl";
13-
import dynamic from "next/dynamic";
14-
// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
15-
const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter"), { ssr: false });
8+
import {
9+
getSyntaxHighlighterLang,
10+
MarkdownLang,
11+
StyledSyntaxHighlighter,
12+
} from "./styledSyntaxHighlighter";
1613

1714
export function StyledMarkdown({ content }: { content: string }) {
1815
return (
@@ -40,14 +37,14 @@ const components: Components = {
4037
li: ({ node, ...props }) => <li className="my-1" {...props} />,
4138
a: ({ node, ...props }) => <a className="link link-info" {...props} />,
4239
strong: ({ node, ...props }) => (
43-
<strong className="text-primary dark:text-secondary" {...props} />
40+
<strong className="text-primary" {...props} />
4441
),
4542
table: ({ node, ...props }) => (
46-
<div className="w-max max-w-full overflow-x-auto mx-auto my-2 rounded-lg border border-base-content/5 shadow-sm">
43+
<div className="w-max max-w-full overflow-x-auto mx-auto my-2 rounded-box border border-current/20 shadow-sm">
4744
<table className="table w-max" {...props} />
4845
</div>
4946
),
50-
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
47+
hr: ({ node, ...props }) => <hr className="border-accent my-4" {...props} />,
5148
pre: ({ node, ...props }) => props.children,
5249
code: ({ node, className, ref, style, ...props }) => (
5350
<CodeComponent {...{ node, className, ref, style, ...props }} />
@@ -84,20 +81,12 @@ function CodeComponent({
8481
ref,
8582
style,
8683
...props
87-
}: {
88-
node: unknown;
89-
className?: string;
90-
ref?: unknown;
91-
style?: unknown;
92-
[key: string]: unknown;
93-
}) {
94-
const theme = useChangeTheme();
95-
const codetheme = theme === "tomorrow" ? tomorrow : atomOneDark;
84+
}: JSX.IntrinsicElements["code"] & ExtraProps) {
9685
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
9786
className || ""
9887
);
9988
if (match) {
100-
const runtimeLang = getRuntimeLang(match[1]);
89+
const runtimeLang = getRuntimeLang(match[1] as MarkdownLang | undefined);
10190
if (match[2] === "-exec" && match[3]) {
10291
/*
10392
```python-exec:main.py
@@ -111,13 +100,11 @@ function CodeComponent({
111100
*/
112101
if (runtimeLang) {
113102
return (
114-
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
115-
<ExecFile
116-
language={runtimeLang}
117-
filenames={match[3].split(",")}
118-
content={String(props.children || "").replace(/\n$/, "")}
119-
/>
120-
</div>
103+
<ExecFile
104+
language={runtimeLang}
105+
filenames={match[3].split(",")}
106+
content={String(props.children || "").replace(/\n$/, "")}
107+
/>
121108
);
122109
}
123110
} else if (match[2] === "-repl") {
@@ -129,57 +116,45 @@ function CodeComponent({
129116
}
130117
if (runtimeLang) {
131118
return (
132-
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
133-
<ReplTerminal
134-
terminalId={match[3]}
135-
language={runtimeLang}
136-
initContent={String(props.children || "").replace(/\n$/, "")}
137-
/>
138-
</div>
119+
<ReplTerminal
120+
terminalId={match[3]}
121+
language={runtimeLang}
122+
initContent={String(props.children || "").replace(/\n$/, "")}
123+
/>
139124
);
140125
}
141126
} else if (match[3]) {
142127
// ファイル名指定がある場合、ファイルエディター
143-
const aceLang = getAceLang(match[1]);
128+
const aceLang = getAceLang(match[1] as MarkdownLang | undefined);
144129
return (
145-
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
146-
<EditorComponent
147-
language={aceLang}
148-
filename={match[3]}
149-
readonly={match[2] === "-readonly"}
150-
initContent={String(props.children || "").replace(/\n$/, "")}
151-
/>
152-
</div>
130+
<EditorComponent
131+
language={aceLang}
132+
filename={match[3]}
133+
readonly={match[2] === "-readonly"}
134+
initContent={String(props.children || "").replace(/\n$/, "")}
135+
/>
153136
);
154137
}
138+
const syntaxHighlighterLang = getSyntaxHighlighterLang(
139+
match[1] as MarkdownLang | undefined
140+
);
155141
return (
156-
<SyntaxHighlighter
157-
language={match[1]}
158-
PreTag="div"
159-
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
160-
style={codetheme}
161-
{...props}
162-
>
142+
<StyledSyntaxHighlighter language={syntaxHighlighterLang}>
163143
{String(props.children || "").replace(/\n$/, "")}
164-
</SyntaxHighlighter>
144+
</StyledSyntaxHighlighter>
165145
);
166146
} else if (String(props.children).includes("\n")) {
167147
// 言語指定なしコードブロック
168148
return (
169-
<SyntaxHighlighter
170-
PreTag="div"
171-
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
172-
style={codetheme}
173-
{...props}
174-
>
149+
<StyledSyntaxHighlighter language={undefined}>
175150
{String(props.children || "").replace(/\n$/, "")}
176-
</SyntaxHighlighter>
151+
</StyledSyntaxHighlighter>
177152
);
178153
} else {
179154
// inline
180155
return (
181156
<code
182-
className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm "
157+
className="bg-current/10 border border-current/20 px-1 py-0.5 mx-0.5 rounded-md"
183158
{...props}
184159
/>
185160
);

app/[docs_id]/page.tsx

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,75 @@
1+
import { Metadata } from "next";
12
import { notFound } from "next/navigation";
23
import { getCloudflareContext } from "@opennextjs/cloudflare";
34
import { readFile } from "node:fs/promises";
45
import { join } from "node:path";
5-
import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
6+
import { splitMarkdown } from "./splitMarkdown";
67
import { PageContent } from "./pageContent";
78
import { ChatHistoryProvider } from "./chatHistory";
89
import { getChatFromCache } from "@/lib/chatHistory";
10+
import { getLanguageName } from "@/pagesList";
911

10-
export default async function Page({
12+
async function getMarkdownContent(docs_id: string): Promise<string> {
13+
try {
14+
if (process.env.NODE_ENV === "development") {
15+
return await readFile(
16+
join(process.cwd(), "public", "docs", `${docs_id}.md`),
17+
"utf-8"
18+
);
19+
} else {
20+
const cfAssets = getCloudflareContext().env.ASSETS;
21+
const res = await cfAssets!.fetch(
22+
`https://assets.local/docs/${docs_id}.md`
23+
);
24+
if (!res.ok) {
25+
notFound();
26+
}
27+
return await res.text();
28+
}
29+
} catch (e) {
30+
console.error(e);
31+
notFound();
32+
}
33+
}
34+
35+
export async function generateMetadata({
1136
params,
1237
}: {
1338
params: Promise<{ docs_id: string }>;
14-
}) {
39+
}): Promise<Metadata> {
1540
const { docs_id } = await params;
41+
const mdContent = await getMarkdownContent(docs_id);
42+
const splitMdContent = splitMarkdown(mdContent);
1643

17-
let mdContent: Promise<string>;
18-
if (process.env.NODE_ENV === "development") {
19-
mdContent = readFile(
20-
join(process.cwd(), "public", "docs", `${docs_id}.md`),
21-
"utf-8"
22-
).catch((e) => {
23-
console.error(e);
24-
notFound();
25-
});
26-
} else {
27-
const cfAssets = getCloudflareContext().env.ASSETS;
28-
mdContent = cfAssets!
29-
.fetch(`https://assets.local/docs/${docs_id}.md`)
30-
.then(async (res) => {
31-
if (!res.ok) {
32-
notFound();
33-
}
34-
return res.text();
35-
})
36-
.catch((e) => {
37-
console.error(e);
38-
notFound();
39-
});
40-
}
44+
// 先頭の 第n章: を除いたものをタイトルとする
45+
const title = splitMdContent[0]?.title?.split(" ").slice(1).join(" ");
4146

42-
const splitMdContent: Promise<MarkdownSection[]> = mdContent.then((text) =>
43-
splitMarkdown(text)
44-
);
47+
const description = splitMdContent[0].content;
48+
49+
const chapter = docs_id.split("-")[1];
50+
51+
return {
52+
title: `${getLanguageName(docs_id)}-${chapter}. ${title}`,
53+
description,
54+
};
55+
}
56+
57+
export default async function Page({
58+
params,
59+
}: {
60+
params: Promise<{ docs_id: string }>;
61+
}) {
62+
const { docs_id } = await params;
4563

64+
const mdContent = getMarkdownContent(docs_id);
65+
const splitMdContent = mdContent.then((text) => splitMarkdown(text));
4666
const initialChatHistories = getChatFromCache(docs_id);
4767

4868
return (
49-
<ChatHistoryProvider initialChatHistories={await initialChatHistories} docs_id={docs_id}>
69+
<ChatHistoryProvider
70+
initialChatHistories={await initialChatHistories}
71+
docs_id={docs_id}
72+
>
5073
<PageContent
5174
documentContent={await mdContent}
5275
splitMdContent={await splitMdContent}

app/[docs_id]/pageContent.tsx

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

3-
import { useEffect, useRef, useState } from "react";
3+
import { Fragment, useEffect, useRef, useState } from "react";
44
import { MarkdownSection } from "./splitMarkdown";
55
import { ChatForm } from "./chatForm";
66
import { Heading, StyledMarkdown } from "./markdown";
@@ -90,10 +90,9 @@ export function PageContent(props: PageContentProps) {
9090
}}
9191
>
9292
{dynamicMdContent.map((section, index) => (
93-
<>
93+
<Fragment key={index}>
9494
<div
9595
className="max-w-200"
96-
key={`${index}-content`}
9796
id={`${index}`} // 目次からaタグで飛ぶために必要
9897
ref={(el) => {
9998
sectionRefs.current[index] = el;
@@ -103,14 +102,14 @@ export function PageContent(props: PageContentProps) {
103102
<Heading level={section.level}>{section.title}</Heading>
104103
<StyledMarkdown content={section.content} />
105104
</div>
106-
<div key={`${index}-chat`}>
105+
<div>
107106
{/* 右側に表示するチャット履歴欄 */}
108107
{chatHistories
109108
.filter((c) => c.sectionId === section.sectionId)
110109
.map(({ chatId, messages }) => (
111110
<div
112111
key={chatId}
113-
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-100"
112+
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-200"
114113
>
115114
<div className="max-h-60 overflow-y-auto">
116115
{messages.map((msg, index) => (
@@ -120,12 +119,10 @@ export function PageContent(props: PageContentProps) {
120119
>
121120
<div
122121
className={clsx(
123-
"chat-bubble p-1!",
124122
msg.role === "user" &&
125-
"bg-primary text-primary-content",
126-
msg.role === "ai" &&
127-
"bg-secondary-content dark:bg-neutral text-black dark:text-white",
128-
msg.role === "error" && "chat-bubble-error"
123+
"chat-bubble p-0.5! bg-secondary/30",
124+
msg.role === "ai" && "chat-bubble p-0.5!",
125+
msg.role === "error" && "text-error"
129126
)}
130127
style={{ maxWidth: "100%", wordBreak: "break-word" }}
131128
>
@@ -137,7 +134,7 @@ export function PageContent(props: PageContentProps) {
137134
</div>
138135
))}
139136
</div>
140-
</>
137+
</Fragment>
141138
))}
142139
{isFormVisible ? (
143140
// sidebarの幅が80であることからleft-84 (sidebar.tsx参照)

0 commit comments

Comments
 (0)