+
{langEntry?.name ?? langId} Sandbox
+
+ ブラウザ上で動作する {langEntry?.name} の実行環境です。
+ このページでは自由にコードを書いて試したり、コードについてAIに質問することもできます。
+
+
+
+ -
+
+ REPLでの実行
+
+ -
+
+ ファイル実行
+
+ -
+
+ 複数ファイル対応
+
+
+
+
+
+
+ {/* 1. REPL */}
+ {config?.repl && (
+
{
+ sectionRefs.current.set("sandbox-repl", el);
+ }}
+ >
+ REPL
+
+
+ )}
+
+ {/* 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 && (
+
+
+ {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) => (
+
+ ))}
+
+ )}
+
+ {isFormVisible ? (
+
+ setIsFormVisible(false)}
+ />
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts
index 803b59cf..86d9e656 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 {
@@ -111,7 +112,10 @@ export async function POST(request: NextRequest) {
?.replace(/^slug:/i, "")
.replace(/^["'`]+|["'`.,:;]+$/g, "")
.trim() as typeof path.page | undefined;
- if (selectedPageSlug && langEntry.pages.some((page) => page.slug === selectedPageSlug)) {
+ if (
+ selectedPageSlug &&
+ langEntry.pages.some((page) => page.slug === selectedPageSlug)
+ ) {
targetPath = {
lang: path.lang,
page: selectedPageSlug,
@@ -129,52 +133,69 @@ 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(
- `ユーザーはドキュメント内の ${sectionTitlesInView.join(", ")} の付近のセクションを閲覧している際にこの質問を行っていると推測されます。`
+ `あなたは${langName}プログラミングの学習者をサポートする講師AIアシスタントです。`
);
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(``);
- if (Object.keys(replOutputs).length > 0) {
+ } else {
+ prompt.push(`あなたは${langName}言語のチュートリアルの講師をしています。`);
prompt.push(
- `# ターミナルのログ(ユーザーが入力したコマンドとその実行結果)`
+ `以下の${langName}チュートリアルのドキュメントの内容を正確に理解し、ユーザーからの質問に対して、初心者にも分かりやすく、丁寧な解説を提供してください。`
);
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(
- "例えば ```python-repl:foo のコードブロックに対してユーザーが実行したログが ターミナル #foo です。"
+ `質問内容とドキュメントの内容の関連性が深く、比較的長めの解説をしたい場合、またはドキュメントへの補足がしたい場合は、そちらの形式での回答を検討してください。`
);
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(``);
+ }
+
+ if (Object.keys(replOutputs).length > 0) {
+ prompt.push(
+ `# ターミナルのログ(ユーザーが入力したコマンドとその実行結果)`
+ );
+ prompt.push(``);
+ if (isSandbox) {
+ prompt.push("以下はユーザーがREPLで実行したコマンドです。");
+ } else {
+ prompt.push(
+ "以下はドキュメント内で実行例を示した各コードブロックの内容に加えてユーザーが追加で実行したコマンドです。"
+ );
+ prompt.push(
+ "例えば ```python-repl:foo のコードブロックに対してユーザーが実行したログが ターミナル #foo です。"
+ );
+ }
+ prompt.push(``);
for (const [replId, replCommands] of Object.entries(replOutputs)) {
prompt.push(`## ターミナル #${replId}`);
for (const replCmd of replCommands) {
@@ -192,12 +213,16 @@ export async function POST(request: NextRequest) {
if (Object.keys(files).length > 0) {
prompt.push("# ファイルエディターの内容");
prompt.push(``);
- prompt.push(
- "以下はドキュメント内でファイルの内容を示した各コードブロックの内容に加えてユーザーが編集を加えたものです。"
- );
- prompt.push(
- "例えば ```python:foo.py のコードブロックに対してユーザーが編集した後の内容が ファイル: foo.py です。"
- );
+ if (isSandbox) {
+ prompt.push("以下はユーザーが編集したファイルの内容です。");
+ } else {
+ prompt.push(
+ "以下はドキュメント内でファイルの内容を示した各コードブロックの内容に加えてユーザーが編集を加えたものです。"
+ );
+ prompt.push(
+ "例えば ```python:foo.py のコードブロックに対してユーザーが編集した後の内容が ファイル: foo.py です。"
+ );
+ }
prompt.push(``);
for (const [filename, content] of Object.entries(files)) {
prompt.push(`## ファイル: ${filename}`);
@@ -224,15 +249,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 +269,7 @@ export async function POST(request: NextRequest) {
" - 太字やコードブロックなどのMarkdownの記法は使わずテキストのみで出力してください。"
);
prompt.push(
- "- 3行目以降に、ドキュメントの内容に基づいて、ユーザーに伝える回答をMarkdown形式で記述してください。"
+ "- 3行目以降に、ユーザーに伝える回答をMarkdown形式で記述してください。"
);
prompt.push(
" - ユーザーが入力したターミナルのコマンドやファイルの内容、実行結果を参考にして回答してください。"
@@ -250,29 +279,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 +335,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);
}
@@ -421,7 +456,11 @@ export async function POST(request: NextRequest) {
// クライアントでもrevalidateChatActionを呼ぶが、一応こちらでもrevalidateしておく
if (deleteChatOnCreated) {
- await revalidateChatOnDemand(deleteChatOnCreated, context.userId!, path);
+ await revalidateChatOnDemand(
+ deleteChatOnCreated,
+ context.userId!,
+ path
+ );
}
await revalidateChatOnDemand(chatId, context.userId!, path);
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