From 21d4fe96fb32f0d5c83f39e11a397c1f7151ca28 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:02:27 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E7=94=BB=E9=9D=A2=E5=86=85=E3=81=AE?= =?UTF-8?q?=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=99=E3=82=8B=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/page.tsx | 10 ++----- app/[docs_id]/pageContent.tsx | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 app/[docs_id]/pageContent.tsx diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index 0900973..01049bf 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -5,6 +5,7 @@ import { join } from "node:path"; import { MarkdownSection, splitMarkdown } from "./splitMarkdown"; import { Section } from "./section"; import pyodideLock from "pyodide/pyodide-lock.json"; +import { PageContent } from "./pageContent"; export default async function Page({ params, @@ -45,14 +46,7 @@ export default async function Page({ return (
- {splitMdContent.map((section, index) => { - const sectionId = `${docs_id}-${index}`; - return ( -
-
-
- ); - })} +
); } diff --git a/app/[docs_id]/pageContent.tsx b/app/[docs_id]/pageContent.tsx new file mode 100644 index 0000000..e64d5ec --- /dev/null +++ b/app/[docs_id]/pageContent.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Section } from "./section"; +import { MarkdownSection } from "./splitMarkdown"; + +interface PageContentProps { + splitMdContent: MarkdownSection[]; + docs_id: string; +} +export function PageContent(props: PageContentProps) { + // 各セクションが画面内にあるかどうかを調べる + const [sectionInView, setSectionInView] = useState([]); + const sectionRefs = useRef>([]); + // sectionRefsの長さをsplitMdContentに合わせる + while (sectionRefs.current.length < props.splitMdContent.length) { + sectionRefs.current.push(null); + } + sectionRefs.current = sectionRefs.current.slice( + 0, + props.splitMdContent.length + ); + + useEffect(() => { + const handleScroll = () => { + const newSectionInView = sectionRefs.current.map((sectionRef) => { + if (sectionRef) { + const rect = sectionRef.getBoundingClientRect(); + return rect.top < window.innerHeight && rect.bottom >= 0; + } + return false; + }); + setSectionInView(newSectionInView); + }; + window.addEventListener("scroll", handleScroll); + handleScroll(); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + return props.splitMdContent.map((section, index) => { + const sectionId = `${props.docs_id}-${index}`; + return ( +
{ + sectionRefs.current[index] = el; + }} + > +
+
+ ); + }); +} From f6b6471a177e1f62c94e3cf219179c3ad21ed51d Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:29:35 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E8=B3=AA=E5=95=8F=E3=83=9C=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=B9=E3=82=92=E7=94=BB=E9=9D=A2=E3=81=AB=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 197 +++++++++++++--------------------- app/[docs_id]/page.tsx | 6 +- app/[docs_id]/pageContent.tsx | 52 ++++++--- app/[docs_id]/section.tsx | 7 -- 4 files changed, 113 insertions(+), 149 deletions(-) diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index feec78d..452bf20 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -9,10 +9,13 @@ import useSWR from "swr"; import { getQuestionExample } from "../actions/questionExample"; import { getLanguageName } from "../pagesList"; import { ReplCommand, ReplOutput } from "../terminal/repl"; +import { MarkdownSection } from "./splitMarkdown"; interface ChatFormProps { - documentContent: string; - sectionId: string; + docs_id: string; + splitMdContent: MarkdownSection[]; + sectionInView: boolean[]; + onClose: () => void; replOutputs: ReplCommand[]; fileContents: Array<{ name: string; @@ -22,21 +25,30 @@ interface ChatFormProps { } export function ChatForm({ - documentContent, - sectionId, + docs_id, + splitMdContent, + sectionInView, + onClose, replOutputs, fileContents, execResults, }: ChatFormProps) { - const [messages, updateChatHistory] = useChatHistory(sectionId); + // const [messages, updateChatHistory] = useChatHistory(sectionId); const [inputValue, setInputValue] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [isFormVisible, setIsFormVisible] = useState(false); - const lang = getLanguageName(sectionId); + const lang = getLanguageName(docs_id); + + const documentContentInView = splitMdContent + .filter((_, index) => sectionInView[index]) + .map( + (section) => + `${"#".repeat(section.level)} ${section.title}\n${section.content}` + ) + .join("\n\n"); const { data: exampleData, error: exampleError } = useSWR( // 質問フォームを開いたときだけで良い - isFormVisible ? { lang, documentContent } : null, + { lang, documentContentInView }, getQuestionExample, { // リクエストは古くても構わないので1回でいい @@ -91,126 +103,65 @@ export function ChatForm({ setIsLoading(false); }; - const handleClearHistory = () => { - updateChatHistory([]); - }; - return ( - <> - {isFormVisible && ( -
+
+ -
-
setInputValue(e.target.value)} + disabled={isLoading} + > +
+
+
+ -
-
- -
-
-
- )} - {!isFormVisible && ( - - )} - - {messages.length > 0 && ( -
-
-

AIとのチャット

- -
- {messages.map((msg, index) => ( -
-
- -
-
- ))} -
- )} - - {isLoading && ( -
- AIが考え中です… + 閉じる + +
+
+
- )} - + + ); } diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index 01049bf..74f5603 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -44,9 +44,5 @@ export default async function Page({ const splitMdContent: MarkdownSection[] = await splitMarkdown(mdContent); - return ( -
- -
- ); + return ; } diff --git a/app/[docs_id]/pageContent.tsx b/app/[docs_id]/pageContent.tsx index e64d5ec..7f699c1 100644 --- a/app/[docs_id]/pageContent.tsx +++ b/app/[docs_id]/pageContent.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react"; import { Section } from "./section"; import { MarkdownSection } from "./splitMarkdown"; +import { ChatForm } from "./chatForm"; interface PageContentProps { splitMdContent: MarkdownSection[]; @@ -39,18 +40,41 @@ export function PageContent(props: PageContentProps) { }; }, []); - return props.splitMdContent.map((section, index) => { - const sectionId = `${props.docs_id}-${index}`; - return ( -
{ - sectionRefs.current[index] = el; - }} - > -
-
- ); - }); + const [isFormVisible, setIsFormVisible] = useState(false); + + return ( +
+ {props.splitMdContent.map((section, index) => { + const sectionId = `${props.docs_id}-${index}`; + return ( +
{ + sectionRefs.current[index] = el; + }} + > +
+
+ ); + })} + {isFormVisible ? ( +
+ setIsFormVisible(false)} + /> +
+ ) : ( + + )} +
+ ); } diff --git a/app/[docs_id]/section.tsx b/app/[docs_id]/section.tsx index 4aada64..c566cfa 100644 --- a/app/[docs_id]/section.tsx +++ b/app/[docs_id]/section.tsx @@ -73,13 +73,6 @@ export function Section({ section, sectionId }: SectionProps) {
{section.title} -
); From 9c5f481185b491612d95931c459516079fbfbb23 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:44:03 +0900 Subject: [PATCH 03/10] =?UTF-8?q?SectionContext=E3=81=AE=E5=AF=BE=E8=B1=A1?= =?UTF-8?q?=E3=82=92Section=E5=86=85=E3=81=8B=E3=82=89=E3=83=9A=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E5=85=A8=E4=BD=93=E3=81=B8=E3=81=A8=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 38 +++++++++--------- app/[docs_id]/embedContext.tsx | 70 ++++++++++++++++++++++++++++++++++ app/[docs_id]/page.tsx | 8 +++- app/[docs_id]/pageContent.tsx | 1 + app/[docs_id]/section.tsx | 69 +++------------------------------ app/actions/chatActions.ts | 2 +- app/terminal/editor.tsx | 8 ++-- app/terminal/exec.tsx | 4 +- app/terminal/repl.tsx | 9 +++-- 9 files changed, 112 insertions(+), 97 deletions(-) create mode 100644 app/[docs_id]/embedContext.tsx diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index 452bf20..fa86557 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -1,27 +1,20 @@ "use client"; -import { useState, FormEvent } from "react"; -import clsx from "clsx"; +import { useState, FormEvent, useEffect } from "react"; import { askAI } from "@/app/actions/chatActions"; -import { StyledMarkdown } from "./markdown"; -import { useChatHistory, type Message } from "../hooks/useChathistory"; +import { type Message } from "../hooks/useChathistory"; import useSWR from "swr"; import { getQuestionExample } from "../actions/questionExample"; import { getLanguageName } from "../pagesList"; -import { ReplCommand, ReplOutput } from "../terminal/repl"; import { MarkdownSection } from "./splitMarkdown"; +import { useEmbed } from "./embedContext"; +import { useFile } from "../terminal/file"; interface ChatFormProps { docs_id: string; splitMdContent: MarkdownSection[]; sectionInView: boolean[]; onClose: () => void; - replOutputs: ReplCommand[]; - fileContents: Array<{ - name: string; - content: string; - }>; - execResults: Record; } export function ChatForm({ @@ -29,9 +22,6 @@ export function ChatForm({ splitMdContent, sectionInView, onClose, - replOutputs, - fileContents, - execResults, }: ChatFormProps) { // const [messages, updateChatHistory] = useChatHistory(sectionId); const [inputValue, setInputValue] = useState(""); @@ -39,6 +29,9 @@ export function ChatForm({ const lang = getLanguageName(docs_id); + const { replOutputs, execResults } = useEmbed()!; + const { files } = useFile(); + const documentContentInView = splitMdContent .filter((_, index) => sectionInView[index]) .map( @@ -63,13 +56,18 @@ export function ChatForm({ // 質問フォームを開くたびにランダムに選び直し、 // exampleData[Math.floor(exampleChoice * exampleData.length)] を採用する const [exampleChoice, setExampleChoice] = useState(0); // 0〜1 + useEffect(() => { + if (exampleChoice === 0) { + setExampleChoice(Math.random()); + } + }, [exampleChoice]); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); const userMessage: Message = { sender: "user", text: inputValue }; - updateChatHistory([userMessage]); + // updateChatHistory([userMessage]); let userQuestion = inputValue; if (!userQuestion && exampleData) { @@ -81,9 +79,10 @@ export function ChatForm({ const result = await askAI({ userQuestion, - documentContent: documentContent, + splitMdContent, + sectionInView, replOutputs, - fileContents, + files, execResults, }); @@ -93,10 +92,10 @@ export function ChatForm({ text: `エラー: ${result.error}`, isError: true, }; - updateChatHistory([userMessage, errorMessage]); + // updateChatHistory([userMessage, errorMessage]); } else { const aiMessage: Message = { sender: "ai", text: result.response }; - updateChatHistory([userMessage, aiMessage]); + // updateChatHistory([userMessage, aiMessage]); setInputValue(""); } @@ -109,7 +108,6 @@ export function ChatForm({ style={{ width: "100%", textAlign: "center", - boxShadow: "-moz-initial", }} onSubmit={handleSubmit} > diff --git a/app/[docs_id]/embedContext.tsx b/app/[docs_id]/embedContext.tsx new file mode 100644 index 0000000..7d1d5df --- /dev/null +++ b/app/[docs_id]/embedContext.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { + createContext, + ReactNode, + useCallback, + useContext, + useState, +} from "react"; +import { ReplCommand, ReplOutput } from "../terminal/repl"; + +// セクション内に埋め込まれているターミナルとファイルエディターの内容をSection側から取得できるよう、 +// Contextに保存する +// TODO: C++では複数ファイルを実行する場合がありうるが、ここではfilenameを1つしか受け付けない想定になっている +interface IEmbedContext { + addReplOutput: ( + terminalId: string, + command: string, + output: ReplOutput[] + ) => void; + setExecResult: (filename: string, output: ReplOutput[]) => void; + + replOutputs: Record; + execResults: Record; +} +const EmbedContext = createContext(null); +export const useEmbed = () => useContext(EmbedContext); + +export function EmbedContextProvider({ children }: { children: ReactNode }) { + const [replOutputs, setReplOutputs] = useState>( + {} + ); + const [execResults, setExecResults] = useState>( + {} + ); + const addReplOutput = useCallback( + (terminalId: string, command: string, output: ReplOutput[]) => + setReplOutputs((outs) => ({ + ...outs, + terminalId: [...(outs[terminalId] ?? []), { command, output }], + })), + [] + ); + const setExecResult = useCallback( + (filename: string, output: ReplOutput[]) => + setExecResults((results) => { + results[filename] = output; + return results; + }), + [] + ); + + // replOutputs: section内にあるターミナルにユーザーが入力したコマンドとその実行結果 + // fileContents: section内にあるファイルエディターの内容 + // execResults: section内にあるファイルの実行結果 + // console.log(section.title, replOutputs, fileContents, execResults); + + return ( + + {children} + + ); +} diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index 74f5603..9237154 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -3,9 +3,9 @@ import { getCloudflareContext } from "@opennextjs/cloudflare"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { MarkdownSection, splitMarkdown } from "./splitMarkdown"; -import { Section } from "./section"; import pyodideLock from "pyodide/pyodide-lock.json"; import { PageContent } from "./pageContent"; +import { EmbedContextProvider } from "./embedContext"; export default async function Page({ params, @@ -44,5 +44,9 @@ export default async function Page({ const splitMdContent: MarkdownSection[] = await splitMarkdown(mdContent); - return ; + return ( + + + + ); } diff --git a/app/[docs_id]/pageContent.tsx b/app/[docs_id]/pageContent.tsx index 7f699c1..b9bd744 100644 --- a/app/[docs_id]/pageContent.tsx +++ b/app/[docs_id]/pageContent.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react"; import { Section } from "./section"; import { MarkdownSection } from "./splitMarkdown"; import { ChatForm } from "./chatForm"; +import { useEmbed } from "./embedContext"; interface PageContentProps { splitMdContent: MarkdownSection[]; diff --git a/app/[docs_id]/section.tsx b/app/[docs_id]/section.tsx index c566cfa..b0a5c06 100644 --- a/app/[docs_id]/section.tsx +++ b/app/[docs_id]/section.tsx @@ -1,28 +1,8 @@ "use client"; -import { - createContext, - ReactNode, - useCallback, - useContext, - useState, -} from "react"; +import { ReactNode } from "react"; import { type MarkdownSection } from "./splitMarkdown"; import { StyledMarkdown } from "./markdown"; -import { ChatForm } from "./chatForm"; -import { ReplCommand, ReplOutput } from "../terminal/repl"; -import { useFile } from "../terminal/file"; - -// セクション内に埋め込まれているターミナルとファイルエディターの内容をSection側から取得できるよう、 -// Contextに保存する -// TODO: C++では複数ファイルを実行する場合がありうるが、ここではfilenameを1つしか受け付けない想定になっている -interface ISectionCodeContext { - addReplOutput: (command: string, output: ReplOutput[]) => void; - addFile: (filename: string) => void; - setExecResult: (filename: string, output: ReplOutput[]) => void; -} -const SectionCodeContext = createContext(null); -export const useSectionCode = () => useContext(SectionCodeContext); interface SectionProps { section: MarkdownSection; @@ -31,50 +11,11 @@ interface SectionProps { // 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする export function Section({ section, sectionId }: SectionProps) { - const [replOutputs, setReplOutputs] = useState([]); - const [execResults, setExecResults] = useState>( - {} - ); - const [filenames, setFilenames] = useState([]); - const { files } = useFile(); - const fileContents: { name: string; content: string }[] = filenames.map( - (name) => ({ name, content: files[name] || "" }) - ); - const addReplOutput = useCallback( - (command: string, output: ReplOutput[]) => - setReplOutputs((outs) => [...outs, { command, output }]), - [] - ); - const addFile = useCallback( - (filename: string) => - setFilenames((filenames) => - filenames.includes(filename) ? filenames : [...filenames, filename] - ), - [] - ); - const setExecResult = useCallback( - (filename: string, output: ReplOutput[]) => - setExecResults((results) => { - results[filename] = output; - return results; - }), - [] - ); - - // replOutputs: section内にあるターミナルにユーザーが入力したコマンドとその実行結果 - // fileContents: section内にあるファイルエディターの内容 - // execResults: section内にあるファイルの実行結果 - // console.log(section.title, replOutputs, fileContents, execResults); - return ( - -
- {section.title} - -
-
+ <> + {section.title} + + ); } diff --git a/app/actions/chatActions.ts b/app/actions/chatActions.ts index b126e93..c443ba5 100644 --- a/app/actions/chatActions.ts +++ b/app/actions/chatActions.ts @@ -12,7 +12,7 @@ const ChatSchema = z.object({ userQuestion: z .string() .min(1, { message: "メッセージを入力してください。" }), - documentContent: z + splitMdContent: z .string() .min(1, { message: "コンテキストとなるドキュメントがありません。" }), replOutputs: z diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index bed41ed..4ff26a8 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -22,7 +22,6 @@ const AceEditor = dynamic( import "./editor.css"; import { useFile } from "./file"; import { useEffect } from "react"; -import { useSectionCode } from "../[docs_id]/section"; import clsx from "clsx"; import { useChangeTheme } from "../[docs_id]/themeToggle"; // snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python"; @@ -41,14 +40,13 @@ export function EditorComponent(props: EditorProps) { const theme = useChangeTheme(); const { files, writeFile } = useFile(); const code = files[props.filename] || props.initContent; - const sectionContext = useSectionCode(); - const addSectionFile = sectionContext?.addFile; + // const addSectionFile = sectionContext?.addFile; useEffect(() => { if (!files[props.filename]) { writeFile(props.filename, props.initContent); } - addSectionFile?.(props.filename); - }, [files, props.filename, props.initContent, writeFile, addSectionFile]); + // addSectionFile?.(props.filename); + }, [files, props.filename, props.initContent, writeFile]); return (
diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index 7bb2a29..461fcfd 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -8,10 +8,10 @@ import { systemMessageColor, useTerminal, } from "./terminal"; -import { useSectionCode } from "../[docs_id]/section"; import { useWandbox } from "./wandbox/wandbox"; import { ReplOutput, writeOutput } from "./repl"; import { useState } from "react"; +import { useEmbed } from "../[docs_id]/embedContext"; export type ExecLang = "python" | "cpp"; @@ -34,7 +34,7 @@ export function ExecFile(props: ExecProps) { } }, }); - const sectionContext = useSectionCode(); + const sectionContext = useEmbed(); const pyodide = usePyodide(); const wandbox = useWandbox(); diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index 5cbda66..0bc859c 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -12,8 +12,8 @@ import { systemMessageColor, useTerminal, } from "./terminal"; -import { useSectionCode } from "../[docs_id]/section"; import { Terminal } from "@xterm/xterm"; +import { useEmbed } from "../[docs_id]/embedContext"; export interface ReplOutput { type: "stdout" | "stderr" | "error" | "return" | "trace" | "system"; // 出力の種類 @@ -58,6 +58,7 @@ export function writeOutput( } interface ReplComponentProps { + terminalId: string; initRuntime: () => void; runtimeInitializing: boolean; runtimeReady: boolean; @@ -79,10 +80,11 @@ export function ReplTerminal(props: ReplComponentProps) { const inputBuffer = useRef([]); const initDone = useRef(false); - const sectionContext = useSectionCode(); + const sectionContext = useEmbed(); const addReplOutput = sectionContext?.addReplOutput; const { + terminalId, initRuntime, runtimeInitializing, runtimeReady, @@ -256,7 +258,7 @@ export function ReplTerminal(props: ReplComponentProps) { sendCommand(command) ); onOutput(outputs); - addReplOutput?.(command, outputs); + addReplOutput?.(terminalId, command, outputs); } } else if (code === 127) { // Backspace @@ -293,6 +295,7 @@ export function ReplTerminal(props: ReplComponentProps) { } }, [ + terminalId, updateBuffer, sendCommand, onOutput, From bfaf9f3e3c04048e8e50bbdcba0d60e46f200bfc Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Thu, 23 Oct 2025 02:57:04 +0900 Subject: [PATCH 04/10] =?UTF-8?q?AI=E3=81=AB=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E5=85=A8=E4=BD=93=E3=81=A8=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=83=9F=E3=83=8A=E3=83=AB=E3=83=AD=E3=82=B0=E5=85=A8?= =?UTF-8?q?=E3=81=A6=E3=82=92=E6=B8=A1=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 4 ++ app/[docs_id]/embedContext.tsx | 15 +++-- app/[docs_id]/markdown.tsx | 33 +++++----- app/[docs_id]/page.tsx | 6 +- app/[docs_id]/pageContent.tsx | 2 + app/actions/chatActions.ts | 107 ++++++++++++++++++------------- app/terminal/python/embedded.tsx | 9 ++- 7 files changed, 109 insertions(+), 67 deletions(-) diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index fa86557..5cd4be8 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -12,6 +12,7 @@ import { useFile } from "../terminal/file"; interface ChatFormProps { docs_id: string; + documentContent: string; splitMdContent: MarkdownSection[]; sectionInView: boolean[]; onClose: () => void; @@ -19,6 +20,7 @@ interface ChatFormProps { export function ChatForm({ docs_id, + documentContent, splitMdContent, sectionInView, onClose, @@ -79,6 +81,7 @@ export function ChatForm({ const result = await askAI({ userQuestion, + documentContent, splitMdContent, sectionInView, replOutputs, @@ -95,6 +98,7 @@ export function ChatForm({ // updateChatHistory([userMessage, errorMessage]); } else { const aiMessage: Message = { sender: "ai", text: result.response }; + console.log(aiMessage); // updateChatHistory([userMessage, aiMessage]); setInputValue(""); } diff --git a/app/[docs_id]/embedContext.tsx b/app/[docs_id]/embedContext.tsx index 7d1d5df..deeda93 100644 --- a/app/[docs_id]/embedContext.tsx +++ b/app/[docs_id]/embedContext.tsx @@ -35,10 +35,17 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) { ); const addReplOutput = useCallback( (terminalId: string, command: string, output: ReplOutput[]) => - setReplOutputs((outs) => ({ - ...outs, - terminalId: [...(outs[terminalId] ?? []), { command, output }], - })), + setReplOutputs((outs) => { + outs = { ...outs }; + if (!(terminalId in outs)) { + outs[terminalId] = []; + } + outs[terminalId] = [ + ...outs[terminalId], + { command: command, output: output }, + ]; + return outs; + }), [] ); const setExecResult = useCallback( diff --git a/app/[docs_id]/markdown.tsx b/app/[docs_id]/markdown.tsx index ffdfd0a..49fda76 100644 --- a/app/[docs_id]/markdown.tsx +++ b/app/[docs_id]/markdown.tsx @@ -104,6 +104,23 @@ function CodeComponent({
); } + } else if (match[2] === "-repl") { + // repl付きの言語指定 + // 現状はPythonのみ対応 + switch (match[1]) { + case "python": + return ( +
+ +
+ ); + default: + console.warn(`Unsupported language for repl: ${match[1]}`); + break; + } } else if (match[3]) { // ファイル名指定がある場合、ファイルエディター let aceLang: AceLang | undefined = undefined; @@ -140,22 +157,6 @@ function CodeComponent({ /> ); - } else if (match[2] === "-repl") { - // repl付きの言語指定 - // 現状はPythonのみ対応 - switch (match[1]) { - case "python": - return ( -
- -
- ); - default: - console.warn(`Unsupported language for repl: ${match[1]}`); - break; - } } return ( - + ); } diff --git a/app/[docs_id]/pageContent.tsx b/app/[docs_id]/pageContent.tsx index b9bd744..543f8dd 100644 --- a/app/[docs_id]/pageContent.tsx +++ b/app/[docs_id]/pageContent.tsx @@ -7,6 +7,7 @@ import { ChatForm } from "./chatForm"; import { useEmbed } from "./embedContext"; interface PageContentProps { + documentContent: string; splitMdContent: MarkdownSection[]; docs_id: string; } @@ -62,6 +63,7 @@ export function PageContent(props: PageContentProps) { {isFormVisible ? (
; @@ -78,19 +76,26 @@ export async function askAI(params: ChatParams): Promise { const { userQuestion, documentContent, + splitMdContent, + sectionInView, replOutputs, - fileContents, + files, execResults, } = parseResult.data; try { // ターミナルログの文字列を構築 let terminalLogsSection = ""; - if (replOutputs && replOutputs.length > 0) { - terminalLogsSection = - "\n# ターミナルのログ(ユーザーが入力したコマンドとその実行結果)\n"; - for (const replCmd of replOutputs) { - terminalLogsSection += `\n## コマンド: ${replCmd.command}\n`; + terminalLogsSection = + "\n# ターミナルのログ(ユーザーが入力したコマンドとその実行結果)\n"; + terminalLogsSection += + "\n以下はドキュメント内で実行例を示した各コードブロックの内容に加えてユーザーが追加で実行したコマンドです。\n"; + terminalLogsSection += + "例えば ```python-repl:1 のコードブロックに対してユーザーが実行したログが ターミナル #1 です。\n"; + for (const [replId, replInstance] of Object.entries(replOutputs)) { + terminalLogsSection += `\n## ターミナル #${replId}\n`; + for (const replCmd of replInstance) { + terminalLogsSection += `\n- コマンド: ${replCmd.command}\n`; terminalLogsSection += "```\n"; for (const output of replCmd.output) { terminalLogsSection += `${output.message}\n`; @@ -101,19 +106,21 @@ export async function askAI(params: ChatParams): Promise { // ファイルエディターの内容を構築 let fileContentsSection = ""; - if (fileContents && fileContents.length > 0) { - fileContentsSection = "\n# ファイルエディターの内容\n"; - for (const file of fileContents) { - fileContentsSection += `\n## ファイル: ${file.name}\n`; - fileContentsSection += "```\n"; - fileContentsSection += file.content; - fileContentsSection += "\n```\n"; - } + fileContentsSection = "\n# ファイルエディターの内容\n"; + fileContentsSection += + "\n以下はドキュメント内でファイルの内容を示した各コードブロックの内容に加えてユーザーが編集を加えたものです。\n"; + fileContentsSection += + "例えば ```python:foo.py のコードブロックに対してユーザーが編集した後の内容が ファイル: foo.py です。\n"; + for (const [filename, content] of Object.entries(files)) { + fileContentsSection += `\n## ファイル: ${filename}\n`; + fileContentsSection += "```\n"; + fileContentsSection += content; + fileContentsSection += "\n```\n"; } // ファイル実行結果を構築 let execResultsSection = ""; - if (execResults && Object.keys(execResults).length > 0) { + if (execResults) { execResultsSection = "\n# ファイルの実行結果\n"; for (const [filename, outputs] of Object.entries(execResults)) { execResultsSection += `\n## ファイル: ${filename}\n`; @@ -125,12 +132,21 @@ export async function askAI(params: ChatParams): Promise { } } + const sectionTitlesInView = splitMdContent.filter((_, index) => sectionInView[index]).map(section => section.title).join(", "); + const prompt = ` 以下のPythonチュートリアルのドキュメントの内容を正確に理解し、ユーザーからの質問に対して、初心者にも分かりやすく、丁寧な解説を提供してください。 +ユーザーはドキュメント内の ${sectionTitlesInView} の付近のセクションを閲覧している際にこの質問を行っていると推測されます。 +質問に答える際には、ユーザーが閲覧しているセクションの内容を特に考慮してください。 + # ドキュメント ${documentContent} -${terminalLogsSection}${fileContentsSection}${execResultsSection} + +${terminalLogsSection} +${fileContentsSection} +${execResultsSection} + # ユーザーからの質問 ${userQuestion} @@ -143,6 +159,7 @@ ${userQuestion} - `; +console.log(prompt) const result = await generateContent(prompt); const text = result.text; if (!text) { diff --git a/app/terminal/python/embedded.tsx b/app/terminal/python/embedded.tsx index cd7a4d2..26e9788 100644 --- a/app/terminal/python/embedded.tsx +++ b/app/terminal/python/embedded.tsx @@ -4,13 +4,20 @@ import { useMemo } from "react"; import { ReplTerminal, ReplOutput, ReplCommand } from "../repl"; import { usePyodide } from "./pyodide"; -export function PythonEmbeddedTerminal({ content }: { content: string }) { +export function PythonEmbeddedTerminal({ + terminalId, + content, +}: { + terminalId: string; + content: string; +}) { const initCommands = useMemo(() => splitContents(content), [content]); const { init, initializing, ready, runPython, checkSyntax, mutex } = usePyodide(); return ( Date: Fri, 24 Oct 2025 00:08:09 +0900 Subject: [PATCH 05/10] =?UTF-8?q?embedContent=E3=81=A8mdSection=E3=82=92?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[docs_id]/chatForm.tsx | 34 ++--- app/[docs_id]/embedContext.tsx | 77 ---------- app/[docs_id]/page.tsx | 15 +- app/[docs_id]/pageContent.tsx | 63 +++++--- app/[docs_id]/splitMarkdown.ts | 5 + app/actions/chatActions.ts | 237 ++++++++++++++----------------- app/layout.tsx | 6 +- app/terminal/editor.tsx | 6 +- app/terminal/embedContext.tsx | 128 +++++++++++++++++ app/terminal/exec.tsx | 4 +- app/terminal/file.tsx | 60 -------- app/terminal/python/pyodide.tsx | 4 +- app/terminal/repl.tsx | 4 +- app/terminal/wandbox/wandbox.tsx | 4 +- 14 files changed, 312 insertions(+), 335 deletions(-) delete mode 100644 app/[docs_id]/embedContext.tsx create mode 100644 app/terminal/embedContext.tsx delete mode 100644 app/terminal/file.tsx diff --git a/app/[docs_id]/chatForm.tsx b/app/[docs_id]/chatForm.tsx index 5cd4be8..2a4c07f 100644 --- a/app/[docs_id]/chatForm.tsx +++ b/app/[docs_id]/chatForm.tsx @@ -6,24 +6,21 @@ import { type Message } from "../hooks/useChathistory"; import useSWR from "swr"; import { getQuestionExample } from "../actions/questionExample"; import { getLanguageName } from "../pagesList"; -import { MarkdownSection } from "./splitMarkdown"; -import { useEmbed } from "./embedContext"; -import { useFile } from "../terminal/file"; +import { DynamicMarkdownSection } from "./pageContent"; +import { useEmbedContext } from "../terminal/embedContext"; interface ChatFormProps { docs_id: string; documentContent: string; - splitMdContent: MarkdownSection[]; - sectionInView: boolean[]; - onClose: () => void; + sectionContent: DynamicMarkdownSection[]; + close: () => void; } export function ChatForm({ docs_id, documentContent, - splitMdContent, - sectionInView, - onClose, + sectionContent, + close, }: ChatFormProps) { // const [messages, updateChatHistory] = useChatHistory(sectionId); const [inputValue, setInputValue] = useState(""); @@ -31,15 +28,11 @@ export function ChatForm({ const lang = getLanguageName(docs_id); - const { replOutputs, execResults } = useEmbed()!; - const { files } = useFile(); + const { files, replOutputs, execResults } = useEmbedContext(); - const documentContentInView = splitMdContent - .filter((_, index) => sectionInView[index]) - .map( - (section) => - `${"#".repeat(section.level)} ${section.title}\n${section.content}` - ) + const documentContentInView = sectionContent + .filter((s) => s.inView) + .map((s) => s.rawContent) .join("\n\n"); const { data: exampleData, error: exampleError } = useSWR( // 質問フォームを開いたときだけで良い @@ -68,7 +61,7 @@ export function ChatForm({ e.preventDefault(); setIsLoading(true); - const userMessage: Message = { sender: "user", text: inputValue }; + // const userMessage: Message = { sender: "user", text: inputValue }; // updateChatHistory([userMessage]); let userQuestion = inputValue; @@ -82,8 +75,7 @@ export function ChatForm({ const result = await askAI({ userQuestion, documentContent, - splitMdContent, - sectionInView, + sectionContent, replOutputs, files, execResults, @@ -147,7 +139,7 @@ export function ChatForm({