From a1ed9365e56ad4c8301516a94d4d5ed749c2acdb Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Wed, 25 Mar 2026 05:51:20 -0700 Subject: [PATCH 1/3] fix: improve Render deployment config for reliability - Switch agent from pserv to web type (enables health checks) - Switch app from Docker to native Node runtime - Add auto-scaling (1-3 instances) on both services - Add health check endpoints (agent /ok, app /api/health) - Pin NODE_VERSION=22 - Update fromService reference for new web type --- apps/app/src/app/api/health/route.ts | 3 +++ render.yaml | 29 +++++++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 apps/app/src/app/api/health/route.ts diff --git a/apps/app/src/app/api/health/route.ts b/apps/app/src/app/api/health/route.ts new file mode 100644 index 0000000..41063cf --- /dev/null +++ b/apps/app/src/app/api/health/route.ts @@ -0,0 +1,3 @@ +export function GET() { + return Response.json({ status: "ok" }); +} diff --git a/render.yaml b/render.yaml index cb296a0..25b182a 100644 --- a/render.yaml +++ b/render.yaml @@ -1,11 +1,16 @@ services: - # ── Agent (LangGraph Python) — private, not exposed to internet ── - - type: pserv + # ── Agent (LangGraph Python) — Docker required for langgraph-api image ── + - type: web name: open-generative-ui-agent runtime: docker plan: starter dockerfilePath: docker/Dockerfile.agent healthCheckPath: /ok + scaling: + minInstances: 1 + maxInstances: 3 + targetMemoryPercent: 80 + targetCPUPercent: 70 envVars: - key: OPENAI_API_KEY sync: false @@ -18,17 +23,28 @@ services: - apps/agent/** - docker/Dockerfile.agent - # ── Frontend (Next.js) — public web service ── + # ── Frontend (Next.js) — native Node runtime ── - type: web name: open-generative-ui-app - runtime: docker + runtime: node plan: starter - dockerfilePath: docker/Dockerfile.app + scaling: + minInstances: 1 + maxInstances: 3 + targetMemoryPercent: 80 + targetCPUPercent: 70 + buildCommand: corepack enable && pnpm install --no-frozen-lockfile && pnpm --filter @repo/app build + startCommand: pnpm --filter @repo/app start + healthCheckPath: /api/health envVars: + - key: NODE_VERSION + value: "22" + - key: SKIP_INSTALL_DEPS + value: "true" - key: LANGGRAPH_DEPLOYMENT_URL fromService: name: open-generative-ui-agent - type: pserv + type: web property: hostport - key: LANGSMITH_API_KEY sync: false @@ -44,4 +60,3 @@ services: - package.json - pnpm-lock.yaml - turbo.json - - docker/Dockerfile.app From 5b4500a4f0dcd16a91b33c1baa12ca89fb52fbe2 Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Wed, 25 Mar 2026 06:24:22 -0700 Subject: [PATCH 2/3] fix: resolve React lint errors in canvas and save-template-overlay - Replace useState/useEffect mounted pattern with useSyncExternalStore for hydration-safe client detection - Wrap setAgentState in useCallback to stabilize effect dependencies - Replace ref-during-render with state+effect for pending_template capture - Fix ConfirmChangesProps types (remove any, drop unused args prop) - Add eslint-disable for intentionally scoped effect dependencies Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/app/src/app/canvas/page.tsx | 447 ++++++++++++++++++ .../generative-ui/save-template-overlay.tsx | 24 +- 2 files changed, 461 insertions(+), 10 deletions(-) create mode 100644 apps/app/src/app/canvas/page.tsx diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx new file mode 100644 index 0000000..61dba71 --- /dev/null +++ b/apps/app/src/app/canvas/page.tsx @@ -0,0 +1,447 @@ +"use client"; + +import "@copilotkit/react-core/v2/styles.css"; +import "./style.css"; + +import MarkdownIt from "markdown-it"; +import React, { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react"; +import { diffWords } from "diff"; +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { + useAgent, + UseAgentUpdate, + useHumanInTheLoop, + useConfigureSuggestions, + CopilotChat, +} from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; +import { z } from "zod"; + +const extensions = [StarterKit]; + +export default function CanvasPage() { + // Defer CopilotChat to client-only to avoid Radix hydration ID mismatch + const subscribe = useCallback((cb: () => void) => { cb(); return () => {}; }, []); + const mounted = useSyncExternalStore(subscribe, () => true, () => false); + + return ( + <> + {/* Animated background */} +
+
+
+ + {/* App shell */} +
+
+ {/* Header Banner */} +
+
+
+
+ + + + + +
+

+ Document to Diagram + — powered by CopilotKit +

+
+ + Get started + +
+
+ + + {/* Content Area */} +
+
+ {/* Left: Document Editor */} +
+ +
+ + {/* Right: Chat Panel */} +
+ {mounted && } +
+
+
+
+
+
+ + ); +} + +interface AgentState { + document: string; +} + +const DEFAULT_DOCUMENT = `# How do WebSockets Work? + +## 1. The Handshake (HTTP Upgrade) + +It starts as a regular HTTP request. The client sends a special header asking to "upgrade" the connection: + +- GET /chat HTTP/1.1 +- Upgrade: websocket +- Connection: Upgrade + +The server responds with 101 Switching Protocols, and from that point on, the connection is no longer HTTP — it's a WebSocket. + +## 2. The Persistent Connection + +Unlike HTTP (where each request opens and closes a connection), the WebSocket connection stays open. Both sides can now send messages to each other at any time without waiting for the other to ask first. + +## 3. Frames, Not Requests + +Data is sent as lightweight "frames" — small packets that can carry text, binary data, or control signals (like ping/pong to keep the connection alive). + +## HTTP vs WebSocket + +| Aspect | HTTP | WebSocket | +|--------|------|-----------| +| Direction | One-way (request → response) | Two-way (either side) | +| Connection | Opens and closes each time | Stays open | +| Overhead | Headers sent every request | Minimal after handshake | +| Use case | Loading pages, REST APIs | Chat, live feeds, games | + +## A simple mental model + +Think of HTTP like sending letters — you write one, wait for a reply, then write another. WebSocket is like a phone call — once connected, both people can speak freely at any time without hanging up between each sentence. + +## Common use cases + +- Chat apps — messages appear instantly without polling +- Live dashboards — stock prices, sports scores, analytics +- Multiplayer games — real-time position and state sync +- Collaborative tools — like Google Docs, where edits appear live`; + +const DocumentEditor = () => { + const editor = useEditor({ + extensions, + immediatelyRender: false, + editorProps: { + attributes: { class: "tiptap" }, + }, + }); + + const [placeholderVisible, setPlaceholderVisible] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const currentDocumentRef = useRef(DEFAULT_DOCUMENT); + const lastPlainTextRef = useRef(""); + const wasRunning = useRef(false); + const isMountedRef = useRef(true); + + // Initialize editor with default document on mount (convert markdown → HTML) + const initializedRef = useRef(false); + useEffect(() => { + if (!editor || !isMountedRef.current || initializedRef.current) return; + initializedRef.current = true; + editor.commands.setContent(fromMarkdown(currentDocumentRef.current)); + lastPlainTextRef.current = editor.getText() || ""; + }, [editor]); + + // Cleanup on unmount to prevent state updates after component is removed + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + // Track editor focus state + useEffect(() => { + if (!editor) return; + + const handleFocus = () => { + if (isMountedRef.current) setIsFocused(true); + }; + const handleBlur = () => { + if (isMountedRef.current) setIsFocused(false); + }; + + editor.on("focus", handleFocus); + editor.on("blur", handleBlur); + + return () => { + editor.off("focus", handleFocus); + editor.off("blur", handleBlur); + }; + }, [editor]); + + useConfigureSuggestions({ + suggestions: [ + { + title: "Generate WebSocket document", + message: "Create a comprehensive document explaining how WebSockets work, including the handshake process, persistent connections, frames, comparison with HTTP, and common use cases.", + }, + { + title: "Explain REST API architecture", + message: "Write a detailed document about REST API design principles, HTTP methods, status codes, request/response structure, and best practices.", + }, + { + title: "Microservices architecture", + message: "Write a comprehensive guide to microservices architecture, covering service decomposition, inter-service communication, data consistency, and deployment patterns.", + }, + ], + }); + + const { agent } = useAgent({ + agentId: "default", + updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged], + }); + + const agentState = agent.state as AgentState | undefined; + const setAgentState = useCallback((s: AgentState) => agent.setState(s), [agent]); + const isLoading = agent.isRunning; + + // Handle loading state transitions + useEffect(() => { + if (!isMountedRef.current) return; + + if (isLoading) { + currentDocumentRef.current = agentState?.document || editor?.getText() || ""; + } + editor?.setEditable(!isLoading); + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only reacting to isLoading changes + }, [isLoading, editor]); + + // Handle final state update when run completes + useEffect(() => { + if (!isMountedRef.current) return; + + if (wasRunning.current && !isLoading) { + const saved = currentDocumentRef.current; + const newDoc = agentState?.document || ""; + if (saved.trim().length > 0 && saved !== newDoc) { + const diff = diffPartialText(saved, newDoc, true); + editor?.commands.setContent(fromMarkdown(diff)); + } + currentDocumentRef.current = newDoc; + lastPlainTextRef.current = editor?.getText() || ""; + } + wasRunning.current = isLoading; + // eslint-disable-next-line react-hooks/exhaustive-deps -- only trigger on run status transitions + }, [isLoading]); + + // Handle streaming updates while agent is running + useEffect(() => { + if (!isMountedRef.current) return; + + if (isLoading && agentState?.document) { + const saved = currentDocumentRef.current; + if (saved.trim().length > 0) { + const diff = diffPartialText(saved, agentState.document); + editor?.commands.setContent(fromMarkdown(diff)); + } else { + editor?.commands.setContent(fromMarkdown(agentState.document)); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- only trigger on document content changes + }, [agentState?.document]); + + const text = editor?.getText() || ""; + + // Sync user edits to agent state + useEffect(() => { + if (!isMountedRef.current) return; + + setPlaceholderVisible(text.length === 0 && !isFocused); + + if (!isLoading && text !== lastPlainTextRef.current) { + lastPlainTextRef.current = text; + currentDocumentRef.current = text; + setAgentState({ document: text }); + } + }, [text, isLoading, isFocused, setAgentState]); + + // Human-in-the-loop: confirm_changes + useHumanInTheLoop( + { + agentId: "default", + name: "confirm_changes", + description: "Present the proposed changes to the user for review", + parameters: z.object({ + document: z.string().describe("The full updated document in markdown format"), + }), + render({ status, respond }: { args: { document?: string }; status: string; respond?: (result: unknown) => Promise }) { + if (status === "executing") { + return ( + { + const savedDoc = currentDocumentRef.current; + editor?.commands.setContent(fromMarkdown(savedDoc)); + setAgentState({ document: savedDoc }); + }} + onConfirm={() => { + const newDoc = agentState?.document || ""; + editor?.commands.setContent(fromMarkdown(newDoc)); + currentDocumentRef.current = newDoc; + lastPlainTextRef.current = editor?.getText() || ""; + setAgentState({ document: newDoc }); + }} + /> + ); + } + return <>; + }, + }, + [agentState?.document], + ); + + return ( +
+ {placeholderVisible && ( +
+ How do WebSockets work? +
+ )} +
+ +
+
+ ); +}; + +interface ConfirmChangesProps { + respond: ((result: unknown) => Promise) | undefined; + status: string; + onReject: () => void; + onConfirm: () => void; +} + +function ConfirmChanges({ respond, status, onReject, onConfirm }: ConfirmChangesProps) { + const [accepted, setAccepted] = useState(null); + + return ( +
+

Confirm Changes

+

Accept the proposed changes?

+ {accepted === null && ( +
+ + +
+ )} + {accepted !== null && ( +
+
+ {accepted ? "✓ Accepted" : "✗ Rejected"} +
+
+ )} +
+ ); +} + +function fromMarkdown(text: string) { + const md = new MarkdownIt({ + typographer: true, + html: true, + }); + + return md.render(text); +} + +function diffPartialText(oldText: string, newText: string, isComplete: boolean = false) { + let oldTextToCompare = oldText; + if (oldText.length > newText.length && !isComplete) { + oldTextToCompare = oldText.slice(0, newText.length); + } + + const changes = diffWords(oldTextToCompare, newText); + + let result = ""; + changes.forEach((part) => { + if (part.added) { + result += `${part.value}`; + } else if (part.removed) { + result += `${part.value}`; + } else { + result += part.value; + } + }); + + if (oldText.length > newText.length && !isComplete) { + result += oldText.slice(newText.length); + } + + return result; +} diff --git a/apps/app/src/components/generative-ui/save-template-overlay.tsx b/apps/app/src/components/generative-ui/save-template-overlay.tsx index e071bc9..2606ec5 100644 --- a/apps/app/src/components/generative-ui/save-template-overlay.tsx +++ b/apps/app/src/components/generative-ui/save-template-overlay.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useMemo, useRef, type ReactNode } from "react"; +import { useState, useCallback, useEffect, useMemo, type ReactNode } from "react"; import { useAgent } from "@copilotkit/react-core/v2"; import { SEED_TEMPLATES } from "@/components/template-library/seed-templates"; @@ -35,25 +35,29 @@ export function SaveTemplateOverlay({ const [saveState, setSaveState] = useState("idle"); const [templateName, setTemplateName] = useState(""); - // Capture pending_template at mount time — it may be cleared by the agent later + // Capture pending_template once — it may be cleared by the agent later. + // Syncs external agent state into local state (legitimate effect-based setState). const pending = agent.state?.pending_template as { id: string; name: string } | null | undefined; - const sourceRef = useRef<{ id: string; name: string } | null>(null); - if (pending?.id && !sourceRef.current) { - sourceRef.current = pending; - } + const [capturedSource, setCapturedSource] = useState<{ id: string; name: string } | null>(null); + useEffect(() => { + if (pending?.id) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- one-time capture of external agent state + setCapturedSource((prev) => prev ?? pending); + } + }, [pending]); // Check if this content matches an existing template: // 1. Exact HTML match (seed templates rendered as-is) // 2. Source template captured from pending_template (applied templates with modified data) const matchedTemplate = useMemo(() => { // First check source template from apply flow - if (sourceRef.current) { + if (capturedSource) { const allTemplates = [ ...SEED_TEMPLATES, ...((agent.state?.templates as { id: string; name: string }[]) || []), ]; - const source = allTemplates.find((t) => t.id === sourceRef.current!.id); - if (source) return source; + const found = allTemplates.find((t) => t.id === capturedSource.id); + if (found) return found; } // Then check exact HTML match if (!html) return null; @@ -64,7 +68,7 @@ export function SaveTemplateOverlay({ ...((agent.state?.templates as { id: string; name: string; html: string }[]) || []), ]; return allTemplates.find((t) => t.html && normalise(t.html) === norm) ?? null; - }, [html, agent.state?.templates]); + }, [html, agent.state?.templates, capturedSource]); const handleSave = useCallback(() => { const name = templateName.trim() || title || "Untitled Template"; From 73a69ecbbfc522efd2a4e757ae933d2f345a4e47 Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Wed, 25 Mar 2026 06:50:02 -0700 Subject: [PATCH 3/3] fix: remove canvas/page.tsx from tracking This file depends on packages not in the project's package.json (@tiptap/react, markdown-it, diff) and was not previously tracked. Removing to fix CI build failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/app/src/app/canvas/page.tsx | 447 ------------------------------- 1 file changed, 447 deletions(-) delete mode 100644 apps/app/src/app/canvas/page.tsx diff --git a/apps/app/src/app/canvas/page.tsx b/apps/app/src/app/canvas/page.tsx deleted file mode 100644 index 61dba71..0000000 --- a/apps/app/src/app/canvas/page.tsx +++ /dev/null @@ -1,447 +0,0 @@ -"use client"; - -import "@copilotkit/react-core/v2/styles.css"; -import "./style.css"; - -import MarkdownIt from "markdown-it"; -import React, { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react"; -import { diffWords } from "diff"; -import { useEditor, EditorContent } from "@tiptap/react"; -import StarterKit from "@tiptap/starter-kit"; -import { - useAgent, - UseAgentUpdate, - useHumanInTheLoop, - useConfigureSuggestions, - CopilotChat, -} from "@copilotkit/react-core/v2"; -import { CopilotKit } from "@copilotkit/react-core"; -import { z } from "zod"; - -const extensions = [StarterKit]; - -export default function CanvasPage() { - // Defer CopilotChat to client-only to avoid Radix hydration ID mismatch - const subscribe = useCallback((cb: () => void) => { cb(); return () => {}; }, []); - const mounted = useSyncExternalStore(subscribe, () => true, () => false); - - return ( - <> - {/* Animated background */} -
-
-
- - {/* App shell */} -
-
- {/* Header Banner */} -
-
-
-
- - - - - -
-

- Document to Diagram - — powered by CopilotKit -

-
- - Get started - -
-
- - - {/* Content Area */} -
-
- {/* Left: Document Editor */} -
- -
- - {/* Right: Chat Panel */} -
- {mounted && } -
-
-
-
-
-
- - ); -} - -interface AgentState { - document: string; -} - -const DEFAULT_DOCUMENT = `# How do WebSockets Work? - -## 1. The Handshake (HTTP Upgrade) - -It starts as a regular HTTP request. The client sends a special header asking to "upgrade" the connection: - -- GET /chat HTTP/1.1 -- Upgrade: websocket -- Connection: Upgrade - -The server responds with 101 Switching Protocols, and from that point on, the connection is no longer HTTP — it's a WebSocket. - -## 2. The Persistent Connection - -Unlike HTTP (where each request opens and closes a connection), the WebSocket connection stays open. Both sides can now send messages to each other at any time without waiting for the other to ask first. - -## 3. Frames, Not Requests - -Data is sent as lightweight "frames" — small packets that can carry text, binary data, or control signals (like ping/pong to keep the connection alive). - -## HTTP vs WebSocket - -| Aspect | HTTP | WebSocket | -|--------|------|-----------| -| Direction | One-way (request → response) | Two-way (either side) | -| Connection | Opens and closes each time | Stays open | -| Overhead | Headers sent every request | Minimal after handshake | -| Use case | Loading pages, REST APIs | Chat, live feeds, games | - -## A simple mental model - -Think of HTTP like sending letters — you write one, wait for a reply, then write another. WebSocket is like a phone call — once connected, both people can speak freely at any time without hanging up between each sentence. - -## Common use cases - -- Chat apps — messages appear instantly without polling -- Live dashboards — stock prices, sports scores, analytics -- Multiplayer games — real-time position and state sync -- Collaborative tools — like Google Docs, where edits appear live`; - -const DocumentEditor = () => { - const editor = useEditor({ - extensions, - immediatelyRender: false, - editorProps: { - attributes: { class: "tiptap" }, - }, - }); - - const [placeholderVisible, setPlaceholderVisible] = useState(false); - const [isFocused, setIsFocused] = useState(false); - const currentDocumentRef = useRef(DEFAULT_DOCUMENT); - const lastPlainTextRef = useRef(""); - const wasRunning = useRef(false); - const isMountedRef = useRef(true); - - // Initialize editor with default document on mount (convert markdown → HTML) - const initializedRef = useRef(false); - useEffect(() => { - if (!editor || !isMountedRef.current || initializedRef.current) return; - initializedRef.current = true; - editor.commands.setContent(fromMarkdown(currentDocumentRef.current)); - lastPlainTextRef.current = editor.getText() || ""; - }, [editor]); - - // Cleanup on unmount to prevent state updates after component is removed - useEffect(() => { - return () => { - isMountedRef.current = false; - }; - }, []); - - // Track editor focus state - useEffect(() => { - if (!editor) return; - - const handleFocus = () => { - if (isMountedRef.current) setIsFocused(true); - }; - const handleBlur = () => { - if (isMountedRef.current) setIsFocused(false); - }; - - editor.on("focus", handleFocus); - editor.on("blur", handleBlur); - - return () => { - editor.off("focus", handleFocus); - editor.off("blur", handleBlur); - }; - }, [editor]); - - useConfigureSuggestions({ - suggestions: [ - { - title: "Generate WebSocket document", - message: "Create a comprehensive document explaining how WebSockets work, including the handshake process, persistent connections, frames, comparison with HTTP, and common use cases.", - }, - { - title: "Explain REST API architecture", - message: "Write a detailed document about REST API design principles, HTTP methods, status codes, request/response structure, and best practices.", - }, - { - title: "Microservices architecture", - message: "Write a comprehensive guide to microservices architecture, covering service decomposition, inter-service communication, data consistency, and deployment patterns.", - }, - ], - }); - - const { agent } = useAgent({ - agentId: "default", - updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged], - }); - - const agentState = agent.state as AgentState | undefined; - const setAgentState = useCallback((s: AgentState) => agent.setState(s), [agent]); - const isLoading = agent.isRunning; - - // Handle loading state transitions - useEffect(() => { - if (!isMountedRef.current) return; - - if (isLoading) { - currentDocumentRef.current = agentState?.document || editor?.getText() || ""; - } - editor?.setEditable(!isLoading); - // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only reacting to isLoading changes - }, [isLoading, editor]); - - // Handle final state update when run completes - useEffect(() => { - if (!isMountedRef.current) return; - - if (wasRunning.current && !isLoading) { - const saved = currentDocumentRef.current; - const newDoc = agentState?.document || ""; - if (saved.trim().length > 0 && saved !== newDoc) { - const diff = diffPartialText(saved, newDoc, true); - editor?.commands.setContent(fromMarkdown(diff)); - } - currentDocumentRef.current = newDoc; - lastPlainTextRef.current = editor?.getText() || ""; - } - wasRunning.current = isLoading; - // eslint-disable-next-line react-hooks/exhaustive-deps -- only trigger on run status transitions - }, [isLoading]); - - // Handle streaming updates while agent is running - useEffect(() => { - if (!isMountedRef.current) return; - - if (isLoading && agentState?.document) { - const saved = currentDocumentRef.current; - if (saved.trim().length > 0) { - const diff = diffPartialText(saved, agentState.document); - editor?.commands.setContent(fromMarkdown(diff)); - } else { - editor?.commands.setContent(fromMarkdown(agentState.document)); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- only trigger on document content changes - }, [agentState?.document]); - - const text = editor?.getText() || ""; - - // Sync user edits to agent state - useEffect(() => { - if (!isMountedRef.current) return; - - setPlaceholderVisible(text.length === 0 && !isFocused); - - if (!isLoading && text !== lastPlainTextRef.current) { - lastPlainTextRef.current = text; - currentDocumentRef.current = text; - setAgentState({ document: text }); - } - }, [text, isLoading, isFocused, setAgentState]); - - // Human-in-the-loop: confirm_changes - useHumanInTheLoop( - { - agentId: "default", - name: "confirm_changes", - description: "Present the proposed changes to the user for review", - parameters: z.object({ - document: z.string().describe("The full updated document in markdown format"), - }), - render({ status, respond }: { args: { document?: string }; status: string; respond?: (result: unknown) => Promise }) { - if (status === "executing") { - return ( - { - const savedDoc = currentDocumentRef.current; - editor?.commands.setContent(fromMarkdown(savedDoc)); - setAgentState({ document: savedDoc }); - }} - onConfirm={() => { - const newDoc = agentState?.document || ""; - editor?.commands.setContent(fromMarkdown(newDoc)); - currentDocumentRef.current = newDoc; - lastPlainTextRef.current = editor?.getText() || ""; - setAgentState({ document: newDoc }); - }} - /> - ); - } - return <>; - }, - }, - [agentState?.document], - ); - - return ( -
- {placeholderVisible && ( -
- How do WebSockets work? -
- )} -
- -
-
- ); -}; - -interface ConfirmChangesProps { - respond: ((result: unknown) => Promise) | undefined; - status: string; - onReject: () => void; - onConfirm: () => void; -} - -function ConfirmChanges({ respond, status, onReject, onConfirm }: ConfirmChangesProps) { - const [accepted, setAccepted] = useState(null); - - return ( -
-

Confirm Changes

-

Accept the proposed changes?

- {accepted === null && ( -
- - -
- )} - {accepted !== null && ( -
-
- {accepted ? "✓ Accepted" : "✗ Rejected"} -
-
- )} -
- ); -} - -function fromMarkdown(text: string) { - const md = new MarkdownIt({ - typographer: true, - html: true, - }); - - return md.render(text); -} - -function diffPartialText(oldText: string, newText: string, isComplete: boolean = false) { - let oldTextToCompare = oldText; - if (oldText.length > newText.length && !isComplete) { - oldTextToCompare = oldText.slice(0, newText.length); - } - - const changes = diffWords(oldTextToCompare, newText); - - let result = ""; - changes.forEach((part) => { - if (part.added) { - result += `${part.value}`; - } else if (part.removed) { - result += `${part.value}`; - } else { - result += part.value; - } - }); - - if (oldText.length > newText.length && !isComplete) { - result += oldText.slice(newText.length); - } - - return result; -}