diff --git a/package.json b/package.json index eb2a9b7..b7d98ae 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "antlr4": "4.7.2", "bootstrap": "4.4.1", "chroma-js": "2.1.0", - "diff": "4.0.2", + "diff": "^4.0.2", + "diff2html": "^3.4.48", "elementtree": "0.1.7", "marked": "4.0.12", "monaco-editor": "0.21.2", @@ -16,6 +17,7 @@ "openai": "^4.30.0", "path": "0.12.7", "pluralize": "7.0.0", + "prismjs": "^1.29.0", "rc-slider": "8.7.1", "rc-tooltip": "4.0.0-alpha.3", "react": "16.12.0", diff --git a/src/activeLLM/suggestFix.js b/src/activeLLM/suggestFix.js index 039df69..41a970e 100644 --- a/src/activeLLM/suggestFix.js +++ b/src/activeLLM/suggestFix.js @@ -1,43 +1,281 @@ import OpenAI from "openai"; +/** + * Validates whether the returned content is likely a full file or just a partial snippet. + * Returns an object with validation results. + */ +function validateFullFileContent(modifiedContent, originalContent) { + const result = { + isFullFile: true, + warnings: [], + confidence: 1.0 + }; + + if (!modifiedContent || modifiedContent.trim().length === 0) { + result.isFullFile = false; + result.warnings.push("Modified content is empty"); + result.confidence = 0; + return result; + } + + const originalLines = originalContent.split('\n'); + const modifiedLines = modifiedContent.split('\n'); + const originalLineCount = originalLines.length; + const modifiedLineCount = modifiedLines.length; + + // Check 1: Line count ratio - modified should be at least 70% of original + const lineRatio = modifiedLineCount / originalLineCount; + if (lineRatio < 0.7) { + result.isFullFile = false; + result.warnings.push(`Line count too low: ${modifiedLineCount} vs original ${originalLineCount} (${(lineRatio * 100).toFixed(1)}%)`); + result.confidence -= 0.3; + } + + // Check 2: For Java files, check for package declaration + const originalHasPackage = /^\s*package\s+[\w.]+\s*;/m.test(originalContent); + const modifiedHasPackage = /^\s*package\s+[\w.]+\s*;/m.test(modifiedContent); + if (originalHasPackage && !modifiedHasPackage) { + result.isFullFile = false; + result.warnings.push("Missing package declaration"); + result.confidence -= 0.25; + } + + // Check 3: Check for import statements preservation + const originalImports = (originalContent.match(/^\s*import\s+[\w.*]+\s*;/gm) || []).length; + const modifiedImports = (modifiedContent.match(/^\s*import\s+[\w.*]+\s*;/gm) || []).length; + if (originalImports > 0 && modifiedImports < originalImports * 0.5) { + result.isFullFile = false; + result.warnings.push(`Missing imports: ${modifiedImports} vs original ${originalImports}`); + result.confidence -= 0.25; + } + + // Check 4: Check for class/interface declaration + const originalHasClass = /^\s*(public\s+)?(abstract\s+)?(class|interface|enum)\s+\w+/m.test(originalContent); + const modifiedHasClass = /^\s*(public\s+)?(abstract\s+)?(class|interface|enum)\s+\w+/m.test(modifiedContent); + if (originalHasClass && !modifiedHasClass) { + result.isFullFile = false; + result.warnings.push("Missing class/interface declaration"); + result.confidence -= 0.25; + } + + // Check 5: Character length ratio + const charRatio = modifiedContent.length / originalContent.length; + if (charRatio < 0.5) { + result.isFullFile = false; + result.warnings.push(`Character count too low: ${modifiedContent.length} vs original ${originalContent.length} (${(charRatio * 100).toFixed(1)}%)`); + result.confidence -= 0.2; + } + + result.confidence = Math.max(0, result.confidence); + result.isFullFile = result.confidence >= 0.7; + + return result; +} + +/** + * Attempts to merge a code snippet into the original file content. + * Uses context matching to find the right location for the snippet. + */ +function mergeSnippetIntoOriginal(snippet, originalContent, violationSnippet) { + console.log("Attempting to merge snippet into original file..."); + + const originalLines = originalContent.split('\n'); + const snippetLines = snippet.split('\n').filter(line => line.trim().length > 0); + + if (snippetLines.length === 0) { + console.warn("Snippet is empty, returning original content"); + return originalContent; + } + + // Strategy 1: Try to find the violation snippet in original and replace the matching region + if (violationSnippet && violationSnippet.trim().length > 0) { + const violationLines = violationSnippet.split('\n').filter(line => line.trim().length > 0); + + if (violationLines.length > 0) { + // Find where the violation starts in the original + const violationStart = findSnippetLocation(originalLines, violationLines); + + if (violationStart !== -1) { + console.log(`Found violation at line ${violationStart + 1}`); + + // Find the extent of the violation in original + const violationEnd = violationStart + violationLines.length; + + // Replace the violation region with the snippet + const beforeViolation = originalLines.slice(0, violationStart); + const afterViolation = originalLines.slice(violationEnd); + + const merged = [...beforeViolation, ...snippetLines, ...afterViolation].join('\n'); + console.log("Successfully merged snippet using violation location"); + return merged; + } + } + } + + // Strategy 2: Try to find context lines from the snippet in the original + // Look for the first few non-empty lines of the snippet in the original + const contextLines = snippetLines.slice(0, Math.min(3, snippetLines.length)); + const snippetLocation = findSnippetLocation(originalLines, contextLines); + + if (snippetLocation !== -1) { + console.log(`Found snippet context at line ${snippetLocation + 1}`); + + // Replace from this location with the snippet + const beforeSnippet = originalLines.slice(0, snippetLocation); + const afterSnippet = originalLines.slice(snippetLocation + snippetLines.length); + + const merged = [...beforeSnippet, ...snippetLines, ...afterSnippet].join('\n'); + console.log("Successfully merged snippet using context matching"); + return merged; + } + + // Strategy 3: If snippet contains method signature, find and replace that method + const methodMatch = snippet.match(/^\s*(public|private|protected)?\s*(static)?\s*[\w<>\[\]]+\s+(\w+)\s*\([^)]*\)/m); + if (methodMatch) { + const methodName = methodMatch[3]; + console.log(`Looking for method: ${methodName}`); + + const methodLocation = findMethodInOriginal(originalLines, methodName); + if (methodLocation.start !== -1) { + console.log(`Found method ${methodName} at lines ${methodLocation.start + 1}-${methodLocation.end + 1}`); + + const beforeMethod = originalLines.slice(0, methodLocation.start); + const afterMethod = originalLines.slice(methodLocation.end + 1); + + const merged = [...beforeMethod, ...snippetLines, ...afterMethod].join('\n'); + console.log("Successfully merged snippet by replacing method"); + return merged; + } + } + + console.warn("Could not find suitable merge location, returning original with snippet appended as comment"); + // Last resort: return original with a warning comment + return originalContent + "\n\n// TODO: The following fix could not be automatically merged:\n/*\n" + snippet + "\n*/"; +} + +/** + * Finds the starting line index where a snippet appears in the original lines. + * Uses fuzzy matching to handle whitespace differences. + */ +function findSnippetLocation(originalLines, snippetLines) { + if (snippetLines.length === 0) return -1; + + const normalize = (line) => line.trim().replace(/\s+/g, ' '); + const firstSnippetLine = normalize(snippetLines[0]); + + for (let i = 0; i < originalLines.length; i++) { + if (normalize(originalLines[i]) === firstSnippetLine) { + // Check if subsequent lines also match + let allMatch = true; + for (let j = 1; j < snippetLines.length && (i + j) < originalLines.length; j++) { + if (normalize(originalLines[i + j]) !== normalize(snippetLines[j])) { + allMatch = false; + break; + } + } + if (allMatch) { + return i; + } + } + } + return -1; +} + +/** + * Finds a method's start and end lines in the original content. + */ +function findMethodInOriginal(originalLines, methodName) { + const methodPattern = new RegExp(`^\\s*(public|private|protected)?\\s*(static)?\\s*[\\w<>\\[\\]]+\\s+${methodName}\\s*\\(`); + + for (let i = 0; i < originalLines.length; i++) { + if (methodPattern.test(originalLines[i])) { + // Found method start, now find the end by counting braces + let braceCount = 0; + let methodStarted = false; + + for (let j = i; j < originalLines.length; j++) { + const line = originalLines[j]; + for (const char of line) { + if (char === '{') { + braceCount++; + methodStarted = true; + } else if (char === '}') { + braceCount--; + } + } + if (methodStarted && braceCount === 0) { + return { start: i, end: j }; + } + } + // If we couldn't find the end, return just the start + return { start: i, end: i }; + } + } + return { start: -1, end: -1 }; +} + +/** + * Ensures the modified content is a full file by validating and merging if needed. + */ +function ensureFullFileContent(modifiedContent, originalContent, violationSnippet) { + const validation = validateFullFileContent(modifiedContent, originalContent); + + console.log("Content validation result:", { + isFullFile: validation.isFullFile, + confidence: validation.confidence, + warnings: validation.warnings + }); + + if (validation.isFullFile) { + console.log("Content validated as full file"); + return modifiedContent; + } + + console.warn("Content appears to be partial, attempting to merge into original file"); + return mergeSnippetIntoOriginal(modifiedContent, originalContent, violationSnippet); +} + export async function suggestFix( rule, example, violation, exampleFilePath, violationFilePath, + violationFileContent, setState, ) { - const prompt = `Here is a design rule and its description: ${rule} - Here is a code example that follows this design rule: ${example} - The example file path is ${exampleFilePath} - Now, here is a code snippet that violates this design rule: ${violation} - The violated code's file path is ${violationFilePath} - Can you suggest a fix to make this violation follow the given design rule? - Generate code with surrounding code included that follows the design rule. - Be sure to maintain proper whitespace with \\t and \\n. - Give a brief explanation of your fix as well. - Ensure to include the fileName of where to insert the fix in the format Example.java. - Strictly output in JSON format. The JSON should have the following format:{"code": "...", "explanation": "...", "fileName": "..."}`; - - //the following prompt is an older version. It is commented out because the one used right now is more - //concise and produces better output - /*const prompt = `Here is a design rule and its description: ${rule} - Here is a code example that follows this design rule: ${example} - The example file path is ${exampleFilePath} - Now, here is a code snippet that violates this design rule. ${violation} - The violated code's file path is ${violationFilePath} - Suggest a fix to make this violation follow the given design rule? - Generate code with surrounding code included that follows the design rule. - Be sure to maintain proper whitespace with \\t and \\n. - Give a brief explanation of your fix as well. Strictly output in JSON format. - Ensure that you include the fileName where the fix should be inserted at. - This should just be in the format Example.java - The JSON should have the following format:{"code": "...", "explanation": "...", "fileName": "..."}`;*/ + const promptA = `You are assisting with enforcing the following design rule: +${rule} + +Here is a code example that follows the rule: +${example} +Example file path: ${exampleFilePath} + +<<>> +${violationFilePath} +<<>> + +<<>> +${violationFileContent} +<<>> + +<<>> +${violation} +<<>> + +Rewrite the original file so it satisfies the rule while preserving every unrelated line verbatim. Constraints: +- Copy every existing package declaration, import statement, comment, Javadoc, and formatting exactly as provided unless a specific line must change to satisfy the rule. +- Do NOT delete or reorder imports; append new imports after the existing block if required. +- When you modify a line, change only the minimal portion needed; leave all other lines identical. +- Preserve indentation and whitespace on all untouched lines. + +Take your time and provide an unstructured response. Include: (1) a detailed explanation of your changes, (2) the fully rewritten file content, and (3) the target file name/path. Do not output JSON in this step.`; let attempt = 1; let success = false; + console.log("violation codde is sent to chatGPT:"); + //console.log(violationFileContent); while (attempt <= 3 && !success) { try { @@ -50,29 +288,62 @@ export async function suggestFix( dangerouslyAllowBrowser: true, }); - const chatCompletion = await openai.chat.completions.create({ - model: "gpt-3.5-turbo", + const chatCompletionA = await openai.chat.completions.create({ + model: "gpt-5.2", temperature: 0.75, - messages: [{role: "user", content: prompt}], + messages: [{role: "user", content: promptA}], }); - const suggestedSnippet = chatCompletion.choices[0].message.content; + const responseA = chatCompletionA.choices[0].message.content; + + console.log("ReceivedResponseA from chatGPT:"); + //console.log(responseA); + + const promptB = `Here is the input prompt given to you to fix a design rule: +${promptA} + +This is the response you generated: +${responseA} + +Now, based on these, structure the response to this prompt in a structured JSON format. The JSON should have the following format: {\"explanation\":\"...\", \"code\":\"...\", \"fileName\":\"...\"}. \"code\" must be the fully rewritten file content. Return only JSON.`; + + const chatCompletionB = await openai.chat.completions.create({ + model: "gpt-5.2", + temperature: 0.2, + messages: [{role: "user", content: promptB}], + }); + + const suggestedSnippet = chatCompletionB.choices[0].message.content; const stripped = suggestedSnippet.replace(/^`json|`json$/g, "").trim(); const parsedJSON = JSON.parse(stripped); + const rawModifiedContent = parsedJSON["modifiedFileContent"] ?? parsedJSON["code"] ?? ""; + const explanation = parsedJSON["explanation"] ?? ""; + const fileName = parsedJSON["fileName"] ?? violationFilePath ?? ""; + + // Validate and ensure full file content + const modifiedFileContent = ensureFullFileContent( + rawModifiedContent, + violationFileContent, + violation + ); + + console.log("Final Solution from chatGPT:"); + console.log(parsedJSON); // sets the relevant state in the React component that made the request // see ../ui/rulePanel.js for more details - setState({suggestedSnippet: parsedJSON["code"]}); - setState({snippetExplanation: parsedJSON["explanation"]}); - setState({suggestionFileName: parsedJSON["fileName"]}); + setState({suggestedSnippet: modifiedFileContent}); + setState({snippetExplanation: explanation}); + setState({suggestionFileName: fileName}); const llmModifiedFileContent = { command: "LLM_MODIFIED_FILE_CONTENT", data: { filePath: `${violationFilePath}`, - fileToChange: `${parsedJSON["fileName"]}`, - modifiedFileContent: `${parsedJSON["code"]}`, - explanation: `${parsedJSON["explanation"]}`, + fileToChange: `${fileName}`, + modifiedFileContent: modifiedFileContent, + explanation: explanation, + originalFileContent: violationFileContent, }, }; @@ -80,6 +351,7 @@ export async function suggestFix( setState({llmModifiedFileContent: llmModifiedFileContent}); success = true; + return llmModifiedFileContent; } catch (error) { console.log(error); success = false; @@ -87,3 +359,4 @@ export async function suggestFix( } } } + diff --git a/src/core/ActiveLLM.code-workspace b/src/core/ActiveLLM.code-workspace new file mode 100644 index 0000000..00d0a72 --- /dev/null +++ b/src/core/ActiveLLM.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "../../.." + } + ] +} \ No newline at end of file diff --git a/src/core/coreConstants.js b/src/core/coreConstants.js index 2817b0d..5dda3cc 100644 --- a/src/core/coreConstants.js +++ b/src/core/coreConstants.js @@ -1,11 +1,14 @@ export const webSocketSendMessage = { + send_info_for_edit_fix:"SEND_INFO_FOR_EDIT_FIX", modified_rule_msg: "MODIFIED_RULE", modified_tag_msg: "MODIFIED_TAG", snippet_xml_msg: "XML_RESULT", + llm_modified_file_content: "LLM_MODIFIED_FILE_CONTENT", converted_java_snippet_msg: "CONVERTED_JAVA_SNIPPET", code_to_xml_msg: "EXPR_STMT", new_rule_msg: "NEW_RULE", new_tag_msg: "NEW_TAG", + send_llm_snippet_msg: "LLM_SNIPPET", open_file_msg: "OPEN_FILE", @@ -18,6 +21,7 @@ export const webSocketSendMessage = { }; export const webSocketReceiveMessage = { + receive_content_for_edit_fix: "RECEIVE_CONTENT_FOR_EDIT_FIX", xml_files_msg: "XML", rule_table_msg: "RULE_TABLE", tag_table_msg: "TAG_TABLE", diff --git a/src/core/utilities.js b/src/core/utilities.js index 15d38d6..7ddb5c4 100644 --- a/src/core/utilities.js +++ b/src/core/utilities.js @@ -30,6 +30,40 @@ class Utilities { tagInfo: data }; break; + + case webSocketSendMessage.send_llm_snippet_msg: + console.log("In command"); + console.log(data); + messageJson.data={ + code:data.suggestedSnippet, + explanation:data.snippetExplanation + } + break; + + case webSocketSendMessage.send_info_for_edit_fix: + messageJson.data={ + filePathOfSuggestedFix:data.data.fileToChange, + filePathOfViolation:data.data.filePath, + modifiedFileContent:data.data.modifiedFileContent + } + break; + + + case webSocketSendMessage.llm_modified_file_content: + if (!data.llmModifiedFileContent) { + console.warn('No LLM modified file content to send.'); + return; + } + + messageJson.data = { + filePath: data.llmModifiedFileContent.data.filePath, + explanation: data.llmModifiedFileContent.data.explanation, + fileToChange: data.llmModifiedFileContent.data.fileToChange, + modifiedFileContent: data.llmModifiedFileContent.data.modifiedFileContent, + originalFileContent: data.originalFileContent || data.llmModifiedFileContent.data.originalFileContent || '', + } + + break; case webSocketSendMessage.snippet_xml_msg: messageJson.data = { fileName: data.fileName, @@ -246,4 +280,4 @@ class Utilities { } -export default Utilities; \ No newline at end of file +export default Utilities; diff --git a/src/core/webSocketManager.js b/src/core/webSocketManager.js index 15254cd..cb0ff07 100644 --- a/src/core/webSocketManager.js +++ b/src/core/webSocketManager.js @@ -5,6 +5,7 @@ import {Component} from "react"; import {connect} from "react-redux"; + import { receiveExpressionStatementXML, ignoreFileChange, updateFilePath, @@ -51,6 +52,7 @@ class WebSocketManager extends Component { switch (message.command) { + case webSocketReceiveMessage.enter_chat_msg: this.props.onLoadingGif(true); break; @@ -170,17 +172,25 @@ class WebSocketManager extends Component { case webSocketReceiveMessage.file_change_in_ide_msg: // data: "filePath" + let focusedFilePath = message.data; - if (!this.props.ignoreFileChange) { - this.props.onFilePathChange(focusedFilePath); - window.location.hash = `#/${hashConst.rulesForFile}/` + focusedFilePath.replace(/\//g, "%2F"); - } else { - counter--; - if (counter === 0) { - this.props.onFalsifyIgnoreFile(); - counter = 3; - } + console.log("File name: "); + console.log(focusedFilePath); + focusedFilePath = String(focusedFilePath); + + if(!focusedFilePath.includes("edit_fix_window")){ + if (!this.props.ignoreFileChange) { + this.props.onFilePathChange(focusedFilePath); + window.location.hash = `#/${hashConst.rulesForFile}/` + focusedFilePath.replace(/\//g, "%2F"); + } else { + counter--; + if (counter === 0) { + this.props.onFalsifyIgnoreFile(); + counter = 3; + } + } } + break; /* Mining Rules */ diff --git a/src/prism-vs.css b/src/prism-vs.css new file mode 100644 index 0000000..54377df --- /dev/null +++ b/src/prism-vs.css @@ -0,0 +1,168 @@ +/** + * VS theme by Andrew Lock (https://andrewlock.net) + * Inspired by Visual Studio syntax coloring + */ + +code[class*="language-"], +pre[class*="language-"] { + color: #393A34; + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + font-size: .9em; + line-height: 1.2em; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre > code[class*="language-"] { + font-size: 1em; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + background: #C1DEF1; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + background: #C1DEF1; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + border: 1px solid #dddddd; + background-color: white; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .2em; + padding-top: 1px; + padding-bottom: 1px; + background: #f8f8f8; + border: 1px solid #dddddd; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #008000; + font-style: italic; +} + +.token.namespace { + opacity: .7; +} + +.token.string { + color: #A31515; +} + +.token.punctuation, +.token.operator { + color: #393A34; /* no highlight */ +} + +.token.url, +.token.symbol, +.token.number, +.token.boolean, +.token.variable, +.token.constant, +.token.inserted { + color: #36acaa; +} + +.token.atrule, +.token.keyword, +.token.attr-value, +.language-autohotkey .token.selector, +.language-json .token.boolean, +.language-json .token.number, +code[class*="language-css"] { + color: #0000ff; +} + +.token.function { + color: #393A34; +} + +.token.deleted, +.language-autohotkey .token.tag { + color: #9a050f; +} + +.token.selector, +.language-autohotkey .token.keyword { + color: #00009f; +} + +.token.important { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.class-name, +.language-json .token.property { + color: #2B91AF; +} + +.token.tag, +.token.selector { + color: #800000; +} + +.token.attr-name, +.token.property, +.token.regex, +.token.entity { + color: #ff0000; +} + +.token.directive.tag .tag { + background: #ffff00; + color: #393A34; +} + +/* overrides color-values for the Line Numbers plugin + * http://prismjs.com/plugins/line-numbers/ + */ +.line-numbers.line-numbers .line-numbers-rows { + border-right-color: #a5a5a5; +} + +.line-numbers .line-numbers-rows > span:before { + color: #2B91AF; +} + +/* overrides color-values for the Line Highlight plugin +* http://prismjs.com/plugins/line-highlight/ +*/ +.line-highlight.line-highlight { + background: rgba(193, 222, 241, 0.2); + background: -webkit-linear-gradient(left, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0)); + background: linear-gradient(to right, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0)); +} diff --git a/src/prism-vsc-dark-plus.css b/src/prism-vsc-dark-plus.css new file mode 100644 index 0000000..d3bd501 --- /dev/null +++ b/src/prism-vsc-dark-plus.css @@ -0,0 +1,275 @@ +pre[class*="language-"], +code[class*="language-"] { + color: #d4d4d4; + font-size: 13px; + text-shadow: none; + font-family: Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::selection, +code[class*="language-"]::selection, +pre[class*="language-"] *::selection, +code[class*="language-"] *::selection { + text-shadow: none; + background: #264F78; +} + +@media print { + pre[class*="language-"], + code[class*="language-"] { + text-shadow: none; + } +} + +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + background: #1e1e1e; +} + +:not(pre) > code[class*="language-"] { + padding: .1em .3em; + border-radius: .3em; + color: #db4c69; + background: #1e1e1e; +} +/********************************************************* +* Tokens +*/ +.namespace { + opacity: .7; +} + +.token.doctype .token.doctype-tag { + color: #569CD6; +} + +.token.doctype .token.name { + color: #9cdcfe; +} + +.token.comment, +.token.prolog { + color: #6a9955; +} + +.token.punctuation, +.language-html .language-css .token.punctuation, +.language-html .language-javascript .token.punctuation { + color: #d4d4d4; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.inserted, +.token.unit { + color: #b5cea8; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.deleted { + color: #ce9178; +} + +.language-css .token.string.url { + text-decoration: underline; +} + +.token.operator, +.token.entity { + color: #d4d4d4; +} + +.token.operator.arrow { + color: #569CD6; +} + +.token.atrule { + color: #ce9178; +} + +.token.atrule .token.rule { + color: #c586c0; +} + +.token.atrule .token.url { + color: #9cdcfe; +} + +.token.atrule .token.url .token.function { + color: #dcdcaa; +} + +.token.atrule .token.url .token.punctuation { + color: #d4d4d4; +} + +.token.keyword { + color: #569CD6; +} + +.token.keyword.module, +.token.keyword.control-flow { + color: #c586c0; +} + +.token.function, +.token.function .token.maybe-class-name { + color: #dcdcaa; +} + +.token.regex { + color: #d16969; +} + +.token.important { + color: #569cd6; +} + +.token.italic { + font-style: italic; +} + +.token.constant { + color: #9cdcfe; +} + +.token.class-name, +.token.maybe-class-name { + color: #4ec9b0; +} + +.token.console { + color: #9cdcfe; +} + +.token.parameter { + color: #9cdcfe; +} + +.token.interpolation { + color: #9cdcfe; +} + +.token.punctuation.interpolation-punctuation { + color: #569cd6; +} + +.token.boolean { + color: #569cd6; +} + +.token.property, +.token.variable, +.token.imports .token.maybe-class-name, +.token.exports .token.maybe-class-name { + color: #9cdcfe; +} + +.token.selector { + color: #d7ba7d; +} + +.token.escape { + color: #d7ba7d; +} + +.token.tag { + color: #569cd6; +} + +.token.tag .token.punctuation { + color: #808080; +} + +.token.cdata { + color: #808080; +} + +.token.attr-name { + color: #9cdcfe; +} + +.token.attr-value, +.token.attr-value .token.punctuation { + color: #ce9178; +} + +.token.attr-value .token.punctuation.attr-equals { + color: #d4d4d4; +} + +.token.entity { + color: #569cd6; +} + +.token.namespace { + color: #4ec9b0; +} +/********************************************************* +* Language Specific +*/ + +pre[class*="language-javascript"], +code[class*="language-javascript"], +pre[class*="language-jsx"], +code[class*="language-jsx"], +pre[class*="language-typescript"], +code[class*="language-typescript"], +pre[class*="language-tsx"], +code[class*="language-tsx"] { + color: #9cdcfe; +} + +pre[class*="language-css"], +code[class*="language-css"] { + color: #ce9178; +} + +pre[class*="language-html"], +code[class*="language-html"] { + color: #d4d4d4; +} + +.language-regex .token.anchor { + color: #dcdcaa; +} + +.language-html .token.punctuation { + color: #808080; +} +/********************************************************* +* Line highlighting +*/ +pre[class*="language-"] > code[class*="language-"] { + position: relative; + z-index: 1; +} + +.line-highlight.line-highlight { + background: #f7ebc6; + box-shadow: inset 5px 0 0 #f7d87c; + z-index: 0; +} diff --git a/src/ui/rulePanel.js b/src/ui/rulePanel.js index d10a6d4..aba0a11 100644 --- a/src/ui/rulePanel.js +++ b/src/ui/rulePanel.js @@ -8,7 +8,7 @@ import { connect } from "react-redux"; import "../index.css"; import "../App.css"; import { - Tab, Tabs, Badge, FormGroup, ControlLabel, Label, Collapse + Tab, Tabs, Badge, FormGroup, ControlLabel, Label, Collapse, Button } from "react-bootstrap"; import { FaCaretDown, FaCaretUp } from "react-icons/fa"; import { MdEdit } from "react-icons/md"; @@ -21,7 +21,14 @@ import { webSocketSendMessage } from "../core/coreConstants"; import { relatives } from "../core/ruleExecutorConstants"; import { hashConst, none_filePath } from "./uiConstants"; -import { suggestFix } from "../activeLLM/suggestFix"; +import { suggestFix} from "../activeLLM/suggestFix"; +import Prism from 'prismjs'; +import '../../src/prism-vs.css'; // Choose any theme you like + +// Import the language syntax for Java +import 'prismjs/components/prism-java'; + +import WebSocketManager from "../core/webSocketManager"; class RulePanel extends Component { @@ -387,6 +394,7 @@ class RulePanel extends Component { return list.map((d, i) => { return ( { + sessionStorage.setItem(key, JSON.stringify(conversationHistory)); } + getConversationFromSessionStorage = (key) => { + const history = sessionStorage.getItem(key); + return history ? JSON.parse(history) : []; + } + + clearConversationFromSessionStorage = (key) => { + sessionStorage.removeItem(key); + } + + handleSuggestion = async ( rule, example, snippet, exampleFilePath, violationFilePath, + key ) => { const parsedSnippet = Utilities.removeSrcmlAnnotations(snippet); const parsedExample = Utilities.removeSrcmlAnnotations(example); + + const normalizePath = (filePath) => filePath.replace(/\\/g, '/'); + const targetPath = normalizePath(violationFilePath); + let violationFileContent = ''; + if (this.props.xmlFiles && this.props.xmlFiles.length > 0) { + const matchingFile = this.props.xmlFiles.find((file) => normalizePath(file.filePath) === targetPath); + if (matchingFile) { + violationFileContent = Utilities.removeSrcmlAnnotations(matchingFile.xml); + } else { + const targetFileName = targetPath.split('/').pop(); + const fallbackMatch = this.props.xmlFiles.find((file) => normalizePath(file.filePath).endsWith(`/${targetFileName}`)); + if (fallbackMatch) { + violationFileContent = Utilities.removeSrcmlAnnotations(fallbackMatch.xml); + } + } + } + + this.setState({ fixButtonClicked: true, originalFileContent: violationFileContent }); + // prevent multiple calls to suggestFix if (!this.state.suggestionCreated) { - suggestFix( + const conversationHistory = await suggestFix( rule, parsedExample, parsedSnippet, exampleFilePath, violationFilePath, + violationFileContent, this.setState.bind(this), ); + + this.saveConversationToSessionStorage(key, conversationHistory); + // notify the component that this snippet now has a suggested fix this.setState({ suggestionCreated: true }); } }; + + + handleEditFix = async (suggestionFileName, uniqueKey) => { + + const convHistory = this.getConversationFromSessionStorage(uniqueKey); + //console.log("convHistory"); + //console.log(convHistory); + + + const xmlFiles = this.props.xmlFiles; + + + + // Function to extract the file name from the filePath + const extractFileName = (filePath) => { + const parts = filePath.split('/'); + return parts[parts.length - 1]; + }; + + // Iterate over xmlFiles to find the matching file + const matchingFile = xmlFiles.find(file => extractFileName(file.filePath) === suggestionFileName); + let codeOfSuggestionFile = ''; + + if (matchingFile) { + console.log("Found matching file:"); + //console.log(matchingFile.xml); + codeOfSuggestionFile = Utilities.removeSrcmlAnnotations(matchingFile.xml); + // Do something with the matchingFile.xml here + + //console.log("before getting into normalizeFunction"); + //console.log(codeOfSuggestionFile); + + //console.log("after normalizeFunction"); + //console.log(codeOfSuggestionFile); + } else { + console.log("No matching file found"); + } + //console.log("Matching file content"); + //console.log(codeOfSuggestionFile); + const originalCode = Utilities.removeSrcmlAnnotations(this.state.d.surroundingNodes); + const modifiedCode = convHistory.data.modifiedFileContent; + const diff = this.generateDiff(originalCode, modifiedCode); + //console.log("THE DIFF"); + //console.log(diff); + + + + // Define the processDiff function inside handleEditFix + const processDiff = (diffArray) => { + return diffArray.map(diff => { + let message = ''; + if (diff.type === 'added') { + message = 'This line was added by you as part of solution: '; + } else if (diff.type === 'removed') { + message = 'This line was removed by you as part of solution: '; + } + return message + diff.text; + }); + }; + + const processedDiff = processDiff(diff); + + // Join all the elements into a single string with each element on a separate line + const resultText = processedDiff.join('\n'); + + console.log(resultText); + + const response = await editFix(codeOfSuggestionFile, resultText, this.setState.bind(this)); + } + + generateDiff = (originalCode, modifiedCode) => { + const normalizeForComparison = (line) => line.replace(/^\s+/, '').replace(/\s+$/, ''); + const originalLines = originalCode.split('\n'); + const modifiedLines = modifiedCode.split('\n'); + + const originalCounts = new Map(); + originalLines.forEach((line) => { + const key = normalizeForComparison(line); + originalCounts.set(key, (originalCounts.get(key) || 0) + 1); + }); + + const remainingOriginalCounts = new Map(originalCounts); + const diff = []; + + modifiedLines.forEach((line) => { + const key = normalizeForComparison(line); + const remaining = remainingOriginalCounts.get(key) || 0; + if (remaining > 0) { + remainingOriginalCounts.set(key, remaining - 1); + } else { + diff.push({ type: 'added', text: line }); + } + }); + + originalLines.forEach((line) => { + const key = normalizeForComparison(line); + const remaining = remainingOriginalCounts.get(key) || 0; + if (remaining > 0) { + diff.push({ type: 'removed', text: line }); + remainingOriginalCounts.set(key, remaining - 1); + } + }); + + console.log('DIFF'); + console.log(diff); + + return diff; + }; + + renderDiff = () => { + const originalCode = this.state.originalFileContent; + const modifiedCode = this.state.suggestedSnippet || ''; + const diff = this.generateDiff(originalCode, modifiedCode); + + const highlightCode = (code) => { + return Prism.highlight(code, Prism.languages.java, 'java'); + }; + + return ( +
+ {diff.map((line, index) => ( +
+ + {line.type === 'added' ? '+' : '-'} + + +
+ ))} +
+ ); + }; + + render() { - // NOTE: These styles can be moved to index.css in the future. - // There was an issue with that, so this is a quick fix + const uniqueKey = this.state.d.filePath; + const apiKey = localStorage.getItem("OPENAI_API_KEY"); + const titleStyle = { color: "#333", fontSize: "1.10em", @@ -555,14 +744,42 @@ class SnippetView extends Component { }; const buttonParent = { - position: "absolute", - top: "0", - right: "0", + position: "relative", + //top: "0", + //right: "0", zIndex: "1", }; - // Store the API key in a variable - const apiKey = localStorage.getItem("OPENAI_API_KEY"); + const containerStyle = { + display: "flex", + flexDirection: "column", + width: "100%", + padding: "10px", + border: "1px solid grey", + marginTop: "2px", + borderRadius: "5px" + }; + + const paneStyle = { + padding: "10px", + borderBottom: "1px solid grey", + marginTop: "2px", + borderRadius: "0px" + }; + + const highlightCode = (code) => { + return Prism.highlight(code, Prism.languages.java, 'java'); + }; + + const wrapperStyle = { + display: 'flex', + alignItems: 'center', + width: '100%', + }; + + const contentStyle = { + flex: 1, + }; return (
@@ -573,6 +790,7 @@ class SnippetView extends Component { >
{ this.props.onIgnoreFile(true); Utilities.sendToServer( @@ -582,81 +800,168 @@ class SnippetView extends Component { ); }} > -
-
-                        
-                            {/* render the following IF this is a violation of a rule and there is no fix yet */}
-                            {this.state.snippetGroup === "violated" &&
-                                // Use the apiKey variable in the conditional rendering check
-                                apiKey !== null &&
-                                apiKey !== "" &&
-                                !this.state.suggestedSnippet && (
-                                    
-                                )}
-                        
+                        

Violated Code Snippet

+
+
+
+
+
+                            
+                                {/* render the following IF this is a violation of a rule and there is no fix yet */}
+                                {this.state.snippetGroup === "violated" &&
+                                    apiKey !== null &&
+                                    apiKey !== "" &&
+                                    !this.state.suggestedSnippet && (
+                                        
+                                    )}
+                            
+                        
- {this.state.suggestionCreated && !this.state.suggestedSnippet && ( -

Loading Fix...

+ {!this.state.suggestionCreated && this.state.fixButtonClicked && ( +

Loading Fix...

)} + {this.state.suggestionCreated && this.state.suggestedSnippet && ( +
+ {/*
*/} + {/*

Suggested Fix:

+
*/}
+
+                            
+ +

Suggestion Location:

+

+

+ + +
+

Suggested Fix:

+ {this.renderDiff()} +
+ +
+

Explanation:

+

+

+ + + +
+ + {/* + + */} + + +
- {/* render the following IF the component state has received snippet */} - {this.state.suggestedSnippet && ( -
-

Suggested Fix:

-
-                            

Suggestion Location:

-

-

Explanation:

-

- -

)}
@@ -672,4 +977,4 @@ class SnippetView extends Component { exampleFilePath: nextProps.exampleFilePath }); } -} \ No newline at end of file +}