Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6bc5eba
トップページの説明を更新
na-trium-144 Nov 10, 2025
50a6d50
ページのタイトルを設定
na-trium-144 Nov 10, 2025
a88cfb1
アイコン追加
na-trium-144 Nov 11, 2025
3101e01
幅8のロゴをsidebarに押し込む
na-trium-144 Nov 11, 2025
12b722a
manifestのタイトルをpage.tsxからもってこない
na-trium-144 Nov 11, 2025
a61834e
テーマをカスタマイズ
na-trium-144 Nov 14, 2025
cce6754
コードのフォントを小さくするのをやめる
na-trium-144 Nov 14, 2025
1351f30
インラインコードの背景色を文字色に合わせる
na-trium-144 Nov 14, 2025
6423668
背景色のバグを修正
na-trium-144 Nov 14, 2025
43b68f6
タイトルをfont-monoに変更
na-trium-144 Nov 14, 2025
04f15bd
本文のフォントも固定してみた
na-trium-144 Nov 14, 2025
e08700d
トップページのmy.code();をfont-monoに
na-trium-144 Nov 14, 2025
587d6c3
navbarをblurつき半透明
na-trium-144 Nov 14, 2025
2406184
roundedの指定をテーマに合わせる
na-trium-144 Nov 14, 2025
37cc17b
エディターのサイズを修正、ブラウザーの1remにあわせる
na-trium-144 Nov 14, 2025
5549d23
一応tailwindデフォルトのemojiフォント指定を残しておく
na-trium-144 Nov 14, 2025
883cade
Noto Sans JP の別名を全部列挙
na-trium-144 Nov 14, 2025
04bc470
weight 700のcssをimportしてなかった
na-trium-144 Nov 14, 2025
0a1e582
システムのinconsolataはやっぱりダメ
na-trium-144 Nov 14, 2025
e338de0
フォントのフォールバックをめっちゃ指定した
na-trium-144 Nov 14, 2025
c5c0261
imgのalt
na-trium-144 Nov 14, 2025
f2d1de2
paramsをawait
na-trium-144 Nov 14, 2025
3f8028a
monoフォントのフォールバックを修正(気にしすぎ?)
na-trium-144 Nov 14, 2025
92c9dba
ダークテーマの色をtomorrowNightで統一
na-trium-144 Nov 14, 2025
86f8fbe
navbarにページタイトル
na-trium-144 Nov 15, 2025
0724ac6
ダークモードをちょっと明るくした
na-trium-144 Nov 17, 2025
935bc2a
コードエディターが間違ったテーマで表示される場合があるのを修正?
na-trium-144 Nov 17, 2025
3e77a4a
エディターと通常シンタックスハイライトの色をテーマに合わせて上書き
na-trium-144 Nov 17, 2025
5b114a8
completion邪魔かも
na-trium-144 Nov 17, 2025
9ac5b21
テーマ切替時のターミナルのテーマ設定を修正&色を調整
na-trium-144 Nov 17, 2025
8bdb978
MarkdownComponentの型指定修正&SyntaxHighlighterとその言語指定をまとめた
na-trium-144 Nov 17, 2025
af257ac
next/dynamicからreactのlazyに変更し、読み込み中もpreで内容を表示
na-trium-144 Nov 17, 2025
250590a
cursorとselectionの色を変更
na-trium-144 Nov 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/[docs_id]/chatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ export function ChatForm({

return (
<form
className="border border-2 border-secondary shadow-lg rounded-lg bg-base-100"
className="border border-2 border-secondary shadow-lg rounded-box bg-base-100"
style={{
width: "100%",
textAlign: "center",
}}
onSubmit={handleSubmit}
>
<textarea
className="textarea textarea-ghost textarea-md rounded-lg"
className="textarea textarea-ghost textarea-md rounded-box"
placeholder={
"質問を入力してください" +
(exampleData
Expand All @@ -138,7 +138,7 @@ export function ChatForm({
}}
>
<button
className="btn btn-soft btn-secondary rounded-full"
className="btn btn-soft btn-primary rounded-full"
onClick={close}
type="button"
>
Expand All @@ -158,7 +158,7 @@ export function ChatForm({
)}
<button
type="submit"
className="btn btn-soft btn-circle btn-accent border-2 border-accent rounded-full"
className="btn btn-soft btn-circle btn-secondary"
title="送信"
disabled={isLoading}
>
Expand Down
99 changes: 37 additions & 62 deletions app/[docs_id]/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import Markdown, { Components } from "react-markdown";
import Markdown, { Components, ExtraProps } from "react-markdown";
import remarkGfm from "remark-gfm";
import { EditorComponent, getAceLang } from "../terminal/editor";
import { ExecFile } from "../terminal/exec";
import { useChangeTheme } from "./themeToggle";
import {
tomorrow,
atomOneDark,
} from "react-syntax-highlighter/dist/esm/styles/hljs";
import { ReactNode } from "react";
import { JSX, ReactNode } from "react";
import { getRuntimeLang } from "@/terminal/runtime";
import { ReplTerminal } from "@/terminal/repl";
import dynamic from "next/dynamic";
// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter"), { ssr: false });
import {
getSyntaxHighlighterLang,
MarkdownLang,
StyledSyntaxHighlighter,
} from "./styledSyntaxHighlighter";

export function StyledMarkdown({ content }: { content: string }) {
return (
Expand Down Expand Up @@ -40,14 +37,14 @@ const components: Components = {
li: ({ node, ...props }) => <li className="my-1" {...props} />,
a: ({ node, ...props }) => <a className="link link-info" {...props} />,
strong: ({ node, ...props }) => (
<strong className="text-primary dark:text-secondary" {...props} />
<strong className="text-primary" {...props} />
),
table: ({ node, ...props }) => (
<div className="w-max max-w-full overflow-x-auto mx-auto my-2 rounded-lg border border-base-content/5 shadow-sm">
<div className="w-max max-w-full overflow-x-auto mx-auto my-2 rounded-box border border-current/20 shadow-sm">
<table className="table w-max" {...props} />
</div>
),
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
hr: ({ node, ...props }) => <hr className="border-accent my-4" {...props} />,
pre: ({ node, ...props }) => props.children,
code: ({ node, className, ref, style, ...props }) => (
<CodeComponent {...{ node, className, ref, style, ...props }} />
Expand Down Expand Up @@ -84,20 +81,12 @@ function CodeComponent({
ref,
style,
...props
}: {
node: unknown;
className?: string;
ref?: unknown;
style?: unknown;
[key: string]: unknown;
}) {
const theme = useChangeTheme();
const codetheme = theme === "tomorrow" ? tomorrow : atomOneDark;
}: JSX.IntrinsicElements["code"] & ExtraProps) {
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
className || ""
);
if (match) {
const runtimeLang = getRuntimeLang(match[1]);
const runtimeLang = getRuntimeLang(match[1] as MarkdownLang | undefined);
if (match[2] === "-exec" && match[3]) {
/*
```python-exec:main.py
Expand All @@ -111,13 +100,11 @@ function CodeComponent({
*/
if (runtimeLang) {
return (
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
<ExecFile
language={runtimeLang}
filenames={match[3].split(",")}
content={String(props.children || "").replace(/\n$/, "")}
/>
</div>
<ExecFile
language={runtimeLang}
filenames={match[3].split(",")}
content={String(props.children || "").replace(/\n$/, "")}
/>
);
}
} else if (match[2] === "-repl") {
Expand All @@ -129,57 +116,45 @@ function CodeComponent({
}
if (runtimeLang) {
return (
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
<ReplTerminal
terminalId={match[3]}
language={runtimeLang}
initContent={String(props.children || "").replace(/\n$/, "")}
/>
</div>
<ReplTerminal
terminalId={match[3]}
language={runtimeLang}
initContent={String(props.children || "").replace(/\n$/, "")}
/>
);
}
} else if (match[3]) {
// ファイル名指定がある場合、ファイルエディター
const aceLang = getAceLang(match[1]);
const aceLang = getAceLang(match[1] as MarkdownLang | undefined);
return (
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
<EditorComponent
language={aceLang}
filename={match[3]}
readonly={match[2] === "-readonly"}
initContent={String(props.children || "").replace(/\n$/, "")}
/>
</div>
<EditorComponent
language={aceLang}
filename={match[3]}
readonly={match[2] === "-readonly"}
initContent={String(props.children || "").replace(/\n$/, "")}
/>
);
}
const syntaxHighlighterLang = getSyntaxHighlighterLang(
match[1] as MarkdownLang | undefined
);
return (
<SyntaxHighlighter
language={match[1]}
PreTag="div"
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
style={codetheme}
{...props}
>
<StyledSyntaxHighlighter language={syntaxHighlighterLang}>
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
</StyledSyntaxHighlighter>
);
} else if (String(props.children).includes("\n")) {
// 言語指定なしコードブロック
return (
<SyntaxHighlighter
PreTag="div"
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
style={codetheme}
{...props}
>
<StyledSyntaxHighlighter language={undefined}>
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
</StyledSyntaxHighlighter>
);
} else {
// inline
return (
<code
className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm "
className="bg-current/10 border border-current/20 px-1 py-0.5 mx-0.5 rounded-md"
{...props}
/>
);
Expand Down
85 changes: 54 additions & 31 deletions app/[docs_id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,75 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
import { splitMarkdown } from "./splitMarkdown";
import { PageContent } from "./pageContent";
import { ChatHistoryProvider } from "./chatHistory";
import { getChatFromCache } from "@/lib/chatHistory";
import { getLanguageName } from "@/pagesList";

export default async function Page({
async function getMarkdownContent(docs_id: string): Promise<string> {
try {
if (process.env.NODE_ENV === "development") {
return await readFile(
join(process.cwd(), "public", "docs", `${docs_id}.md`),
"utf-8"
);
} else {
const cfAssets = getCloudflareContext().env.ASSETS;
const res = await cfAssets!.fetch(
`https://assets.local/docs/${docs_id}.md`
);
if (!res.ok) {
notFound();
}
return await res.text();
}
} catch (e) {
console.error(e);
notFound();
}
}

export async function generateMetadata({
params,
}: {
params: Promise<{ docs_id: string }>;
}) {
}): Promise<Metadata> {
const { docs_id } = await params;
const mdContent = await getMarkdownContent(docs_id);
const splitMdContent = splitMarkdown(mdContent);

let mdContent: Promise<string>;
if (process.env.NODE_ENV === "development") {
mdContent = readFile(
join(process.cwd(), "public", "docs", `${docs_id}.md`),
"utf-8"
).catch((e) => {
console.error(e);
notFound();
});
} else {
const cfAssets = getCloudflareContext().env.ASSETS;
mdContent = cfAssets!
.fetch(`https://assets.local/docs/${docs_id}.md`)
.then(async (res) => {
if (!res.ok) {
notFound();
}
return res.text();
})
.catch((e) => {
console.error(e);
notFound();
});
}
// 先頭の 第n章: を除いたものをタイトルとする
const title = splitMdContent[0]?.title?.split(" ").slice(1).join(" ");

const splitMdContent: Promise<MarkdownSection[]> = mdContent.then((text) =>
splitMarkdown(text)
);
const description = splitMdContent[0].content;

const chapter = docs_id.split("-")[1];

return {
title: `${getLanguageName(docs_id)}-${chapter}. ${title}`,
description,
};
}

export default async function Page({
params,
}: {
params: Promise<{ docs_id: string }>;
}) {
const { docs_id } = await params;

const mdContent = getMarkdownContent(docs_id);
const splitMdContent = mdContent.then((text) => splitMarkdown(text));
const initialChatHistories = getChatFromCache(docs_id);

return (
<ChatHistoryProvider initialChatHistories={await initialChatHistories} docs_id={docs_id}>
<ChatHistoryProvider
initialChatHistories={await initialChatHistories}
docs_id={docs_id}
>
<PageContent
documentContent={await mdContent}
splitMdContent={await splitMdContent}
Expand Down
19 changes: 8 additions & 11 deletions app/[docs_id]/pageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { Fragment, useEffect, useRef, useState } from "react";
import { MarkdownSection } from "./splitMarkdown";
import { ChatForm } from "./chatForm";
import { Heading, StyledMarkdown } from "./markdown";
Expand Down Expand Up @@ -90,10 +90,9 @@ export function PageContent(props: PageContentProps) {
}}
>
{dynamicMdContent.map((section, index) => (
<>
<Fragment key={index}>
<div
className="max-w-200"
key={`${index}-content`}
id={`${index}`} // 目次からaタグで飛ぶために必要
ref={(el) => {
sectionRefs.current[index] = el;
Expand All @@ -103,14 +102,14 @@ export function PageContent(props: PageContentProps) {
<Heading level={section.level}>{section.title}</Heading>
<StyledMarkdown content={section.content} />
</div>
<div key={`${index}-chat`}>
<div>
{/* 右側に表示するチャット履歴欄 */}
{chatHistories
.filter((c) => c.sectionId === section.sectionId)
.map(({ chatId, messages }) => (
<div
key={chatId}
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-100"
className="max-w-xs mb-2 p-2 text-sm border border-base-content/10 rounded-sm shadow-sm bg-base-200"
>
<div className="max-h-60 overflow-y-auto">
{messages.map((msg, index) => (
Expand All @@ -120,12 +119,10 @@ export function PageContent(props: PageContentProps) {
>
<div
className={clsx(
"chat-bubble p-1!",
msg.role === "user" &&
"bg-primary text-primary-content",
msg.role === "ai" &&
"bg-secondary-content dark:bg-neutral text-black dark:text-white",
msg.role === "error" && "chat-bubble-error"
"chat-bubble p-0.5! bg-secondary/30",
msg.role === "ai" && "chat-bubble p-0.5!",
msg.role === "error" && "text-error"
)}
style={{ maxWidth: "100%", wordBreak: "break-word" }}
>
Expand All @@ -137,7 +134,7 @@ export function PageContent(props: PageContentProps) {
</div>
))}
</div>
</>
</Fragment>
))}
{isFormVisible ? (
// sidebarの幅が80であることからleft-84 (sidebar.tsx参照)
Expand Down
Loading