From ae69a105fded03e858f09fab17969d58bc5d55c5 Mon Sep 17 00:00:00 2001 From: Akshat Batra Date: Sat, 23 Aug 2025 20:51:20 +0530 Subject: [PATCH 1/3] apply and fix code features Signed-off-by: Akshat Batra --- LICENSE | 37 ++- package-lock.json | 10 + package.json | 1 + src/ai-assistant/prompts.ts | 3 +- src/components/AIChatPanel.tsx | 162 +++++++----- src/components/CodeDiffPopup.tsx | 293 ++++++++++++++++++++++ src/components/PlaygroundSidebar.tsx | 25 ++ src/components/ProblemPanel.tsx | 50 +++- src/components/useUndoRedo.ts | 28 ++- src/editors/ConcertoEditor.tsx | 2 +- src/editors/JSONEditor.tsx | 2 +- src/editors/MarkdownEditor.tsx | 3 +- src/pages/MainContainer.tsx | 49 +--- src/styles/components/ProblemPanel.css | 2 +- src/types/components/AIAssistant.types.ts | 14 ++ src/utils/helpers/Errors.tsx | 65 ++++- 16 files changed, 631 insertions(+), 115 deletions(-) create mode 100644 src/components/CodeDiffPopup.tsx diff --git a/LICENSE b/LICENSE index f49a4e16..2823decd 100644 --- a/LICENSE +++ b/LICENSE @@ -198,4 +198,39 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. + +------------------------------------------------------------------------------- + +This project includes third-party software components governed by separate license terms, as follows: + +------------------------------------------------------------------------------- +jsdiff (https://github.com/kpdecker/jsdiff) — BSD 3-Clause License + +Copyright (c) 2009-2015, Kevin Decker +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +For the most up-to-date full license text, see the jsdiff repository: https://github.com/kpdecker/jsdiff/blob/master/LICENSE +------------------------------------------------------------------------------- \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c41e6378..5c8ec03b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/styled-components": "^5.1.34", "antd": "^5.7.2", "core-js": "^3.37.1", + "diff": "^8.0.2", "highlight.js": "^11.10.0", "immer": "^10.1.1", "jest-canvas-mock": "^2.5.2", @@ -10872,6 +10873,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", diff --git a/package.json b/package.json index 79f588b8..a00213cb 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/styled-components": "^5.1.34", "antd": "^5.7.2", "core-js": "^3.37.1", + "diff": "^8.0.2", "highlight.js": "^11.10.0", "immer": "^10.1.1", "jest-canvas-mock": "^2.5.2", diff --git a/src/ai-assistant/prompts.ts b/src/ai-assistant/prompts.ts index 49e16c4d..5274d865 100644 --- a/src/ai-assistant/prompts.ts +++ b/src/ai-assistant/prompts.ts @@ -52,8 +52,7 @@ export const prepareSystemPrompt = { }, default: (editorsContent: editorsContent, aiConfig?: any) => { - let prompt = `You are a helpful assistant that answers questions about open source Accord Project. You assist the user - to work with TemplateMark, Concerto models and JSON data. Code blocks returned by you should enclosed in backticks\n\n`; + let prompt = `You are a helpful assistant that answers questions about open source Accord Project. You assist the user in working with TemplateMark, Concerto models and JSON data. Code blocks returned by you should be enclosed in backticks, the language names that you can use after three backticks are- "concerto","templatemark" and "json", suffix 'Apply' to the language name if it is a complete code block that can be used to replace the corresponding editor content, precisely, concertoApply, templatemarkApply and jsonApply. You must always try to return complete code block that can be applied to the editors. Concerto code, TemplateMark code and JSON data are supplied to TemplateEngine to produce the final output. For instance, a data field that is not in Concerto data model can't be in JSON data and therefore can't be used in TemplateMark you generate. Analyze the JSON data and Concerto model (if provided) carefully, only the fields with simple data types (String, Integer etc.) present in concept annotated with @template decorator can be directly accessed anywhere in the template. Other complex data fields that have custom concept declaration in the Concerto model and are represented as nested fields in JSON data, can only be used within {{#clause conceptName}} {{concept_property_name}} {{/clause}} tags. Therefore, in most cases you have to create a scope using clause tag in TemplateMark to access properties defined under a concept in Concerto. For enumerating through a list you can create a scope to access the properties in list items via {{#olist listName}} {{instancePropertyName}} {{/olist}} or {{#ulist listName}} {{instancePropertyName}} {{/ulist}}. For TemplateMark code, there's no such thing as 'this' keyword within list scope. Optional fields shouldn't be wrapped in an if or with block to check for their availability e.g. if Concerto model has age as optional don't wrap it in if block in TemplateMark. You can also use Typescript within TemplateMark by enclosing the Typescript code in {{% %}}, you must write all of the Typescript code within a single line enclosed in a single pair of opening {{% and closing %}}. You may use Typescript to achieve an objective in TemplateMark only if TemplateMark syntax makes doing something hard, the data objects from JSON are readily available within {{% %}} enclosed Typescript using direct access, e.g. {{% return order.orderLines %}}. For e.g., you could use TypeScript to render ordered/unordered primitive list types such as String[]. Keep your focus on generating valid output based on current editors' contents but if you make a change that isn't compatible with the content of existing editors, you must return the full code for those editors as well. You mustn't add any placeholder in TemplateMark which isn't in Concerto model and JSON data unless you modify the Concerto and JSON data to have that field at the appropriate place.\n\n`; return includeEditorContents(prompt, aiConfig, editorsContent); } }; \ No newline at end of file diff --git a/src/components/AIChatPanel.tsx b/src/components/AIChatPanel.tsx index b8f45a96..ccca8802 100644 --- a/src/components/AIChatPanel.tsx +++ b/src/components/AIChatPanel.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import useAppStore from "../store/store"; import { sendMessage, stopMessage } from "../ai-assistant/chatRelay"; +import CodeDiffPopup from "./CodeDiffPopup"; export const AIChatPanel = () => { const [promptPreset, setPromptPreset] = useState(null); @@ -15,7 +16,21 @@ export const AIChatPanel = () => { editorAgreementData: state.editorAgreementData, })); - const { chatState, resetChat, aiConfig, setAIConfig, setAIConfigOpen, setAIChatOpen, textColor } = useAppStore.getState() + const { + chatState, + resetChat, + aiConfig, + setAIConfig, + setAIConfigOpen, + setAIChatOpen, + setEditorValue, + setTemplateMarkdown, + setEditorModelCto, + setModelCto, + setEditorAgreementData, + setData, + textColor + } = useAppStore.getState() const latestMessageRef = useRef(null); @@ -105,64 +120,88 @@ export const AIChatPanel = () => { } }; - const renderMessageContent = (content: string) => { - if (!content || !content.includes('```')) { - console.log("content is", content); - return ( -
- {children}, - }}> - {content} - -
- ); - } + const [showDiffPopup, setShowDiffPopup] = useState(false); + const [diffCodeProps, setDiffCodeProps] = useState({ + newCode: "", + currentCode: "", + language: "", + onApply: (_code: string) => {} + }); - const parts = []; - let key = 0; + const handleApplyCode = (code: string, language: string) => { + let currentCode = ""; + let applyFunction = (_code: string) => {}; - const segments = content.split('```'); - - if (segments[0]) { - parts.push( -
- - {segments[0]} - -
- ); + if (language === "concerto") { + currentCode = editorsContent.editorModelCto; + applyFunction = (code: string) => { + setEditorModelCto(code); + setModelCto(code); + }; + } else if (language === "templatemark") { + currentCode = editorsContent.editorTemplateMark; + applyFunction = (code: string) => { + setEditorValue(code); + setTemplateMarkdown(code); + }; + } else if (language === "json") { + currentCode = editorsContent.editorAgreementData; + applyFunction = (code: string) => { + setEditorAgreementData(code); + setData(code); + }; } - - for (let i = 1; i < segments.length; i++) { - if (i % 2 === 1 && segments[i]) { - const firstLineBreak = segments[i].indexOf('\n'); - let code = segments[i]; - - if (firstLineBreak > -1) { - code = segments[i].substring(firstLineBreak + 1); - } - - parts.push( -
-
-              {code.trim()}
-            
-
- ); - } else if (i % 2 === 0 && segments[i]) { - parts.push( -
- - {segments[i]} - -
- ); - } + + const normalize = (str: string) => str.trim(); + if (normalize(currentCode) === normalize(code)) { + applyFunction(code); + return; } - - return parts; + + setDiffCodeProps({ + newCode: code, + currentCode, + language, + onApply: applyFunction + }); + setShowDiffPopup(true); + }; + + const renderMessageContent = (content: string) => { + return ( +
+ { + if (className === undefined) return {children}; + let language = ((className as string).match(/language-[^ ]*/)?.toString())?.split("-")[1] || ""; + const isApplicable = ["concertoApply", "templatemarkApply", "jsonApply"].includes(language); + language = language.slice(0, -5) + const codeContent = children?.toString() || ""; + + return ( +
+ {isApplicable && ( + + )} + {children} +
+ ); + } + }}> + {content} +
+
+ ); }; useEffect(() => { @@ -252,7 +291,7 @@ export const AIChatPanel = () => { -
+
{chatState.messages.length === 0 ? ( @@ -303,7 +342,7 @@ export const AIChatPanel = () => {
{promptPreset && (
- + { promptPreset === "textToTemplate" ? "Text to TemplateMark" : "Create Concerto Model" } @@ -534,6 +573,15 @@ export const AIChatPanel = () => {
+ {showDiffPopup && ( + setShowDiffPopup(false)} + /> + )}
); } \ No newline at end of file diff --git a/src/components/CodeDiffPopup.tsx b/src/components/CodeDiffPopup.tsx new file mode 100644 index 00000000..824f0749 --- /dev/null +++ b/src/components/CodeDiffPopup.tsx @@ -0,0 +1,293 @@ +import React, { useState, useEffect } from 'react'; +import { diffLines as computeDiff } from 'diff'; +import { LineDiffProps, CodeDiffPopupProps } from "../types/components/AIAssistant.types" + +const LineDiff: React.FC = ({ line, type, displayLineNumber }) => { + const getBgColor = () => { + if (type === 'added') return 'bg-green-100'; + if (type === 'removed') return 'bg-red-100'; + return ''; + }; + + const getLinePrefix = () => { + if (type === 'added') return '+ '; + if (type === 'removed') return '- '; + return ' '; + }; + + return ( +
+
+ {displayLineNumber !== null ? displayLineNumber + 1 : ''} +
+
+ {getLinePrefix()} +
+
+ {line} +
+
+ ); +}; + +const CodeDiffPopup: React.FC = ({ + newCode, + currentCode, + language, + onApply, + onClose, +}) => { + const [diffResult, setDiffResult] = useState>([]); + const [isDragging, setIsDragging] = useState(false); + const [lastToggledLine, setLastToggledLine] = useState(null); + + useEffect(() => { + const normalizeCode = (code: string) => { + let normalized = code.replace(/\r\n/g, '\n'); + + if (!normalized.endsWith('\n')) { + normalized += '\n'; + } else if (normalized.endsWith('\n\n')) { + while (normalized.endsWith('\n\n')) { + normalized = normalized.slice(0, -1); + } + } + + return normalized; + }; + + const normalizedCurrent = normalizeCode(currentCode); + const normalizedNew = normalizeCode(newCode); + + if (normalizedCurrent === normalizedNew) { + setDiffResult([{ + value: normalizedNew, + added: false, + removed: false, + displayLineNumber: 0, + originalIndex: 0 + }]); + return; + } + + const diff = computeDiff(normalizedCurrent, normalizedNew); + + let resultLineNumber = 0; + let originalIndex = 0; + const formattedDiff: Array<{ + value: string; + added?: boolean; + removed?: boolean; + displayLineNumber: number | null; + originalIndex: number; + }> = []; + + diff.forEach(part => { + const lines = part.value.split('\n'); + if (lines[lines.length - 1] === '') lines.pop(); + + lines.forEach(line => { + if (part.added) { + formattedDiff.push({ + value: line, + added: true, + displayLineNumber: resultLineNumber++, + originalIndex: originalIndex++ + }); + } else if (part.removed) { + formattedDiff.push({ + value: line, + removed: true, + displayLineNumber: null, + originalIndex: -1 + }); + } else { + formattedDiff.push({ + value: line, + displayLineNumber: resultLineNumber++, + originalIndex: originalIndex++ + }); + } + }); + }); + + setDiffResult(formattedDiff); + }, [newCode, currentCode]); + + const calculateResultLineNumbers = () => { + let resultLineCounter = 0; + + return diffResult.map(line => { + if (line.removed) { + return { ...line, displayLineNumber: null }; + } + + if (line.added) { + return { ...line, displayLineNumber: resultLineCounter++ }; + } + + return { ...line, displayLineNumber: resultLineCounter++ }; + }); + }; + + const toggleLine = (index: number) => { + setDiffResult(prev => { + const updated = [...prev]; + const line = updated[index]; + + if (line.added) { + updated[index] = { ...line, added: false, removed: true }; + } else if (line.removed) { + updated[index] = { ...line, added: true, removed: false }; + } + + return updated; + }); + + setLastToggledLine(index); + }; + + const handleMouseDown = (index: number) => { + toggleLine(index); + setIsDragging(true); + }; + + const handleMouseUp = () => { + setIsDragging(false); + setLastToggledLine(null); + }; + + const handleMouseEnter = (index: number) => { + if (isDragging && lastToggledLine !== null) { + const start = Math.min(lastToggledLine, index); + const end = Math.max(lastToggledLine, index); + + setDiffResult(prev => { + const updated = [...prev]; + const lastLine = prev[lastToggledLine]; + const targetState = lastLine.removed; + + for (let i = start; i <= end; i++) { + const line = updated[i]; + if (line.added || line.removed) { + updated[i] = { + ...line, + added: !targetState, + removed: targetState + }; + } + } + return updated; + }); + + setLastToggledLine(index); + } + }; + + const applyChanges = () => { + const newCodeLines = diffResult + .filter(line => !line.removed) + .map(line => line.value); + + onApply(newCodeLines.join('\n')); + onClose(); + }; + + useEffect(() => { + const handleGlobalMouseUp = () => { + if (isDragging) { + setIsDragging(false); + setLastToggledLine(null); + } + }; + + document.addEventListener('mouseup', handleGlobalMouseUp); + + const handleMouseLeave = () => { + if (isDragging) { + setIsDragging(false); + setLastToggledLine(null); + } + }; + + document.addEventListener('mouseleave', handleMouseLeave); + + return () => { + document.removeEventListener('mouseup', handleGlobalMouseUp); + document.removeEventListener('mouseleave', handleMouseLeave); + }; + }, [isDragging]); + + const displayedDiff = calculateResultLineNumbers(); + + return ( +
+
+
+

Apply Code Changes ({language})

+ +
+ +
+
+ + Added + - Removed + Unchanged +
Click on a line to toggle between added and removed. Click and drag to toggle multiple lines at once.
+
+
+ {displayedDiff.map((line, index) => { + const type = line.added ? 'added' : line.removed ? 'removed' : 'unchanged'; + const canToggle = line.added || line.removed; + + return ( +
canToggle && handleMouseDown(index)} + onMouseEnter={() => canToggle && handleMouseEnter(index)} + > + +
+ ); + })} +
+
+ +
+ + +
+
+
+ ); +}; + +export default CodeDiffPopup; diff --git a/src/components/PlaygroundSidebar.tsx b/src/components/PlaygroundSidebar.tsx index fec9f226..52f69bfd 100644 --- a/src/components/PlaygroundSidebar.tsx +++ b/src/components/PlaygroundSidebar.tsx @@ -3,6 +3,7 @@ import { IoCodeSlash } from "react-icons/io5"; import { VscOutput } from "react-icons/vsc"; import { FiTerminal, FiShare2, FiSettings } from "react-icons/fi"; import { FaCirclePlay } from "react-icons/fa6"; +import { IoChatbubbleEllipsesOutline } from "react-icons/io5"; import useAppStore from "../store/store"; import { message } from "antd"; import FullScreenModal from "./FullScreenModal"; @@ -14,17 +15,21 @@ const PlaygroundSidebar = () => { isEditorsVisible, isPreviewVisible, isProblemPanelVisible, + isAIChatOpen, setEditorsVisible, setPreviewVisible, setProblemPanelVisible, + setAIChatOpen, generateShareableLink, } = useAppStore((state) => ({ isEditorsVisible: state.isEditorsVisible, isPreviewVisible: state.isPreviewVisible, isProblemPanelVisible: state.isProblemPanelVisible, + isAIChatOpen: state.isAIChatOpen, setEditorsVisible: state.setEditorsVisible, setPreviewVisible: state.setPreviewVisible, setProblemPanelVisible: state.setProblemPanelVisible, + setAIChatOpen: state.setAIChatOpen, generateShareableLink: state.generateShareableLink, })); @@ -95,6 +100,26 @@ const PlaygroundSidebar = () => { onClick: () => setProblemPanelVisible(!isProblemPanelVisible), active: isProblemPanelVisible }, + { + title: "AI Assistant", + component: ( +
+
+ +
+ AI +
+
+
+ ), + onClick: () => setAIChatOpen(!isAIChatOpen), + active: isAIChatOpen + }, { title: "Fullscreen", component: diff --git a/src/components/ProblemPanel.tsx b/src/components/ProblemPanel.tsx index 118e6954..758c991c 100644 --- a/src/components/ProblemPanel.tsx +++ b/src/components/ProblemPanel.tsx @@ -1,6 +1,8 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import useAppStore from '../store/store'; import '../styles/components/ProblemPanel.css'; +import { sendMessage } from '../ai-assistant/chatRelay'; +import { IoChatbubbleEllipsesOutline } from "react-icons/io5"; export interface ProblemItem { id: string; @@ -18,6 +20,31 @@ const ProblemPanel: React.FC = () => { backgroundColor: state.backgroundColor, textColor: state.textColor })); + + const [isSending, setIsSending] = useState(false); + + const editorsContent = useAppStore((state) => ({ + editorTemplateMark: state.editorValue, + editorModelCto: state.editorModelCto, + editorAgreementData: state.editorAgreementData, + })); + + const handleFixProblem = async (problem: ProblemItem) => { + if (isSending) return; + + setIsSending(true); + try { + const prompt = `Fix this ${problem.type}: ${problem.message} + Source: ${problem.source || 'Unknown'} + ${problem.line ? `Line: ${problem.line}` : ''} + ${problem.column ? `Column: ${problem.column}` : ''}`; + + await sendMessage(prompt, null, editorsContent); + useAppStore.getState().setAIChatOpen(true); + } finally { + setIsSending(false); + } + }; const parseError = (errorMessage: string) => { const errors: Omit[] = []; @@ -84,6 +111,27 @@ const ProblemPanel: React.FC = () => {
Problems + {problems.length > 0 && ( + + )}
{problems.length === 0 ? ( diff --git a/src/components/useUndoRedo.ts b/src/components/useUndoRedo.ts index 8b91ba96..24714198 100644 --- a/src/components/useUndoRedo.ts +++ b/src/components/useUndoRedo.ts @@ -1,9 +1,32 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; function useUndoRedo(initialValue: T, onChange?: (value: T) => void, onSync?: (value: T) => Promise) { const [past, setPast] = useState([]); const [present, setPresent] = useState(initialValue); const [future, setFuture] = useState([]); + const isInitialMount = useRef(true); + const isInternalChange = useRef(false); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } + + if (isInternalChange.current) { + isInternalChange.current = false; + return; + } + + if (initialValue !== present) { + setPast((prevPast) => [...prevPast, present]); + setPresent(initialValue); + setFuture([]); + + if (onChange) onChange(initialValue); + if (onSync) onSync(initialValue); + } + }, [initialValue, present]); useEffect(() => { if (initialValue !== present) { @@ -14,6 +37,7 @@ function useUndoRedo(initialValue: T, onChange?: (value: T) => void, onSync?: }, [initialValue, present]); const setValue = (newValue: T) => { + isInternalChange.current = true; setPast((prevPast) => [...prevPast, present]); setPresent(newValue); setFuture([]); @@ -23,6 +47,7 @@ function useUndoRedo(initialValue: T, onChange?: (value: T) => void, onSync?: const undo = () => { if (past.length === 0) return; + isInternalChange.current = true; const previous = past[past.length - 1]; setPast((prevPast) => prevPast.slice(0, -1)); setFuture((prevFuture) => [present, ...prevFuture]); @@ -33,6 +58,7 @@ function useUndoRedo(initialValue: T, onChange?: (value: T) => void, onSync?: const redo = () => { if (future.length === 0) return; + isInternalChange.current = true; const next = future[0]; setFuture((prevFuture) => prevFuture.slice(1)); setPast((prevPast) => [...prevPast, present]); diff --git a/src/editors/ConcertoEditor.tsx b/src/editors/ConcertoEditor.tsx index c3e6a74c..4ef89a4c 100644 --- a/src/editors/ConcertoEditor.tsx +++ b/src/editors/ConcertoEditor.tsx @@ -152,7 +152,7 @@ export default function ConcertoEditor({ bracketPairColorization: { enabled: true }, }; - const handleEditorDidMount = (editor: any) => { + const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => { editor.onDidChangeCursorSelection(() => { handleSelection(editor); }); diff --git a/src/editors/JSONEditor.tsx b/src/editors/JSONEditor.tsx index f5b577a1..df083159 100644 --- a/src/editors/JSONEditor.tsx +++ b/src/editors/JSONEditor.tsx @@ -33,7 +33,7 @@ export default function JSONEditor({ [] ); - const handleEditorDidMount = (editor: any) => { + const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor) => { editor.onDidChangeCursorSelection(() => { handleSelection(editor); }); diff --git a/src/editors/MarkdownEditor.tsx b/src/editors/MarkdownEditor.tsx index 48a53aa9..de6fbcf7 100644 --- a/src/editors/MarkdownEditor.tsx +++ b/src/editors/MarkdownEditor.tsx @@ -2,6 +2,7 @@ import { lazy, Suspense, useMemo, useCallback, useEffect } from "react"; import useAppStore from "../store/store"; import { useMonaco } from "@monaco-editor/react"; import { useCodeSelection } from "../components/CodeSelectionMenu"; +import type { editor } from "monaco-editor"; const MonacoEditor = lazy(() => import("@monaco-editor/react").then((mod) => ({ default: mod.Editor })) @@ -53,7 +54,7 @@ export default function MarkdownEditor({ scrollBeyondLastLine: false, }; - const handleEditorDidMount = (editor: any) => { + const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor) => { editor.onDidChangeCursorSelection(() => { handleSelection(editor); }); diff --git a/src/pages/MainContainer.tsx b/src/pages/MainContainer.tsx index 6132c92f..3105fac1 100644 --- a/src/pages/MainContainer.tsx +++ b/src/pages/MainContainer.tsx @@ -15,13 +15,11 @@ const MainContainer = () => { const textColor = useAppStore((state) => state.textColor); const { isAIChatOpen, - setAIChatOpen, isEditorsVisible, isPreviewVisible, isProblemPanelVisible, } = useAppStore((state) => ({ isAIChatOpen: state.isAIChatOpen, - setAIChatOpen: state.setAIChatOpen, isEditorsVisible: state.isEditorsVisible, isPreviewVisible: state.isPreviewVisible, isProblemPanelVisible: state.isProblemPanelVisible, @@ -46,51 +44,6 @@ const MainContainer = () => { Concerto Model
-
@@ -162,7 +115,7 @@ const MainContainer = () => { )} {isAIChatOpen && ( <> - + diff --git a/src/styles/components/ProblemPanel.css b/src/styles/components/ProblemPanel.css index 70b28b39..09142cc3 100644 --- a/src/styles/components/ProblemPanel.css +++ b/src/styles/components/ProblemPanel.css @@ -3,7 +3,7 @@ } .problem-panel-header { - @apply flex items-center px-4 py-2 border-b border-gray-200; + @apply flex items-center justify-between px-4 py-2 border-b border-gray-200; } .problem-panel-header-light { diff --git a/src/types/components/AIAssistant.types.ts b/src/types/components/AIAssistant.types.ts index 4e7a998b..0bed0c35 100644 --- a/src/types/components/AIAssistant.types.ts +++ b/src/types/components/AIAssistant.types.ts @@ -41,4 +41,18 @@ export interface CodeSelectionMenuProps { position: { x: number; y: number }; onClose: () => void; editorType: 'markdown' | 'concerto' | 'json'; +} + +export interface LineDiffProps { + line: string; + type: 'added' | 'removed' | 'unchanged'; + displayLineNumber: number | null; +} + +export interface CodeDiffPopupProps { + newCode: string; + currentCode: string; + language: string; + onApply: (code: string) => void; + onClose: () => void; } \ No newline at end of file diff --git a/src/utils/helpers/Errors.tsx b/src/utils/helpers/Errors.tsx index 471c1b77..767793e7 100644 --- a/src/utils/helpers/Errors.tsx +++ b/src/utils/helpers/Errors.tsx @@ -1,11 +1,74 @@ import useAppStore from "../../store/store"; import { Alert, Space } from "antd"; +import { sendMessage } from "../../ai-assistant/chatRelay"; +import { useState } from "react"; function Errors() { const error = useAppStore((state) => state.error); + const editorsContent = useAppStore((state) => ({ + editorTemplateMark: state.editorValue, + editorModelCto: state.editorModelCto, + editorAgreementData: state.editorAgreementData, + })); + const [isSending, setIsSending] = useState(false); + + const handleFixError = async () => { + if (!error || isSending) return; + + setIsSending(true); + try { + await sendMessage(`Fix this error: ${error}`, null, editorsContent); + useAppStore.getState().setAIChatOpen(true); + } finally { + setIsSending(false); + } + }; + return error ? ( - +
+ + +
) : ( <> From 0072dd061dc5d6ddf81c77dd5805b6890265ba5f Mon Sep 17 00:00:00 2001 From: Akshat Batra Date: Fri, 29 Aug 2025 01:28:12 +0530 Subject: [PATCH 2/3] use monaco diff editor Signed-off-by: Akshat Batra --- package-lock.json | 10 - package.json | 1 - src/ai-assistant/activityTracker.ts | 0 src/components/AIChatPanel.tsx | 2 +- src/components/CodeDiffPopup.tsx | 346 ++++++++-------------------- 5 files changed, 91 insertions(+), 268 deletions(-) create mode 100644 src/ai-assistant/activityTracker.ts diff --git a/package-lock.json b/package-lock.json index 5c8ec03b..c41e6378 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "@types/styled-components": "^5.1.34", "antd": "^5.7.2", "core-js": "^3.37.1", - "diff": "^8.0.2", "highlight.js": "^11.10.0", "immer": "^10.1.1", "jest-canvas-mock": "^2.5.2", @@ -10873,15 +10872,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", diff --git a/package.json b/package.json index a00213cb..79f588b8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@types/styled-components": "^5.1.34", "antd": "^5.7.2", "core-js": "^3.37.1", - "diff": "^8.0.2", "highlight.js": "^11.10.0", "immer": "^10.1.1", "jest-canvas-mock": "^2.5.2", diff --git a/src/ai-assistant/activityTracker.ts b/src/ai-assistant/activityTracker.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/components/AIChatPanel.tsx b/src/components/AIChatPanel.tsx index ccca8802..06a6acf3 100644 --- a/src/components/AIChatPanel.tsx +++ b/src/components/AIChatPanel.tsx @@ -131,7 +131,7 @@ export const AIChatPanel = () => { const handleApplyCode = (code: string, language: string) => { let currentCode = ""; let applyFunction = (_code: string) => {}; - + if (language === "concerto") { currentCode = editorsContent.editorModelCto; applyFunction = (code: string) => { diff --git a/src/components/CodeDiffPopup.tsx b/src/components/CodeDiffPopup.tsx index 824f0749..cae684d0 100644 --- a/src/components/CodeDiffPopup.tsx +++ b/src/components/CodeDiffPopup.tsx @@ -1,34 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { diffLines as computeDiff } from 'diff'; -import { LineDiffProps, CodeDiffPopupProps } from "../types/components/AIAssistant.types" +import React, { useState, useMemo, lazy, Suspense } from 'react'; +import { useMonaco } from '@monaco-editor/react'; +import { CodeDiffPopupProps } from "../types/components/AIAssistant.types" +import useAppStore from '../store/store'; -const LineDiff: React.FC = ({ line, type, displayLineNumber }) => { - const getBgColor = () => { - if (type === 'added') return 'bg-green-100'; - if (type === 'removed') return 'bg-red-100'; - return ''; - }; - - const getLinePrefix = () => { - if (type === 'added') return '+ '; - if (type === 'removed') return '- '; - return ' '; - }; - - return ( -
-
- {displayLineNumber !== null ? displayLineNumber + 1 : ''} -
-
- {getLinePrefix()} -
-
- {line} -
-
- ); -}; +const MonacoDiffEditor = lazy(() => + import("@monaco-editor/react").then(mod => ({ default: mod.DiffEditor })) +); const CodeDiffPopup: React.FC = ({ newCode, @@ -37,250 +14,107 @@ const CodeDiffPopup: React.FC = ({ onApply, onClose, }) => { - const [diffResult, setDiffResult] = useState>([]); - const [isDragging, setIsDragging] = useState(false); - const [lastToggledLine, setLastToggledLine] = useState(null); - - useEffect(() => { - const normalizeCode = (code: string) => { - let normalized = code.replace(/\r\n/g, '\n'); - - if (!normalized.endsWith('\n')) { - normalized += '\n'; - } else if (normalized.endsWith('\n\n')) { - while (normalized.endsWith('\n\n')) { - normalized = normalized.slice(0, -1); - } - } - - return normalized; - }; - - const normalizedCurrent = normalizeCode(currentCode); - const normalizedNew = normalizeCode(newCode); - - if (normalizedCurrent === normalizedNew) { - setDiffResult([{ - value: normalizedNew, - added: false, - removed: false, - displayLineNumber: 0, - originalIndex: 0 - }]); - return; - } - - const diff = computeDiff(normalizedCurrent, normalizedNew); - - let resultLineNumber = 0; - let originalIndex = 0; - const formattedDiff: Array<{ - value: string; - added?: boolean; - removed?: boolean; - displayLineNumber: number | null; - originalIndex: number; - }> = []; - - diff.forEach(part => { - const lines = part.value.split('\n'); - if (lines[lines.length - 1] === '') lines.pop(); - - lines.forEach(line => { - if (part.added) { - formattedDiff.push({ - value: line, - added: true, - displayLineNumber: resultLineNumber++, - originalIndex: originalIndex++ - }); - } else if (part.removed) { - formattedDiff.push({ - value: line, - removed: true, - displayLineNumber: null, - originalIndex: -1 - }); - } else { - formattedDiff.push({ - value: line, - displayLineNumber: resultLineNumber++, - originalIndex: originalIndex++ - }); - } - }); - }); - - setDiffResult(formattedDiff); - }, [newCode, currentCode]); - - const calculateResultLineNumbers = () => { - let resultLineCounter = 0; - - return diffResult.map(line => { - if (line.removed) { - return { ...line, displayLineNumber: null }; - } - - if (line.added) { - return { ...line, displayLineNumber: resultLineCounter++ }; - } - - return { ...line, displayLineNumber: resultLineCounter++ }; - }); - }; - - const toggleLine = (index: number) => { - setDiffResult(prev => { - const updated = [...prev]; - const line = updated[index]; - - if (line.added) { - updated[index] = { ...line, added: false, removed: true }; - } else if (line.removed) { - updated[index] = { ...line, added: true, removed: false }; - } - - return updated; - }); - - setLastToggledLine(index); - }; - - const handleMouseDown = (index: number) => { - toggleLine(index); - setIsDragging(true); - }; - - const handleMouseUp = () => { - setIsDragging(false); - setLastToggledLine(null); - }; - - const handleMouseEnter = (index: number) => { - if (isDragging && lastToggledLine !== null) { - const start = Math.min(lastToggledLine, index); - const end = Math.max(lastToggledLine, index); - - setDiffResult(prev => { - const updated = [...prev]; - const lastLine = prev[lastToggledLine]; - const targetState = lastLine.removed; - - for (let i = start; i <= end; i++) { - const line = updated[i]; - if (line.added || line.removed) { - updated[i] = { - ...line, - added: !targetState, - removed: targetState - }; - } - } - return updated; - }); - - setLastToggledLine(index); - } - }; - - const applyChanges = () => { - const newCodeLines = diffResult - .filter(line => !line.removed) - .map(line => line.value); - - onApply(newCodeLines.join('\n')); - onClose(); - }; - - useEffect(() => { - const handleGlobalMouseUp = () => { - if (isDragging) { - setIsDragging(false); - setLastToggledLine(null); - } - }; - - document.addEventListener('mouseup', handleGlobalMouseUp); - - const handleMouseLeave = () => { - if (isDragging) { - setIsDragging(false); - setLastToggledLine(null); - } - }; - - document.addEventListener('mouseleave', handleMouseLeave); - - return () => { - document.removeEventListener('mouseup', handleGlobalMouseUp); - document.removeEventListener('mouseleave', handleMouseLeave); - }; - }, [isDragging]); + language = (language === 'templatemark' ? 'markdown' : language); + const monaco = useMonaco(); + const [modifiedContent, setModifiedContent] = useState(newCode); + + const {backgroundColor} = useAppStore((state) => ({ + backgroundColor: state.backgroundColor, + })); + const isDarkMode = useMemo(() => + backgroundColor && backgroundColor !== '#ffffff', + [backgroundColor]); + const theme = useMemo(() => ({ + popupBg: isDarkMode ? 'bg-gray-900' : 'bg-white', + popupBorder: isDarkMode ? 'border-gray-700' : 'border-gray-300', + headerBg: isDarkMode ? 'bg-gray-800' : 'bg-white', + headerText: isDarkMode ? 'text-gray-100' : 'text-gray-900', + subHeaderBg: isDarkMode ? 'bg-gray-800' : 'bg-gray-100', + subHeaderText: isDarkMode ? 'text-gray-400' : 'text-gray-600', + buttonBg: isDarkMode ? 'bg-gray-700' : 'bg-gray-200', + buttonBgHover: isDarkMode ? 'bg-gray-600' : 'bg-gray-300', + buttonText: isDarkMode ? 'text-gray-100' : 'text-gray-900', + closeButtonText: isDarkMode ? 'text-gray-400' : 'text-gray-500', + closeButtonHover: isDarkMode ? 'hover:text-gray-200' : 'hover:text-gray-800', + applyButtonBg: isDarkMode ? 'bg-blue-700' : 'bg-blue-500', + applyButtonBgHover: isDarkMode ? 'bg-blue-800' : 'bg-blue-600', + applyButtonText: 'text-white', + border: isDarkMode ? 'border border-gray-700' : 'border border-gray-300', + }), [isDarkMode]); + const themeName = useMemo( + () => (isDarkMode ? "darkTheme" : "lightTheme"), + [isDarkMode] + ); - const displayedDiff = calculateResultLineNumbers(); + const diffEditorOptions = useMemo(() => ({ + originalEditable: false, + readOnly: false, + renderSideBySide: true, + enableSplitViewResizing: false, + renderIndicators: true, + folding: false, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + ignoreTrimWhitespace: false, + glyphMargin: true, + }), []); + + React.useEffect(() => { + setModifiedContent(newCode); + }, [newCode]); + + if (!monaco) { + return ( +
+
+
+
Loading Monaco Editor...
+
+
+
+ ); + } return ( -
-
-
-

Apply Code Changes ({language})

-
-
-
- + Added - - Removed - Unchanged -
Click on a line to toggle between added and removed. Click and drag to toggle multiple lines at once.
-
-
- {displayedDiff.map((line, index) => { - const type = line.added ? 'added' : line.removed ? 'removed' : 'unchanged'; - const canToggle = line.added || line.removed; - - return ( -
canToggle && handleMouseDown(index)} - onMouseEnter={() => canToggle && handleMouseEnter(index)} - > - -
- ); - })} +
+
+
+ Review and edit the proposed changes below. +
+ Loading Diff Editor...
}> + +
-
+
From 3c11356a8401eaacdad9f862a37ea593d9b3a9d0 Mon Sep 17 00:00:00 2001 From: Akshat Batra Date: Fri, 29 Aug 2025 16:34:38 +0530 Subject: [PATCH 3/3] revert LICENSE change Signed-off-by: Akshat Batra --- LICENSE | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/LICENSE b/LICENSE index 2823decd..f49a4e16 100644 --- a/LICENSE +++ b/LICENSE @@ -198,39 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. - -------------------------------------------------------------------------------- - -This project includes third-party software components governed by separate license terms, as follows: - -------------------------------------------------------------------------------- -jsdiff (https://github.com/kpdecker/jsdiff) — BSD 3-Clause License - -Copyright (c) 2009-2015, Kevin Decker -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -For the most up-to-date full license text, see the jsdiff repository: https://github.com/kpdecker/jsdiff/blob/master/LICENSE -------------------------------------------------------------------------------- \ No newline at end of file + limitations under the License. \ No newline at end of file