Skip to content

Commit 5740498

Browse files
committed
Merge remote-tracking branch 'origin/main' into terminal
2 parents 0279492 + 0b9b779 commit 5740498

File tree

5 files changed

+147
-114
lines changed

5 files changed

+147
-114
lines changed

app/[docs_id]/chatForm.tsx

Lines changed: 49 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function ChatForm() {
1010
const [inputValue, setInputValue] = useState("");
1111
const [response, setResponse] = useState("");
1212
const [isLoading, setIsLoading] = useState(false);
13+
const [isFormVisible, setIsFormVisible] = useState(false);
1314

1415
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
1516
e.preventDefault();
@@ -42,119 +43,72 @@ export function ChatForm() {
4243
};
4344
return (
4445
<>
45-
<style jsx>{`
46-
/* 簡単なCSSで見た目を整える(オプション) */
47-
.form-container {
48-
background-color: white;
49-
border-radius: 10px;
50-
box-shadow: 0 4px 8px rgba(67, 204, 216, 0.86);
51-
padding: 20px;
52-
width: 90%;
53-
max-width: 1000px;
54-
display: flex;
55-
flex-direction: column;
56-
}
57-
.input-area {
58-
border: 1px solid #ccc;
59-
border-radius: 8px;
60-
padding: 5px 15 px;
61-
margin-bottom: 15px;
62-
min-height: 150px; /* 入力欄の高さ */
63-
display: flex;
64-
}
65-
.text-input {
66-
border: none;
67-
outline: none;
68-
flex-grow: 1;
69-
font-size: 16px;
70-
resize: none; /* テキストエリアのリサイズを無効化 */
71-
overflow: auto;
72-
padding: 10px;
73-
}
74-
.controls {
75-
display: flex;
76-
align-items: center;
77-
justify-content: space-between;
78-
}
79-
.left-icons button {
80-
background: none;
81-
border: none;
82-
font-size: 24px;
83-
cursor: pointer;
84-
color: #555;
85-
margin-right: 15px;
86-
padding: 5px;
87-
}
88-
.left-icons button:hover {
89-
color: #000;
90-
}
91-
.left-icons span {
92-
font-size: 14px;
93-
vertical-align: middle;
94-
margin-left: 5px;
95-
color: #555;
96-
}
97-
.right-controls {
98-
display: flex;
99-
align-items: center;
100-
}
101-
.voice-icon button {
102-
background: none;
103-
border: none;
104-
font-size: 24px;
105-
cursor: pointer;
106-
color: #555;
107-
margin-right: 15px;
108-
padding: 5px;
109-
}
110-
.voice-icon button:hover {
111-
color: #000;
112-
}
113-
.send-button {
114-
background-color: #007bff; /* 青色の送信ボタン */
115-
color: white;
116-
border: none;
117-
border-radius: 50%; /* 丸いボタン */
118-
width: 40px;
119-
height: 40px;
120-
display: flex;
121-
justify-content: center;
122-
align-items: center;
123-
font-size: 20px;
124-
cursor: pointer;
125-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
126-
transition: background-color 0.3s ease;
127-
}
128-
.send-button:hover {
129-
background-color: #0056b3;
130-
}
131-
`}</style>
132-
133-
<form className="form-container" onSubmit={handleSubmit}>
134-
<div className="input-area">
46+
{isFormVisible && (
47+
<form className="border border-2 border-primary shadow-xl p-6 rounded-lg bg-base-100" style={{width:"100%", textAlign:"center", boxShadow:"-moz-initial"}} onSubmit={handleSubmit}>
48+
<h2 className="text-xl font-bold mb-4 text-left relative -top-2 font-mono h-2">
49+
AIへ質問
50+
</h2>
51+
<div className="input-area" style={{height:"80px"}}>
13552
<textarea
136-
className="text-input"
53+
className="textarea textarea-white textarea-md"
13754
placeholder="質問を入力してください"
55+
style={{width: "100%", height: "110px", resize: "none"}}
13856
value={inputValue}
13957
onChange={(e) => setInputValue(e.target.value)}
14058
disabled={isLoading}
14159
></textarea>
14260
</div>
143-
<div className="controls">
144-
<div className="left-icons"></div>
61+
<br />
62+
<div className="controls" style={{position:"relative", top:"22px", display:"flex", alignItems:"center", justifyContent:"space-between"}}>
63+
<div className="left-icons">
64+
<button
65+
className="btn btn-soft btn-secondary rounded-full"
66+
onClick={() => setIsFormVisible(false)}
67+
>
68+
69+
閉じる
70+
</button>
71+
</div>
14572
<div className="right-controls">
14673
<button
14774
type="submit"
148-
className="send-button"
75+
className="btn btn-soft btn-circle btn-primary rounded-full"
14976
title="送信"
77+
style={{marginTop:"10px"}}
15078
disabled={isLoading}
15179
>
15280
<span className="icon"></span>
15381
</button>
15482
</div>
15583
</div>
15684
</form>
157-
{response && <div className="response-container">{response}</div>}
85+
)}
86+
{!isFormVisible && (
87+
<button
88+
className="btn btn-soft btn-secondary rounded-full"
89+
onClick={() => setIsFormVisible(true)}
90+
>
91+
チャットを開く
92+
</button>
93+
)}
94+
95+
{response && (
96+
<article>
97+
<h3 className="text-lg font-semibold mb-2">AIの回答</h3>
98+
<div className="chat chat-start">
99+
<div className="chat-bubble chat-bubble-primary">
100+
<div className="response-container">{response}</div>
101+
</div>
102+
</div>
103+
</article>
104+
)}
105+
106+
{isLoading && (
107+
<div className="mt-2 text-l text-gray-500 animate-pulse">
108+
AIが考え中です…
109+
</div>
110+
)}
111+
158112
</>
159113
);
160114
}

app/[docs_id]/markdown.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Markdown, { Components } from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
44
import { PythonEmbeddedTerminal } from "../terminal/python/embedded";
5+
import { Heading } from "./section";
56

67
export function StyledMarkdown({ content }: { content: string }) {
78
return (
@@ -13,18 +14,12 @@ export function StyledMarkdown({ content }: { content: string }) {
1314

1415
// TailwindCSSがh1などのタグのスタイルを消してしまうので、手動でスタイルを指定する必要がある
1516
const components: Components = {
16-
h1: ({ node, ...props }) => (
17-
<h1 className="text-2xl font-bold my-4" {...props} />
18-
),
19-
h2: ({ node, ...props }) => (
20-
<h2 className="text-xl font-bold mt-4 mb-3 " {...props} />
21-
),
22-
h3: ({ node, ...props }) => (
23-
<h3 className="text-lg font-bold mt-4 mb-2" {...props} />
24-
),
25-
h4: ({ node, ...props }) => (
26-
<h4 className="text-base font-bold mt-3 mb-2" {...props} />
27-
),
17+
h1: ({ children }) => <Heading level={1}>{children}</Heading>,
18+
h2: ({ children }) => <Heading level={2}>{children}</Heading>,
19+
h3: ({ children }) => <Heading level={3}>{children}</Heading>,
20+
h4: ({ children }) => <Heading level={4}>{children}</Heading>,
21+
h5: ({ children }) => <Heading level={5}>{children}</Heading>,
22+
h6: ({ children }) => <Heading level={6}>{children}</Heading>,
2823
p: ({ node, ...props }) => <p className="mx-2 my-2" {...props} />,
2924
ul: ({ node, ...props }) => (
3025
<ul className="list-disc list-outside ml-6 my-2" {...props} />

app/[docs_id]/page.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { notFound } from "next/navigation";
2-
import { ChatForm } from "./chatForm";
3-
import { StyledMarkdown } from "./markdown";
42
import { getCloudflareContext } from "@opennextjs/cloudflare";
5-
import { readFile } from "node:fs/promises";
3+
import { readFile } from "node:fs/promises";
64
import { join } from "node:path";
5+
import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
6+
import { Section } from "./section";
77

88
export default async function Page({
99
params,
@@ -14,13 +14,13 @@ export default async function Page({
1414

1515
let mdContent: string;
1616
try {
17-
if (process.env.NODE_ENV === 'development') {
17+
if (process.env.NODE_ENV === "development") {
1818
mdContent = await readFile(
1919
join(process.cwd(), "public", "docs", `${docs_id}.md`),
2020
"utf-8"
2121
);
2222
} else {
23-
const cfAssets = getCloudflareContext().env.ASSETS;
23+
const cfAssets = getCloudflareContext().env.ASSETS;
2424
mdContent = await cfAssets!
2525
.fetch(`https://assets.local/docs/${docs_id}.md`)
2626
.then((res) => res.text());
@@ -30,10 +30,13 @@ export default async function Page({
3030
notFound();
3131
}
3232

33+
const splitMdContent: MarkdownSection[] = await splitMarkdown(mdContent);
34+
3335
return (
3436
<div className="p-4">
35-
<StyledMarkdown content={mdContent} />
36-
<ChatForm />
37+
{splitMdContent.map((section, index) => (
38+
<Section key={index} section={section} />
39+
))}
3740
</div>
3841
);
3942
}

app/[docs_id]/section.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import { ReactNode } from "react";
4+
import { type MarkdownSection } from "./splitMarkdown";
5+
import { StyledMarkdown } from "./markdown";
6+
import { ChatForm } from "./chatForm";
7+
8+
// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
9+
export function Section({ section }: { section: MarkdownSection }) {
10+
return (
11+
<div>
12+
<Heading level={section.level}>{section.title}</Heading>
13+
<StyledMarkdown content={section.content} />
14+
<ChatForm />
15+
</div>
16+
);
17+
}
18+
19+
export function Heading({ level, children }: { level: number; children: ReactNode }) {
20+
switch (level) {
21+
case 1:
22+
return <h1 className="text-2xl font-bold my-4">{children}</h1>;
23+
case 2:
24+
return <h2 className="text-xl font-bold mt-4 mb-3 ">{children}</h2>;
25+
case 3:
26+
return <h3 className="text-lg font-bold mt-4 mb-2">{children}</h3>;
27+
case 4:
28+
return <h4 className="text-base font-bold mt-3 mb-2">{children}</h4>;
29+
case 5:
30+
// TODO: これ以下は4との差がない (全体的に大きくする必要がある?)
31+
return <h5 className="text-base font-bold mt-3 mb-2">{children}</h5>;
32+
case 6:
33+
return <h6 className="text-base font-bold mt-3 mb-2">{children}</h6>;
34+
}
35+
}

app/[docs_id]/splitMarkdown.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use server";
2+
3+
import { unified } from "unified";
4+
import remarkParse from "remark-parse";
5+
import remarkGfm from "remark-gfm";
6+
7+
export interface MarkdownSection {
8+
level: number;
9+
title: string;
10+
content: string;
11+
}
12+
/**
13+
* Markdownコンテンツを見出しごとに分割し、
14+
* 見出しのレベルとタイトル、内容を含むオブジェクトの配列を返す。
15+
*/
16+
export async function splitMarkdown(
17+
content: string
18+
): Promise<MarkdownSection[]> {
19+
const tree = unified().use(remarkParse).use(remarkGfm).parse(content);
20+
// console.log(tree.children.map(({ type, position }) => ({ type, position: JSON.stringify(position) })));
21+
const headingNodes = tree.children.filter((node) => node.type === "heading");
22+
const splitContent = content.split("\n");
23+
const sections: MarkdownSection[] = [];
24+
for (let i = 0; i < headingNodes.length; i++) {
25+
const startLine = headingNodes.at(i)?.position?.start.line;
26+
if (startLine === undefined) {
27+
continue;
28+
}
29+
let endLine: number | undefined = undefined;
30+
for (let j = i + 1; j < headingNodes.length; j++) {
31+
if (headingNodes.at(j)?.position?.start.line !== undefined) {
32+
endLine = headingNodes.at(j)!.position!.start.line;
33+
break;
34+
}
35+
}
36+
sections.push({
37+
title: splitContent[startLine - 1].replace(/#+\s*/, "").trim(),
38+
content: splitContent
39+
.slice(startLine - 1 + 1, endLine ? endLine - 1 : undefined)
40+
.join("\n")
41+
.trim(),
42+
level: headingNodes.at(i)!.depth,
43+
});
44+
}
45+
return sections;
46+
}

0 commit comments

Comments
 (0)