From be167846eb0e6bdd12de588ae00bcade7d929c83 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 12 Aug 2026 05:01:48 +0000 Subject: [PATCH 01/20] feat: Add /[lang]/sandbox page with REPL, code execution, file addition, and section chat --- app/(docs)/@chat/chat/[chatId]/chatArea.tsx | 24 +- .../@docs/[lang]/[pageId]/pageContent.tsx | 2 +- app/(docs)/@docs/[lang]/sandbox/page.tsx | 55 +++++ .../@docs/[lang]/sandbox/sandboxContent.tsx | 216 ++++++++++++++++++ app/api/chat/route.ts | 146 +++++++----- app/lib/chatHistory.ts | 12 + app/lib/docs.ts | 16 ++ app/sidebar.tsx | 14 ++ app/terminal/page.tsx | 69 +----- app/terminal/sampleConfig.ts | 72 ++++++ 10 files changed, 487 insertions(+), 139 deletions(-) create mode 100644 app/(docs)/@docs/[lang]/sandbox/page.tsx create mode 100644 app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx create mode 100644 app/terminal/sampleConfig.ts diff --git a/app/(docs)/@chat/chat/[chatId]/chatArea.tsx b/app/(docs)/@chat/chat/[chatId]/chatArea.tsx index 63bef250..0de65b0e 100644 --- a/app/(docs)/@chat/chat/[chatId]/chatArea.tsx +++ b/app/(docs)/@chat/chat/[chatId]/chatArea.tsx @@ -127,20 +127,28 @@ export function ChatAreaContent(props: Props) {
diff --git a/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx b/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx index 7beb8036..69bbaee7 100644 --- a/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx +++ b/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx @@ -167,7 +167,7 @@ export function PageContent(props: PageContentProps) { ); } -function ChatListForSection(props: { +export function ChatListForSection(props: { dynamicMdContent: DynamicMarkdownSection[]; sectionId: SectionId; chatHistories: ChatWithMessages[]; diff --git a/app/(docs)/@docs/[lang]/sandbox/page.tsx b/app/(docs)/@docs/[lang]/sandbox/page.tsx new file mode 100644 index 00000000..b213d36c --- /dev/null +++ b/app/(docs)/@docs/[lang]/sandbox/page.tsx @@ -0,0 +1,55 @@ +import { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { SandboxContent } from "./sandboxContent"; +import { getChatFromCache, initContext } from "@/lib/chatHistory"; +import { getPagesListForLang, getTermDefinitions, LangId, PageSlug } from "@/lib/docs"; +import { TermDefinitionProvider } from "@/markdown/term"; +import { DocsAutoRedirect } from "../[pageId]/autoRedirect"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: LangId }>; +}): Promise { + const { lang } = await params; + const langEntry = await getPagesListForLang(lang); + if (!langEntry) notFound(); + + return { + title: `${langEntry.name} - Sandbox`, + description: `${langEntry.name} のインタラクティブなコード実行サンドボックスです。`, + }; +} + +export default async function Page({ + params, +}: { + params: Promise<{ lang: LangId }>; +}) { + const { lang } = await params; + + const langEntry = await getPagesListForLang(lang); + if (!langEntry) notFound(); + + const path = { lang, page: "sandbox" as PageSlug }; + const context = await initContext(); + const chatHistories = await getChatFromCache(path, context.userId); + const termDefinitions = await getTermDefinitions(lang); + + return ( + <> + + + + + + ); +} diff --git a/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx new file mode 100644 index 00000000..e4a34fd9 --- /dev/null +++ b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { useState, FormEvent, useEffect } from "react"; +import { Heading } from "@/markdown/heading"; +import { langConstants, RuntimeLang } from "@my-code/runtime/languages"; +import { ReplTerminal } from "@/terminal/repl"; +import { EditorComponent } from "@/terminal/editor"; +import { ExecFile } from "@/terminal/exec"; +import { sampleConfig } from "@/terminal/sampleConfig"; +import { + DynamicMarkdownSection, + LangId, + PagePath, + PageSlug, + SectionId, +} from "@/lib/docs"; +import { ChatWithMessages } from "@/lib/chatHistory"; +import { ChatForm } from "../[pageId]/chatForm"; +import { ChatListForSection } from "../[pageId]/pageContent"; +import { usePagesListForLang } from "@/pagesListContext"; +import { useEmbedContext } from "@/terminal/embedContext"; +import { useSidebarMdContext } from "@/sidebar"; + +interface SandboxContentProps { + langId: LangId; + path: PagePath; + chatHistories: ChatWithMessages[]; +} + +export function SandboxContent(props: SandboxContentProps) { + const { langId, path, chatHistories } = props; + const langEntry = usePagesListForLang(langId); + const { setSidebarMdContent } = useSidebarMdContext(); + const { writeFile } = useEmbedContext(); + + const runtimeLang = langId as RuntimeLang; + const config = sampleConfig[runtimeLang]; + + const [userFiles, setUserFiles] = useState([]); + const [newFilename, setNewFilename] = useState(""); + const [filenameError, setFilenameError] = useState(null); + const [isFormVisible, setIsFormVisible] = useState(false); + + const dummySection: DynamicMarkdownSection[] = [ + { + id: "sandbox" as SectionId, + level: 1, + title: "sandbox", + file: "sandbox.md", + rawContent: "", + md5: "", + replacedContent: "", + replacedRange: [], + inView: true, + }, + ]; + + useEffect(() => { + setSidebarMdContent(path, dummySection); + }, [path, setSidebarMdContent]); + + const handleAddFile = (e: FormEvent) => { + e.preventDefault(); + const name = newFilename.trim(); + if (!name) return; + + // 既存ファイルチェック + const defaultFiles = config?.editor ? Object.keys(config.editor) : []; + const readonlyFiles = config?.readonlyFiles ?? []; + if ( + defaultFiles.includes(name) || + readonlyFiles.includes(name) || + userFiles.includes(name) + ) { + setFilenameError("同名のファイルがすでに存在します。"); + return; + } + + setFilenameError(null); + setUserFiles((prev) => [...prev, name]); + writeFile({ [name]: "" }); + setNewFilename(""); + }; + + const handleRemoveFile = (filename: string) => { + setUserFiles((prev) => prev.filter((f) => f !== filename)); + }; + + return ( +
+
+ {langEntry?.name ?? langId} Sandbox +
+ +
+ {config?.repl && ( +
+ REPL + +
+ )} + + {config?.editor && ( +
+ サンプルコード + {Object.entries(config.editor).map(([filename, initContent]) => ( + + ))} +
+ )} + + {config?.exec && ( +
+ 実行 + +
+ )} + + {config?.readonlyFiles && config.readonlyFiles.length > 0 && ( +
+ 出力ファイル + {config.readonlyFiles.map((filename) => ( + + ))} +
+ )} + +
+ 追加ファイル +
+ { + setNewFilename(e.target.value); + setFilenameError(null); + }} + /> + +
+ {filenameError && ( +

{filenameError}

+ )} + + {userFiles.map((filename) => ( +
+
+ {filename} + +
+ +
+ ))} +
+ +
+ +
+
+ + {isFormVisible ? ( +
+ setIsFormVisible(false)} + /> +
+ ) : ( + + )} +
+ ); +} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 803b59cf..cef6d3d3 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -14,6 +14,7 @@ import { getPagesListForLang, introSectionId, PagePathSchema, + PageSlug, SectionId, } from "@/lib/docs"; import { @@ -129,40 +130,51 @@ export async function POST(request: NextRequest) { } } + const isSandbox = path.page === ("sandbox" as PageSlug); + const prompt: string[] = []; - prompt.push(`あなたは${langName}言語のチュートリアルの講師をしています。`); - prompt.push( - `以下の${langName}チュートリアルのドキュメントの内容を正確に理解し、ユーザーからの質問に対して、初心者にも分かりやすく、丁寧な解説を提供してください。` - ); - prompt.push(``); - const sectionTitlesInView = targetSectionContent - .filter((s) => s.inView) - .map((s) => s.title); - if (sectionTitlesInView.length > 0) { + if (isSandbox) { + prompt.push(`あなたは${langName}プログラミングの学習者をサポートする講師AIアシスタントです。`); prompt.push( - `ユーザーはドキュメント内の ${sectionTitlesInView.join(", ")} の付近のセクションを閲覧している際にこの質問を行っていると推測されます。` + `ユーザーからの質問に対して、初心者にも分かりやすく、丁寧な解説を提供してください。` ); + prompt.push(``); + } else { + prompt.push(`あなたは${langName}言語のチュートリアルの講師をしています。`); prompt.push( - `質問に答える際には、ユーザーが閲覧しているセクションの内容を特に考慮してください。` + `以下の${langName}チュートリアルのドキュメントの内容を正確に理解し、ユーザーからの質問に対して、初心者にも分かりやすく、丁寧な解説を提供してください。` ); - } - prompt.push(``); - prompt.push( - `質問への回答はユーザー向けのメッセージに加えて、ドキュメント自体を改訂するという形でも可能です。` - ); - prompt.push( - `質問内容とドキュメントの内容の関連性が深く、比較的長めの解説をしたい場合、またはドキュメントへの補足がしたい場合は、そちらの形式での回答を検討してください。` - ); - prompt.push(``); - prompt.push(`# ドキュメント`); - prompt.push(``); - for (const section of targetSectionContent) { - prompt.push(`[セクションid: ${section.id}]`); - prompt.push(section.replacedContent.trim()); + prompt.push(``); + const sectionTitlesInView = targetSectionContent + .filter((s) => s.inView) + .map((s) => s.title); + if (sectionTitlesInView.length > 0) { + prompt.push( + `ユーザーはドキュメント内の ${sectionTitlesInView.join(", ")} の付近のセクションを閲覧している際にこの質問を行っていると推測されます。` + ); + prompt.push( + `質問に答える際には、ユーザーが閲覧しているセクションの内容を特に考慮してください。` + ); + } + prompt.push(``); + prompt.push( + `質問への回答はユーザー向けのメッセージに加えて、ドキュメント自体を改訂するという形でも可能です。` + ); + prompt.push( + `質問内容とドキュメントの内容の関連性が深く、比較的長めの解説をしたい場合、またはドキュメントへの補足がしたい場合は、そちらの形式での回答を検討してください。` + ); + prompt.push(``); + prompt.push(`# ドキュメント`); + prompt.push(``); + for (const section of targetSectionContent) { + prompt.push(`[セクションid: ${section.id}]`); + prompt.push(section.replacedContent.trim()); + prompt.push(``); + } prompt.push(``); } - prompt.push(``); + if (Object.keys(replOutputs).length > 0) { prompt.push( `# ターミナルのログ(ユーザーが入力したコマンドとその実行結果)` @@ -224,15 +236,19 @@ export async function POST(request: NextRequest) { prompt.push("# 指示"); prompt.push(""); - prompt.push( - `- 1行目に、ユーザーの質問ともっとも関連性の高いドキュメント内のセクションのidを回答してください。` - ); - prompt.push( - " - idのみを出力してください。 セクションid: や括弧や引用符などは不要です。" - ); - prompt.push( - " - ユーザーの質問がドキュメントのどのセクションとも直接的に関連しない場合は null と出力してください。" - ); + if (isSandbox) { + prompt.push(`- 1行目に sandbox とのみ出力してください。`); + } else { + prompt.push( + `- 1行目に、ユーザーの質問ともっとも関連性の高いドキュメント内のセクションのidを回答してください。` + ); + prompt.push( + " - idのみを出力してください。 セクションid: や括弧や引用符などは不要です。" + ); + prompt.push( + " - ユーザーの質問がドキュメントのどのセクションとも直接的に関連しない場合は null と出力してください。" + ); + } prompt.push( "- 2行目に、この質問と回答を後から参照するためのわかりやすいタイトルをつけて記述してください。" ); @@ -240,7 +256,7 @@ export async function POST(request: NextRequest) { " - 太字やコードブロックなどのMarkdownの記法は使わずテキストのみで出力してください。" ); prompt.push( - "- 3行目以降に、ドキュメントの内容に基づいて、ユーザーに伝える回答をMarkdown形式で記述してください。" + "- 3行目以降に、ユーザーに伝える回答をMarkdown形式で記述してください。" ); prompt.push( " - ユーザーが入力したターミナルのコマンドやファイルの内容、実行結果を参考にして回答してください。" @@ -250,29 +266,32 @@ export async function POST(request: NextRequest) { " - 回答内でコードブロックを使用する際は ```言語名 としてください。" + "ドキュメント内では ```言語名-repl や ```言語名:ファイル名 、 ```言語名-exec:ファイル名 などの特殊なコードブロックが登場しますが、ユーザーへの回答ではこれらの記法は使用しないでください。" ); - prompt.push("- ドキュメントの一部を改訂したい場合はその差分を"); - prompt.push("<<<<<<< SEARCH"); - prompt.push("修正したい元の文章の塊(一字一句違わずに)"); - prompt.push("======="); - prompt.push("修正後の新しい文章の塊"); - prompt.push(">>>>>>> REPLACE"); - prompt.push("の形式で出力してください。"); - prompt.push( - " - 複数箇所改訂したい場合は上の形式の出力を複数回繰り返してください。" - ); - prompt.push( - " - ドキュメントにテキストを追加したい場合は追加したい箇所の前後のテキストを含めて出力してください。" - ); - prompt.push( - " - セクションid、セクション見出しを編集、追加、削除することはできません。" - ); - prompt.push( - " - ドキュメント内の特殊なコードブロック(```言語名-repl , ```言語名:ファイル名 , ```言語名-exec:ファイル名 )は編集、追加、削除することはできません。それ以外の文章のみを編集してください。" + - "ただし通常のコードブロック(```言語名 )の追加は可能です。" - ); - prompt.push( - " - 改訂後のドキュメントと同じ内容はユーザーに伝える回答としては省略できます。(「修正後のドキュメントを参照してください。」など)" - ); + + if (!isSandbox) { + prompt.push("- ドキュメントの一部を改訂したい場合はその差分を"); + prompt.push("<<<<<<< SEARCH"); + prompt.push("修正したい元の文章の塊(一字一句違わずに)"); + prompt.push("======="); + prompt.push("修正後の新しい文章の塊"); + prompt.push(">>>>>>> REPLACE"); + prompt.push("の形式で出力してください。"); + prompt.push( + " - 複数箇所改訂したい場合は上の形式の出力を複数回繰り返してください。" + ); + prompt.push( + " - ドキュメントにテキストを追加したい場合は追加したい箇所の前後のテキストを含めて出力してください。" + ); + prompt.push( + " - セクションid、セクション見出しを編集、追加、削除することはできません。" + ); + prompt.push( + " - ドキュメント内の特殊なコードブロック(```言語名-repl , ```言語名:ファイル名 , ```言語名-exec:ファイル名 )は編集、追加、削除することはできません。それ以外の文章のみを編集してください。" + + "ただし通常のコードブロック(```言語名 )の追加は可能です。" + ); + prompt.push( + " - 改訂後のドキュメントと同じ内容はユーザーに伝える回答としては省略できます。(「修正後のドキュメントを参照してください。」など)" + ); + } console.log(prompt); @@ -303,12 +322,15 @@ export async function POST(request: NextRequest) { const headerMatch = fullText.match(/^([^\n]+?)\n+([^\n]+?)\n+/); if (headerMatch) { headerParsed = true; - let targetSectionId = headerMatch[1].trim() as SectionId; + let targetSectionId = isSandbox + ? ("sandbox" as SectionId) + : (headerMatch[1].trim() as SectionId); const title = headerMatch[2].trim(); if ( - !targetSectionId || - !targetSectionContent.some((s) => s.id === targetSectionId) + !isSandbox && + (!targetSectionId || + !targetSectionContent.some((s) => s.id === targetSectionId)) ) { targetSectionId = introSectionId(targetPath); } diff --git a/app/lib/chatHistory.ts b/app/lib/chatHistory.ts index daca57b9..95f9887b 100644 --- a/app/lib/chatHistory.ts +++ b/app/lib/chatHistory.ts @@ -140,6 +140,18 @@ export async function addChat( if (!userId) { throw new Error("Not authenticated"); } + + await drizzle + .insert(section) + .values({ + sectionId, + pagePath: `${path.lang}/${path.page}`, + }) + .onConflictDoUpdate({ + target: section.sectionId, + set: { pagePath: `${path.lang}/${path.page}` }, + }); + const [newChat] = await drizzle .insert(chat) .values({ diff --git a/app/lib/docs.ts b/app/lib/docs.ts index 4b8155fd..de1ec332 100644 --- a/app/lib/docs.ts +++ b/app/lib/docs.ts @@ -296,6 +296,22 @@ export async function getMarkdownSections( lang: LangId, page: PageSlug ): Promise { + if (page === ("sandbox" as PageSlug)) { + if (!(await getLanguageIds()).includes(lang)) { + notFound(); + } + return [ + { + file: "sandbox.md", + id: "sandbox" as SectionId, + level: 1, + title: "sandbox", + rawContent: "", + md5: "", + }, + ]; + } + if ( /*!(await getLanguageIds()).includes(lang) || // getPagesListForLangのなかでチェック */ !(await getPagesListForLang(lang)).pages.some((p) => p.slug === page) diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 1615ee93..1d98b8b1 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -225,6 +225,20 @@ export function Sidebar() { )} ))} +
  • + + + Sandbox + +
  • diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index ba9a32ec..0431f410 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -17,15 +17,7 @@ import { fileExecutionTests } from "@my-code/runtime/tests/fileExecution"; import { useRuntimeAll } from "@my-code/runtime/context"; import { captureException } from "@sentry/nextjs"; -import main_py from "./samples/main.py?raw"; -import main_rb from "./samples/main.rb?raw"; -import main_js from "./samples/main.js?raw"; -import main2_ts from "./samples/main2.ts?raw"; -import main_cpp from "./samples/main.cpp?raw"; -import sub_h from "./samples/sub.h?raw"; -import sub_cpp from "./samples/sub.cpp?raw"; -import main2_rs from "./samples/main2.rs?raw"; -import sub_rs from "./samples/sub.rs?raw"; +import { sampleConfig, SampleConfig } from "./sampleConfig"; import { DaisyInfoIcon } from "@/daisyAlertIcon"; export default function RuntimeTestPage() { @@ -69,65 +61,6 @@ export default function RuntimeTestPage() { ); } -interface SampleConfig { - repl: boolean; - replInitContent?: string; // ReplOutput[] ではない。stringのパースはruntimeが行う - editor: Record | false; - exec: string[] | false; - readonlyFiles?: string[]; -} -const sampleConfig: Record = { - python: { - repl: true, - replInitContent: '>>> print("Hello, World!")\nHello, World!', - editor: { - "main.py": main_py, - }, - exec: ["main.py"], - }, - ruby: { - repl: true, - replInitContent: 'irb(main):001:0> puts "Hello, World!"\nHello, World!', - editor: { - "main.rb": main_rb, - }, - exec: ["main.rb"], - }, - javascript: { - repl: true, - replInitContent: '> console.log("Hello, World!");\nHello, World!', - editor: { - "main.js": main_js, - }, - exec: ["main.js"], - }, - typescript: { - repl: false, - editor: { - // main.tsにすると出力ファイルがjavascriptのサンプルと被る - "main2.ts": main2_ts, - }, - exec: ["main2.ts"], - readonlyFiles: ["main2.js"], - }, - cpp: { - repl: false, - editor: { - "main.cpp": main_cpp, - "sub.h": sub_h, - "sub.cpp": sub_cpp, - }, - exec: ["main.cpp", "sub.cpp"], - }, - rust: { - repl: false, - editor: { - "main2.rs": main2_rs, - "sub.rs": sub_rs, - }, - exec: ["main2.rs"], - }, -}; function RuntimeSample({ lang, config, diff --git a/app/terminal/sampleConfig.ts b/app/terminal/sampleConfig.ts new file mode 100644 index 00000000..0106003d --- /dev/null +++ b/app/terminal/sampleConfig.ts @@ -0,0 +1,72 @@ +import { RuntimeLang } from "@my-code/runtime/languages"; + +import main_py from "./samples/main.py?raw"; +import main_rb from "./samples/main.rb?raw"; +import main_js from "./samples/main.js?raw"; +import main2_ts from "./samples/main2.ts?raw"; +import main_cpp from "./samples/main.cpp?raw"; +import sub_h from "./samples/sub.h?raw"; +import sub_cpp from "./samples/sub.cpp?raw"; +import main2_rs from "./samples/main2.rs?raw"; +import sub_rs from "./samples/sub.rs?raw"; + +export interface SampleConfig { + repl: boolean; + replInitContent?: string; // ReplOutput[] ではない。stringのパースはruntimeが行う + editor: Record | false; + exec: string[] | false; + readonlyFiles?: string[]; +} + +export const sampleConfig: Record = { + python: { + repl: true, + replInitContent: '>>> print("Hello, World!")\nHello, World!', + editor: { + "main.py": main_py, + }, + exec: ["main.py"], + }, + ruby: { + repl: true, + replInitContent: 'irb(main):001:0> puts "Hello, World!"\nHello, World!', + editor: { + "main.rb": main_rb, + }, + exec: ["main.rb"], + }, + javascript: { + repl: true, + replInitContent: '> console.log("Hello, World!");\nHello, World!', + editor: { + "main.js": main_js, + }, + exec: ["main.js"], + }, + typescript: { + repl: false, + editor: { + // main.tsにすると出力ファイルがjavascriptのサンプルと被る + "main2.ts": main2_ts, + }, + exec: ["main2.ts"], + readonlyFiles: ["main2.js"], + }, + cpp: { + repl: false, + editor: { + "main.cpp": main_cpp, + "sub.h": sub_h, + "sub.cpp": sub_cpp, + }, + exec: ["main.cpp", "sub.cpp"], + }, + rust: { + repl: false, + editor: { + "main2.rs": main2_rs, + "sub.rs": sub_rs, + }, + exec: ["main2.rs"], + }, +}; From 6a396d57be4d675d16d2760ee4a0705e773a6bf9 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:43:05 +0000 Subject: [PATCH 02/20] feat(terminal): add supportsMultiFile config and delete button to EditorComponent --- app/terminal/editor.tsx | 48 ++++++++++++++++++++++++++++++++++++ app/terminal/sampleConfig.ts | 7 ++++++ 2 files changed, 55 insertions(+) diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index 14d2547b..b2f910cd 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -37,6 +37,7 @@ interface EditorProps { filename: string; initContent: string; readonly?: boolean; + onDelete?: () => void; } export function EditorComponent(props: EditorProps) { const theme = useChangeTheme(); @@ -129,6 +130,53 @@ export function EditorComponent(props: EditorProps) { 元の内容に戻す
    + {props.onDelete && ( + + )}
    {fontSize !== undefined && initAce ? ( diff --git a/app/terminal/sampleConfig.ts b/app/terminal/sampleConfig.ts index 0106003d..0fa23cc7 100644 --- a/app/terminal/sampleConfig.ts +++ b/app/terminal/sampleConfig.ts @@ -16,6 +16,7 @@ export interface SampleConfig { editor: Record | false; exec: string[] | false; readonlyFiles?: string[]; + supportsMultiFile?: boolean; } export const sampleConfig: Record = { @@ -26,6 +27,7 @@ export const sampleConfig: Record = { "main.py": main_py, }, exec: ["main.py"], + supportsMultiFile: true, }, ruby: { repl: true, @@ -34,6 +36,7 @@ export const sampleConfig: Record = { "main.rb": main_rb, }, exec: ["main.rb"], + supportsMultiFile: true, }, javascript: { repl: true, @@ -42,6 +45,7 @@ export const sampleConfig: Record = { "main.js": main_js, }, exec: ["main.js"], + supportsMultiFile: false, }, typescript: { repl: false, @@ -51,6 +55,7 @@ export const sampleConfig: Record = { }, exec: ["main2.ts"], readonlyFiles: ["main2.js"], + supportsMultiFile: false, }, cpp: { repl: false, @@ -60,6 +65,7 @@ export const sampleConfig: Record = { "sub.cpp": sub_cpp, }, exec: ["main.cpp", "sub.cpp"], + supportsMultiFile: true, }, rust: { repl: false, @@ -68,5 +74,6 @@ export const sampleConfig: Record = { "sub.rs": sub_rs, }, exec: ["main2.rs"], + supportsMultiFile: true, }, }; From 05449e8d0d2e36d9d036ea33f4aefbc919030d96 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:43:28 +0000 Subject: [PATCH 03/20] feat(sandbox): reorder sections and add TOC scroll tracking --- .../@docs/[lang]/sandbox/sandboxContent.tsx | 198 ++++++++++++------ 1 file changed, 131 insertions(+), 67 deletions(-) diff --git a/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx index e4a34fd9..7ceebc68 100644 --- a/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx +++ b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, FormEvent, useEffect } from "react"; +import { useState, FormEvent, useEffect, useMemo, useRef } from "react"; import { Heading } from "@/markdown/heading"; import { langConstants, RuntimeLang } from "@my-code/runtime/languages"; import { ReplTerminal } from "@/terminal/repl"; @@ -11,7 +11,6 @@ import { DynamicMarkdownSection, LangId, PagePath, - PageSlug, SectionId, } from "@/lib/docs"; import { ChatWithMessages } from "@/lib/chatHistory"; @@ -41,23 +40,71 @@ export function SandboxContent(props: SandboxContentProps) { const [filenameError, setFilenameError] = useState(null); const [isFormVisible, setIsFormVisible] = useState(false); - const dummySection: DynamicMarkdownSection[] = [ - { - id: "sandbox" as SectionId, - level: 1, - title: "sandbox", + // サイドバーの目次用セクション定義 + const baseSections = useMemo(() => { + const list: Array<{ id: SectionId; title: string; level: number }> = []; + if (config?.repl) { + list.push({ id: "sandbox-repl" as SectionId, title: "REPL", level: 2 }); + } + if (config?.editor || userFiles.length > 0) { + list.push({ id: "sandbox-editor" as SectionId, title: "コード", level: 2 }); + } + if (config?.exec) { + list.push({ id: "sandbox-exec" as SectionId, title: "実行", level: 2 }); + } + if (config?.readonlyFiles && config.readonlyFiles.length > 0) { + list.push({ + id: "sandbox-readonly" as SectionId, + title: "出力ファイル", + level: 2, + }); + } + return list; + }, [config, userFiles.length]); + + const [sectionInView, setSectionInView] = useState([]); + const sectionRefs = useRef>(new Map()); + + useEffect(() => { + const handleScroll = () => { + setSectionInView( + baseSections.map((sec) => { + const el = sectionRefs.current.get(sec.id); + if (el) { + const rect = el.getBoundingClientRect(); + return ( + rect.top < window.innerHeight * 0.9 && + rect.bottom >= window.innerHeight * 0.1 + ); + } + return false; + }) + ); + }; + window.addEventListener("scroll", handleScroll); + handleScroll(); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, [baseSections]); + + const dynamicSections: DynamicMarkdownSection[] = useMemo(() => { + return baseSections.map((sec, i) => ({ + id: sec.id, + title: sec.title, + level: sec.level, file: "sandbox.md", rawContent: "", md5: "", replacedContent: "", replacedRange: [], - inView: true, - }, - ]; + inView: sectionInView[i] ?? false, + })); + }, [baseSections, sectionInView]); useEffect(() => { - setSidebarMdContent(path, dummySection); - }, [path, setSidebarMdContent]); + setSidebarMdContent(path, dynamicSections); + }, [dynamicSections, path, setSidebarMdContent]); const handleAddFile = (e: FormEvent) => { e.preventDefault(); @@ -93,44 +140,102 @@ export function SandboxContent(props: SandboxContentProps) {
    + {/* 1. REPL */} {config?.repl && ( -
    +
    { + sectionRefs.current.set("sandbox-repl", el); + }} + > REPL -
    + )} - {config?.editor && ( -
    - サンプルコード - {Object.entries(config.editor).map(([filename, initContent]) => ( + {/* 2. エディター (既存ファイル + 追加ファイル + 追加ボタン) */} + {(config?.editor || userFiles.length > 0 || config?.supportsMultiFile) && ( +
    { + sectionRefs.current.set("sandbox-editor", el); + }} + > + コード + {config?.editor && + Object.entries(config.editor).map(([filename, initContent]) => ( + + ))} + + {userFiles.map((filename) => ( handleRemoveFile(filename)} /> ))} -
    + + {config?.supportsMultiFile && ( +
    +
    + { + setNewFilename(e.target.value); + setFilenameError(null); + }} + /> + +
    + {filenameError && ( +

    {filenameError}

    + )} +
    + )} + )} + {/* 3. 実行 */} {config?.exec && ( -
    +
    { + sectionRefs.current.set("sandbox-exec", el); + }} + > 実行 -
    + )} + {/* 4. 出力ファイル */} {config?.readonlyFiles && config.readonlyFiles.length > 0 && ( -
    +
    { + sectionRefs.current.set("sandbox-readonly", el); + }} + > 出力ファイル {config.readonlyFiles.map((filename) => ( ))} -
    + )} -
    - 追加ファイル -
    - { - setNewFilename(e.target.value); - setFilenameError(null); - }} - /> - -
    - {filenameError && ( -

    {filenameError}

    - )} - - {userFiles.map((filename) => ( -
    -
    - {filename} - -
    - -
    - ))} -
    -
    @@ -199,7 +263,7 @@ export function SandboxContent(props: SandboxContentProps) { setIsFormVisible(false)} />
    From be051ff5d07cb2b7a52d9b3aee1ae6e4cf425a46 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:43:44 +0000 Subject: [PATCH 04/20] feat(sidebar): move Sandbox to top of language list and render its TOC --- app/sidebar.tsx | 59 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 1d98b8b1..09e23061 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -90,7 +90,10 @@ export function Sidebar() { // 現在表示中のセクション(最初にinViewがtrueのもの)を見つける const currentSectionId = sidebarMdContent.find( - (section, i) => i >= 1 && section.inView + (section, i) => + Boolean(section.title) && + (currentPageId === ("sandbox" as PageSlug) || i >= 1) && + section.inView )?.id; // 目次の開閉状態 @@ -181,6 +184,46 @@ export function Sidebar() { {group.name} From a47b975c402c52bf8787c181aaaad72f559005da Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:17:23 +0900 Subject: [PATCH 05/20] =?UTF-8?q?=E8=A6=8B=E3=81=9F=E7=9B=AE=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@docs/[lang]/[pageId]/pageContent.tsx | 27 ++++++---- .../@docs/[lang]/sandbox/sandboxContent.tsx | 51 ++++++++++++------- app/api/chat/route.ts | 47 +++++++++++------ 3 files changed, 84 insertions(+), 41 deletions(-) diff --git a/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx b/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx index 69bbaee7..82dd4160 100644 --- a/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx +++ b/app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx @@ -171,6 +171,7 @@ export function ChatListForSection(props: { dynamicMdContent: DynamicMarkdownSection[]; sectionId: SectionId; chatHistories: ChatWithMessages[]; + fullWidth?: boolean; }) { const { dynamicMdContent, sectionId, chatHistories } = props; const filteredChatHistories = chatHistories.filter( @@ -195,10 +196,14 @@ export function ChatListForSection(props: { */} @@ -199,7 +208,7 @@ export function SandboxContent(props: SandboxContentProps) { {/* 1. REPL */} - {config?.repl && ( + {language.repl && (
    { @@ -210,90 +219,86 @@ export function SandboxContent(props: SandboxContentProps) {
    )} {/* 2. エディター (既存ファイル + 追加ファイル + 追加ボタン) */} - {(config?.editor || - userFiles.length > 0 || - config?.supportsMultiFile) && ( -
    { - sectionRefs.current.set("sandbox-editor", el); - }} - > - コード - {config?.editor && - Object.entries(config.editor).map(([filename, initContent]) => ( +
    { + sectionRefs.current.set("sandbox-editor", el); + }} + > + コード + {language.sampleFiles && + Object.entries(language.sampleFiles).map( + ([filename, initContent]) => ( - ))} - - {userFiles.map((filename) => ( - handleRemoveFile(filename)} - /> - ))} - - {config?.supportsMultiFile && ( -
    -
    - ファイルを追加: - { - setNewFilename(e.target.value); - setFilenameError(null); - }} - /> - -
    - {filenameError && ( -

    {filenameError}

    - )} -
    + ) )} -
    - )} - {/* 3. 実行 */} - {config?.exec && ( -
    { - sectionRefs.current.set("sandbox-exec", el); - }} - > - 実行 - ( + handleRemoveFile(filename)} /> -
    - )} + ))} + + {language.supportsMultiFile && ( +
    +
    + ファイルを追加: + { + setNewFilename(e.target.value); + setFilenameError(null); + }} + /> + +
    + {filenameError && ( +

    {filenameError}

    + )} +
    + )} +
    + + {/* 3. 実行 */} +
    { + sectionRefs.current.set("sandbox-exec", el); + }} + > + 実行 + +
    {/* 4. 出力ファイル */} - {config?.readonlyFiles && config.readonlyFiles.length > 0 && ( + {language.readonlyFiles && language.readonlyFiles.length > 0 && (
    { @@ -301,7 +306,7 @@ export function SandboxContent(props: SandboxContentProps) { }} > 出力ファイル - {config.readonlyFiles.map((filename) => ( + {language.readonlyFiles.map((filename) => ( | false; - exec: string[] | false; - readonlyFiles?: string[]; - supportsMultiFile?: boolean; -} - -export const sampleConfig: Record = { - python: { - repl: true, - replInitContent: '>>> print("Hello, World!")\nHello, World!', - editor: { - "main.py": main_py, - }, - exec: ["main.py"], - supportsMultiFile: true, - }, - ruby: { - repl: true, - replInitContent: 'irb(main):001:0> puts "Hello, World!"\nHello, World!', - editor: { - "main.rb": main_rb, - }, - exec: ["main.rb"], - supportsMultiFile: true, - }, - javascript: { - repl: true, - replInitContent: '> console.log("Hello, World!");\nHello, World!', - editor: { - "main.js": main_js, - }, - exec: ["main.js"], - supportsMultiFile: false, - }, - typescript: { - repl: false, - editor: { - // main.tsにすると出力ファイルがjavascriptのサンプルと被る - "main2.ts": main2_ts, - }, - exec: ["main2.ts"], - readonlyFiles: ["main2.js"], - supportsMultiFile: false, - }, - cpp: { - repl: false, - editor: { - "main.cpp": main_cpp, - "sub.h": sub_h, - "sub.cpp": sub_cpp, - }, - exec: ["main.cpp", "sub.cpp"], - supportsMultiFile: true, - }, - rust: { - repl: false, - editor: { - "main2.rs": main2_rs, - "sub.rs": sub_rs, - }, - exec: ["main2.rs"], - supportsMultiFile: true, - }, -}; diff --git a/packages/runtime/src/languages.ts b/packages/runtime/src/languages.ts index 3c2ceb72..e9988e66 100644 --- a/packages/runtime/src/languages.ts +++ b/packages/runtime/src/languages.ts @@ -1,3 +1,13 @@ +import main_py from "./samples/main.py?raw"; +import main_rb from "./samples/main.rb?raw"; +import main_js from "./samples/main.js?raw"; +import main2_ts from "./samples/main2.ts?raw"; +import main_cpp from "./samples/main.cpp?raw"; +import sub_h from "./samples/sub.h?raw"; +import sub_cpp from "./samples/sub.cpp?raw"; +import main2_rs from "./samples/main2.rs?raw"; +import sub_rs from "./samples/sub.rs?raw"; + // Markdownで指定される可能性のある言語名を列挙 export type MarkdownLang = | "python" @@ -73,8 +83,10 @@ export type LangConstants = { ) & ( | { - runtime: RuntimeLang; // REPLが実装されている言語の場合 + repl: true; + // ReplOutput[] ではない。stringのパースはruntimeが行う + sampleReplInit: string; // terminal/highlight.ts でインポートするprismの言語定義と対応 prism: "python" | "ruby" | "javascript"; prompt: string; @@ -82,12 +94,35 @@ export type LangConstants = { returnPrefix?: string; } | { - runtime?: RuntimeLang; + repl?: false; + sampleReplInit?: undefined; prism?: undefined; prompt?: undefined; promptMore?: undefined; returnPrefix?: undefined; } + ) & + ( + | { + runtime: RuntimeLang; + // Sandboxにデフォルトで用意するファイル + sampleFiles: Record; + // C++のように実行時に全ソースファイルを指定する必要がある言語は、 + // 現在のファイルリストを受け取って実行すべきファイルを返す関数を定義する + sampleExec: (files: string[]) => string[]; + // Sandboxの実行ボタンの下に読み取り専用で表示されるファイル + readonlyFiles?: string[]; + supportsMultiFile: boolean; + supportsFileOutput: boolean; + } + | { + runtime?: undefined; + sampleFiles?: undefined; + sampleExec?: undefined; + readonlyFiles?: undefined; + supportsMultiFile?: undefined; + supportsFileOutput?: undefined; + } ); export function langConstants(lang: MarkdownLang | undefined): LangConstants { @@ -101,8 +136,16 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { tabSize: 4, runtime: "python", prism: "python", + repl: true, + sampleReplInit: '>>> print("Hello, World!")\nHello, World!', prompt: ">>> ", promptMore: "... ", + sampleFiles: { + "main.py": main_py, + }, + sampleExec: () => ["main.py"], + supportsMultiFile: true, + supportsFileOutput: true, }; case "ruby": case "rb": @@ -113,10 +156,18 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { tabSize: 2, runtime: "ruby", prism: "ruby", + repl: true, + sampleReplInit: 'irb(main):001:0> puts "Hello, World!"\nHello, World!', // TODO: 実際のirbのプロンプトは静的でなく、(main)や番号などの動的な表示がある prompt: "irb> ", promptMore: "irb* ", returnPrefix: "=> ", + sampleFiles: { + "main.rb": main_rb, + }, + sampleExec: () => ["main.rb"], + supportsMultiFile: true, + supportsFileOutput: true, }; case "javascript": case "js": @@ -127,8 +178,16 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { tabSize: 2, runtime: "javascript", prism: "javascript", + repl: true, + sampleReplInit: '> console.log("Hello, World!");\nHello, World!', prompt: "> ", promptMore: "... ", + sampleFiles: { + "main.js": main_js, + }, + sampleExec: () => ["main.js"], + supportsMultiFile: false, + supportsFileOutput: false, }; case "typescript": case "ts": @@ -138,6 +197,15 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { ace: "typescript", tabSize: 2, runtime: "typescript", + repl: false, + sampleFiles: { + // main.tsにすると出力ファイルがjavascriptのサンプルと被る + "main2.ts": main2_ts, + }, + sampleExec: () => ["main2.ts"], + readonlyFiles: ["main2.js"], + supportsMultiFile: false, + supportsFileOutput: false, }; case "cpp": case "c++": @@ -148,6 +216,18 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { // 2文字派と4文字派があるが、geminiが4文字で出力するので4でいいや tabSize: 4, runtime: "cpp", + repl: false, + sampleFiles: { + "main.cpp": main_cpp, + "sub.h": sub_h, + "sub.cpp": sub_cpp, + }, + sampleExec: (files: string[]) => + files.filter((f) => + ["c", "cpp", "cc", "cxx"].includes(f.split(".").at(-1) ?? "") + ), + supportsMultiFile: true, + supportsFileOutput: false, }; case "rust": case "rs": @@ -157,6 +237,14 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { ace: "rust", tabSize: 4, runtime: "rust", + repl: false, + sampleFiles: { + "main2.rs": main2_rs, + "sub.rs": sub_rs, + }, + sampleExec: () => ["main2.rs"], + supportsMultiFile: true, + supportsFileOutput: false, }; case "bash": case "sh": diff --git a/app/terminal/samples/main.cpp b/packages/runtime/src/samples/main.cpp similarity index 100% rename from app/terminal/samples/main.cpp rename to packages/runtime/src/samples/main.cpp diff --git a/app/terminal/samples/main.js b/packages/runtime/src/samples/main.js similarity index 100% rename from app/terminal/samples/main.js rename to packages/runtime/src/samples/main.js diff --git a/app/terminal/samples/main.py b/packages/runtime/src/samples/main.py similarity index 100% rename from app/terminal/samples/main.py rename to packages/runtime/src/samples/main.py diff --git a/app/terminal/samples/main.rb b/packages/runtime/src/samples/main.rb similarity index 100% rename from app/terminal/samples/main.rb rename to packages/runtime/src/samples/main.rb diff --git a/app/terminal/samples/main2.rs b/packages/runtime/src/samples/main2.rs similarity index 100% rename from app/terminal/samples/main2.rs rename to packages/runtime/src/samples/main2.rs diff --git a/app/terminal/samples/main2.ts b/packages/runtime/src/samples/main2.ts similarity index 100% rename from app/terminal/samples/main2.ts rename to packages/runtime/src/samples/main2.ts diff --git a/app/terminal/samples/sub.cpp b/packages/runtime/src/samples/sub.cpp similarity index 100% rename from app/terminal/samples/sub.cpp rename to packages/runtime/src/samples/sub.cpp diff --git a/app/terminal/samples/sub.h b/packages/runtime/src/samples/sub.h similarity index 100% rename from app/terminal/samples/sub.h rename to packages/runtime/src/samples/sub.h diff --git a/app/terminal/samples/sub.rs b/packages/runtime/src/samples/sub.rs similarity index 100% rename from app/terminal/samples/sub.rs rename to packages/runtime/src/samples/sub.rs From b2e6c064ecd8b1f029d4f6d87bc12601a3da9e60 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:30:39 +0900 Subject: [PATCH 15/20] =?UTF-8?q?tooltip=E3=81=AE=E8=89=B2=E3=82=92?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/exec.tsx | 6 +++--- app/terminal/repl.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index 842b496c..d3b1e7ed 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -196,9 +196,9 @@ export function ExecFile(props: ExecProps) { -
    +
    ブラウザ上で動作する {runtimeInfo?.prettyLangName || props.language.runtime} @@ -218,7 +218,7 @@ export function ExecFile(props: ExecProps) {
    {/* 4. 出力ファイル */} - {language.readonlyFiles && language.readonlyFiles.length > 0 && ( + {((language.readonlyFiles && language.readonlyFiles.length > 0) || + language.supportsFileOutput) && (
    { @@ -306,15 +346,55 @@ export function SandboxContent(props: SandboxContentProps) { }} > 出力ファイル - {language.readonlyFiles.map((filename) => ( + {language.readonlyFiles && + language.readonlyFiles.map((filename) => ( + + ))} + {userOutputFiles.map((filename) => ( handleRemoveOutputFile(filename)} /> ))} + + {language.supportsFileOutput && ( +
    +
    + 出力ファイルを追加: + { + setNewOutputFilename(e.target.value); + setOutputFilenameError(null); + }} + /> + +
    + {outputFilenameError && ( +

    + {outputFilenameError} +

    + )} +
    + )}
    )} From dee53bb265d72954b989df8e4fba76eb1d3c40be Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:52:50 +0900 Subject: [PATCH 17/20] fix --- app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx | 2 +- app/terminal/mochaTest.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx index e6e39a48..e7ba7f70 100644 --- a/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx +++ b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx @@ -328,7 +328,7 @@ export function SandboxContent(props: SandboxContentProps) { 実行 {/* eslint-disable-next-line @next/next/no-html-link-for-pages */} - - {/* aタグでページをリロードしないと動作しない。 */} + + {/* クエリパラメータの削除。 aタグでページをリロードしないと動作しない。 */} フィルタを解除 From 1978eb26ff075501e4136e0573685faa1b618ca5 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Tue, 18 Aug 2026 00:03:02 +0900 Subject: [PATCH 18/20] refactor --- .../@docs/[lang]/sandbox/sandboxContent.tsx | 121 ++++++++---------- 1 file changed, 50 insertions(+), 71 deletions(-) diff --git a/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx index e7ba7f70..e93a6d8b 100644 --- a/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx +++ b/app/(docs)/@docs/[lang]/sandbox/sandboxContent.tsx @@ -41,26 +41,37 @@ export function SandboxContent(props: SandboxContentProps) { const [userOutputFiles, setUserOutputFiles] = useState([]); const [newOutputFilename, setNewOutputFilename] = useState(""); - const [outputFilenameError, setOutputFilenameError] = useState(null); + const [outputFilenameError, setOutputFilenameError] = useState( + null + ); const [isFormVisible, setIsFormVisible] = useState(false); // サイドバーの目次用セクション定義 + const hasRepl = language.repl; + const hasEditor = true; + const hasAddFile = language.supportsMultiFile; + const hasExec = true; + const hasOutputFile = + (language.readonlyFiles && language.readonlyFiles.length > 0) || + language.supportsFileOutput; + const hasAddOutputFile = language.supportsFileOutput; const baseSections = useMemo(() => { const list: Array<{ id: SectionId; title: string; level: number }> = []; - if (language.repl) { + if (hasRepl) { list.push({ id: "sandbox-repl" as SectionId, title: "REPL", level: 2 }); } - list.push({ - id: "sandbox-editor" as SectionId, - title: "コード", - level: 2, - }); - list.push({ id: "sandbox-exec" as SectionId, title: "実行", level: 2 }); - if ( - (language.readonlyFiles && language.readonlyFiles.length > 0) || - language.supportsFileOutput - ) { + if (hasEditor) { + list.push({ + id: "sandbox-editor" as SectionId, + title: "コード", + level: 2, + }); + } + if (hasExec) { + list.push({ id: "sandbox-exec" as SectionId, title: "実行", level: 2 }); + } + if (hasOutputFile) { list.push({ id: "sandbox-readonly" as SectionId, title: "出力ファイル", @@ -68,7 +79,7 @@ export function SandboxContent(props: SandboxContentProps) { }); } return list; - }, [language]); + }, [hasRepl, hasEditor, hasExec, hasOutputFile]); const [sectionInView, setSectionInView] = useState([]); const sectionRefs = useRef>(new Map()); @@ -184,57 +195,28 @@ export function SandboxContent(props: SandboxContentProps) {
      -
    • - - REPLでの実行 -
    • -
    • - - ファイル実行 -
    • -
    • - ( +
    • - 複数ファイル対応 -
    • -
    • - - ファイル出力対応 -
    • + > + + {name} + + ))}
    {/* 1. REPL */} - {language.repl && ( + {hasRepl && (
    { @@ -293,7 +275,7 @@ export function SandboxContent(props: SandboxContentProps) { /> ))} - {language.supportsMultiFile && ( + {hasAddFile && (
    ファイルを追加: @@ -337,8 +319,7 @@ export function SandboxContent(props: SandboxContentProps) {
    {/* 4. 出力ファイル */} - {((language.readonlyFiles && language.readonlyFiles.length > 0) || - language.supportsFileOutput) && ( + {hasOutputFile && (
    { @@ -367,13 +348,13 @@ export function SandboxContent(props: SandboxContentProps) { /> ))} - {language.supportsFileOutput && ( + {hasAddOutputFile && (
    - 出力ファイルを追加: + 出力を表示するファイルを追加: {outputFilenameError && ( -

    - {outputFilenameError} -

    +

    {outputFilenameError}

    )}
    )} From 7f9adfe9b96710508128ee69597f3ad76f01fdff Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:04:39 +0900 Subject: [PATCH 19/20] =?UTF-8?q?=E6=9C=80=E5=A4=A7=E5=8C=96=E3=83=9C?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E9=96=A2=E9=80=A3=E3=81=AEid=E3=82=92?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=82=B3=E3=83=BC=E3=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/modal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/terminal/modal.tsx b/app/terminal/modal.tsx index 3d84ef9f..8b5bdf8c 100644 --- a/app/terminal/modal.tsx +++ b/app/terminal/modal.tsx @@ -23,7 +23,7 @@ export function Modal(props: Props) { const { id, open, setOpen } = props; useEffect(() => { const onHashChange = () => { - if (location.hash === `#${id}`) { + if (location.hash === "#" + encodeURIComponent(id)) { setOpen(true); } else { // アニメーションが終わった後にmodalモードを解除する @@ -57,7 +57,7 @@ export function Modal(props: Props) { className={clsx(open && "modal h-dvh")} role={open ? "dialog" : undefined} ref={modalDivRef} - id={id} + id={encodeURIComponent(id)} >
    From c248174085508b7c5df731ff4f76d258999153f5 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:21:29 +0900 Subject: [PATCH 20/20] =?UTF-8?q?editor=E3=81=AE=E8=A7=92=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E3=80=81=E3=81=A4=E3=81=84=E3=81=A7=E3=81=AB?= =?UTF-8?q?padding=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/editor.tsx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index b2f910cd..d3b67d14 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -67,7 +67,7 @@ export function EditorComponent(props: EditorProps) { // 現在の内容の行数、最小8行、最大50vh const editorHeight = Math.max( Math.min( - code.split("\n").length, + code.split("\n").length + 1, Math.floor((windowHeight * 0.5) / ((fontSize || 16) + 1)) ), 8 @@ -87,11 +87,11 @@ export function EditorComponent(props: EditorProps) { return ( -
    +
    {props.readonly @@ -182,7 +182,9 @@ export function EditorComponent(props: EditorProps) { {fontSize !== undefined && initAce ? ( {code} + + {code} + } > ) : ( - + {code} )} @@ -217,16 +223,21 @@ function FallbackPre({ children, editorHeight, isModal, + className, }: { children: string; editorHeight: number; isModal?: boolean; + className?: string; }) { // AceEditorはなぜかline-heightが小さい // fontSize + 1px になるっぽい? return (