Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
2 changes: 1 addition & 1 deletion app/[docs_id]/chatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function ChatForm({ documentContent, sectionId }: ChatFormProps) {
className={clsx(
"chat-bubble",
{ "bg-primary text-primary-content": msg.sender === 'user' },
{ "bg-secondary-content text-black": msg.sender === 'ai' && !msg.isError },
{ "bg-secondary-content dark:bg-neutral text-black dark:text-white": msg.sender === 'ai' && !msg.isError },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text-neutral-content か text-base-content (daisyuiの色) にしておけばlightモードでは黒、darkモードでは白になるんじゃないかな
(daisyuiのドキュメントの右上のボタンからテーマを変更するとテーマごとの色を確認できる)

ダークモードで文字を完全な白にするのはコントラストが強すぎるので普通は少しグレーにするのがいいらしいです(ダークモードユーザーじゃないのでしらんけど)

{ "chat-bubble-error": msg.isError }
)}
style={{maxWidth: "100%", wordBreak: "break-word"}}
Expand Down
243 changes: 124 additions & 119 deletions app/[docs_id]/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { PythonEmbeddedTerminal } from "../terminal/python/embedded";
import { Heading } from "./section";
import { type AceLang, EditorComponent } from "../terminal/editor";
import { ExecFile, ExecLang } from "../terminal/exec";
import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useChangeTheme } from "./themeToggle";
import { tomorrow, atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

twilightなかった?

まあ別にatomOneDarkでも良いんだけど

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prism系の方ならありました。hljsだと見当たらないような気がします。


export function StyledMarkdown({ content }: { content: string }) {
return (
Expand All @@ -15,6 +16,7 @@ export function StyledMarkdown({ content }: { content: string }) {
);
}


// TailwindCSSがh1などのタグのスタイルを消してしまうので、手動でスタイルを指定する必要がある
const components: Components = {
h1: ({ children }) => <Heading level={1}>{children}</Heading>,
Expand All @@ -33,7 +35,7 @@ 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" {...props} />
<strong className="text-primary dark:text-secondary" {...props} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここに書くとaiの吹き出し以外でも色が変わってしまうのでは…

),
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">
Expand All @@ -42,130 +44,133 @@ const components: Components = {
),
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
pre: ({ node, ...props }) => props.children,
code: ({ node, className, ref, style, ...props }) => {
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
className || ""
);
if (match) {
if (match[2] === "-exec" && match[3]) {
/*
```python-exec:main.py
code: ({ node, className, ref, style, ...props }) => <CodeComponent {...{ node, className, ref, style, ...props }} />,
};
function CodeComponent({ node, className, ref, style, ...props }: { node: unknown; className?: string; ref?: unknown; style?: unknown; [key: string]: unknown }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

型指定が汚い 🤔 (別issueにする)

const theme = useChangeTheme();
const codetheme= theme === "tomorrow" ? tomorrow : atomOneDark;
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
className || ""
);
if (match) {
if (match[2] === "-exec" && match[3]) {
/*
```python-exec:main.py
hello, world!
```
---------------------------
[▶ 実行] `python main.py`
hello, world!
```
---------------------------
[▶ 実行] `python main.py`
hello, world!
---------------------------
*/
let execLang: ExecLang | undefined = undefined;
switch (match[1]) {
case "python":
execLang = "python";
break;
case "cpp":
case "c++":
execLang = "cpp";
break;
default:
console.warn(`Unsupported language for exec: ${match[1]}`);
break;
}
if (execLang) {
return (
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
<ExecFile
language={execLang}
filenames={match[3].split(",")}
content={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
}
} else if (match[3]) {
// ファイル名指定がある場合、ファイルエディター
let aceLang: AceLang | undefined = undefined;
switch (match[1]) {
case "python":
aceLang = "python";
break;
case "cpp":
case "c++":
aceLang = "c_cpp";
break;
case "json":
aceLang = "json";
break;
case "csv":
aceLang = "csv";
break;
case "text":
case "txt":
aceLang = "text";
break;
default:
console.warn(`Unsupported language for editor: ${match[1]}`);
break;
}
---------------------------
*/
let execLang: ExecLang | undefined = undefined;
switch (match[1]) {
case "python":
execLang = "python";
break;
case "cpp":
case "c++":
execLang = "cpp";
break;
default:
console.warn(`Unsupported language for exec: ${match[1]}`);
break;
}
if (execLang) {
return (
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
<EditorComponent
language={aceLang}
tabSize={4}
filename={match[3]}
readonly={match[2] === "-readonly"}
initContent={String(props.children || "").replace(/\n$/, "")}
<ExecFile
language={execLang}
filenames={match[3].split(",")}
content={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
} else if (match[2] === "-repl") {
// repl付きの言語指定
// 現状はPythonのみ対応
switch (match[1]) {
case "python":
return (
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
<PythonEmbeddedTerminal
content={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
default:
console.warn(`Unsupported language for repl: ${match[1]}`);
break;
}
}
} else if (match[3]) {
// ファイル名指定がある場合、ファイルエディター
let aceLang: AceLang | undefined = undefined;
switch (match[1]) {
case "python":
aceLang = "python";
break;
case "cpp":
case "c++":
aceLang = "c_cpp";
break;
case "json":
aceLang = "json";
break;
case "csv":
aceLang = "csv";
break;
case "text":
case "txt":
aceLang = "text";
break;
default:
console.warn(`Unsupported language for editor: ${match[1]}`);
break;
}
return (
<SyntaxHighlighter
language={match[1]}
PreTag="div"
className="border border-base-content/50 mx-2 my-2 rounded-lg text-sm p-4!"
style={tomorrow} // todo dark theme (editor.tsx で指定したのと同じテーマを選ぶようにすること)
{...props}
>
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
);
} 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={tomorrow} // todo dark theme
{...props}
>
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
);
} else {
// inline
return (
<code
className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm "
{...props}
/>
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
<EditorComponent
language={aceLang}
tabSize={4}
filename={match[3]}
readonly={match[2] === "-readonly"}
initContent={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
} else if (match[2] === "-repl") {
// repl付きの言語指定
// 現状はPythonのみ対応
switch (match[1]) {
case "python":
return (
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
<PythonEmbeddedTerminal
content={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
default:
console.warn(`Unsupported language for repl: ${match[1]}`);
break;
}
}
},
};
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}
>
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
);
} 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}
>
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
);
} else {
// inline
return (
<code
className="bg-base-200/60 border border-base-300 px-1 py-0.5 rounded text-sm "
{...props}
/>
);
}
}
78 changes: 78 additions & 0 deletions app/[docs_id]/themeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";
import { useState, useEffect} from "react";

export function useChangeTheme(){
const [theme, setTheme] = useState("tomorrow");
useEffect(() => {
const checkIsDarkSchemePreferred = () =>
window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ?? false;
const initialTheme = checkIsDarkSchemePreferred() ? "dark" : "light";
document.documentElement.setAttribute("data-theme", initialTheme);
setTheme(initialTheme === "dark" ? "twilight" : "tomorrow");

const updateTheme = () => {
const theme = document.documentElement.getAttribute("data-theme");
setTheme(theme === "dark" ? "twilight" : "tomorrow");
};

const observer = new MutationObserver(updateTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});


return () => observer.disconnect();
}, []);
return theme;

};
export function ThemeToggle() {
const [isChecked, setIsChecked] = useState(false);
const theme = useChangeTheme();
useEffect(() => {
setIsChecked(theme === "twilight");
}, [theme]);

return (
<label className="flex cursor-pointer gap-2" style={{ marginLeft: "1em" }}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<circle cx="12" cy="12" r="5" />
<path
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
</svg>
<input
type="checkbox"
checked={isChecked}
className="toggle theme-controller"
onChange={(e) => {
const isdark = e.target.checked;
setIsChecked(isdark);
const theme = isdark ? "dark" : "light";
document.documentElement.setAttribute("data-theme", theme);
}}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</label>
);
}
8 changes: 7 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui"
{
themes: light --default, dark --prefersdark;
};

@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));


/* CDNからダウンロードするURLを指定したらなんかエラー出るので、npmでインストールしてlayout.tsxでimportすることにした */
@theme {
Expand Down
8 changes: 5 additions & 3 deletions app/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ThemeToggle } from "./[docs_id]/themeToggle";
export function Navbar() {
return (
<div className="navbar bg-base-200 w-full">
Expand All @@ -23,9 +24,10 @@ export function Navbar() {
</svg>
</label>
</div>
<div className="mx-2 flex-1 px-2 font-bold text-xl lg:hidden">
{/* タイトル(サイドバー非表示の場合のみ) */}
Navbar Title
<div className="mx-2 flex flex-row items-center px-2 font-bold text-xl lg:hidden">
{/* サイドバーが常時表示されている場合のみ */}
<span className="flex-1">Navbar Title</span>
<ThemeToggle />
</div>
</div>
);
Expand Down
Loading