diff --git a/ai-hub/app/core/pipelines/code_changer.py b/ai-hub/app/core/pipelines/code_changer.py index cb2f9c0..d6b2858 100644 --- a/ai-hub/app/core/pipelines/code_changer.py +++ b/ai-hub/app/core/pipelines/code_changer.py @@ -50,17 +50,26 @@ ----- - ### 2\. Code Generation Rules - Please provide **one complete and functional code change** for the specified `file_path`. You must output the **entire, modified file**. + ### 2\. 💻 Code Generation Rules - * **Identical Code:** For sections of code that remain unchanged from the original file, use the single-line syntax `#[unchanged]|||`. For example, `#[unchanged]|/app/main.py|267|334` means the code is identical to lines 267 through 334 of the original file at `/app/main.py`. - * **Completeness:** The provided code must be self-contained, including all necessary dependencies, imports, and fully resolved definitions. - - Don't abuse the identidical syntax, it should be only used when a big chunk of duplicated code is in the updated file. + Please provide **one complete and functional code file** per request, for the specified `file_path`. You must output the **entire, modified file**. - ### **Code Quality Requirements** + --- + ### **1. Syntax and Formatting** - "Please provide complete and functional code that is ready to execute. When I request a code change, you must output the **entire, modified file**. Do not use abbreviations, placeholders, or comments like `...` or `# Existing code remains the same`. The code you provide should include all necessary dependencies and imports, and have all definitions fully resolved within the provided scope. Ensure the code is modular, well-structured, and follows best practices, including good naming conventions and clear, concise comments where necessary." + * **Identical Code Sections:** Use the `#[unchanged_section]|||` syntax for large, sequential blocks of code that are not being modified. This is the **only permitted syntax** for indicating unchanged code. Do not use this for small or scattered sections. + * **Complete File Output:** Always provide the **full file contents** in the `content` block, including all necessary imports and dependencies. Do not use placeholders like `...`, or comments such as `# rest of the code`, or `# existing code`. + * **Imports:** Ensure all required imports are included in the provided file. Use the same import syntax as the original file and prefer absolute paths. + + --- + + ### **2. Quality and Best Practices** + + * **Completeness:** The code you provide must be **self-contained and ready to execute**. All necessary definitions and functions must be fully resolved within the output. + * **Modularity and Style:** The code must be well-structured, modular, and follow best practices. Use clear naming conventions and add concise comments to explain complex logic. + * **Single File Modification:** Each response should **only modify a single file**. Provide the complete, modified file. + * **Token Limit Handling:** If modifying a large file might exceed the token limit, prioritize providing the partial change. Leave comments in the code for sections you haven't completed, and use the `#[unchanged_section]` syntax to avoid outputting the remaining unmodified code. + #### 🔹 Change Types * **File Modification**: @@ -98,8 +107,8 @@ desc="A JSON list of dictionaries with 'file_path' and 'content' for files modified by previous steps." ) - reasoning = dspy.OutputField(desc="A detailed reasoning process for the code change.") - content = dspy.OutputField(desc="The generated code.") + reasoning = dspy.OutputField(desc="Provide a detailed, comprehensive reasoning process for any requested code changes. The explanation must clearly justify all modifications. Remind yourselves, you should duplicate unchanged original code to ensure your new code is comprehensive, use `#[unchanged_section]|||` to save output lines.") + content = dspy.OutputField(desc="The generated full usable code without using abbreviations, placeholders, or comments like `code remains the same`") class CodeRagCodeChanger(dspy.Module): diff --git a/ai-hub/app/core/pipelines/code_reviewer.py b/ai-hub/app/core/pipelines/code_reviewer.py new file mode 100644 index 0000000..90cb06b --- /dev/null +++ b/ai-hub/app/core/pipelines/code_reviewer.py @@ -0,0 +1,194 @@ +import dspy +import json +from typing import List, Dict, Any, Tuple, Optional, Callable + +class CodeReviewerSignature(dspy.Signature): + """ + ### 🧠 Core Directives + + ## Code Review Directives + + Your role is a specialized code review AI. Your primary task is to evaluate a recent set of code changes and confirm if they **fully and accurately address the user's original request**. + ### Critical Constraints + Your review is strictly limited to **code content completeness**. You are explicitly forbidden from suggesting or performing any file splits, moves, or large-scale refactoring. This constraint is critical to prevent endless rework loops. + Pay close attention to any placeholders, comments or notes within the code that indicate a section is "the same as the original", "to-do" or "unchanged," as these areas are your focus for completion. + + **Zero** tolerance for those implementations are leaving comments or placeholders and any comments saying "replace with your actual..." + + There is only one exception that you can treat the code section is the same as the original file: + There is a system syntax to denote large, sequential blocks of code that are not being modified: + `#[unchanged_section]|||` e.g. #[unchanged_section]|/app/main.py|10|24 + + + If the output is satisfactory, you will signal this. If not, you will + provide a new, detailed, and actionable sequential plan to correct the + deficiencies. + + --- + + ### 1. Input Structure + + You will be provided with: + + - `original_question`: The user's initial request. + - `execution_plan`: The step-by-step plan that was previously generated. + - `final_code_changes`: The list of final code files and their content that + resulted from the execution of the plan. + - `original_files`: The original, unmodified files for context. + + --- + + ### 2. Decision Logic + + You must choose one of two mutually exclusive decisions: `complete` or `modify`. + + ### `decision='complete'` + + * **When to use:** Choose this if the `final_code_changes` fully and correctly + address the `original_question` and adhere to the `execution_plan`. The + generated code should be bug-free, well-structured, no placeholder, not partial code, and **COMPLETE**. + + ### `decision='modify'` + + #### **When to Use** + + You must evaluate `final_code_changes` to determine whether the generated code fully satisfies the user's request and correctly implements the intended execution plan. + + Choose `decision='modify'` if the `final_code_changes` include comments or indications such as: + + * `"same as before"` + * `"most similar code"` + * Or **any other sign that the code is incomplete or partially implemented** + + These phrases typically suggest that the code is unfinished, uses placeholder content, or lacks full implementation. In such cases, the generated code must be revised to: + + * Be fully implemented and functional + * Contain no vague or placeholder comments + * Meet all requirements outlined in the original user request and execution plan + * Be production-ready, with no unfinished areas remaining + + You should also generate a new execution plan that clearly outlines the necessary changes to achieve full and proper implementation. For those logic need to be implemented, search the original files to locate the real implementation and sepecifically mention where to find those logic to each step of the plans. + + ----- + + ### **High-Level Plan** + + The `answer` field must contain a **high-level strategy plan** for the proposed code changes. This plan should be broken down into a series of **specific, actionable instructions**, presented as a numbered list. + + * Each instruction must be a **discrete, testable step**. This ensures the changes are modular and easy to follow. + * The instructions for creating a new file should be a separate, explicit step that includes the exact, executable code or content to be added. Avoid high-level descriptions; instead, provide a detailed, step-by-step guide for an entry-level developer. For example, specify: "Add a function named sqrt() that accepts a string input and returns a string array. Clearly define the parameters, expected output, and the logic required to cover scenarios 'a', 'b', 'c', and 'd'." * Your sequential steps must eventually form a complete, shippable code solution eventually. Do not use 'to-do' notes or placeholders in early steps, but did not complete those to-dos in later steps. Every step should contribute to the final, functional code. + * Your proposed plan must be fully completed and implemented by the provided steps. Do not create placeholders or incomplete tasks in one step without following through to implement the full logic in a later step. + * The number of steps should be balanced based on the complexity of the code. Avoid breaking the plan into too many fine-grained steps, but also avoid combining a massive change into a single step. Aim for a logical, well-paced sequence that can be followed step-by-step. + + **Example Plan Breakdown:** + + * **Plan Breakdown:** + 1. **Complete code in /workspace.py** Replace the comment mentioned at line 5212 (`blablabla`) with the code from original file /work.py from line 142 to 152 + + ----- + + ### **Code Change Instructions Format** + + The response must be a **JSON list of objects**. No other text, fields, or conversational elements are allowed. + + ```json + [ + { + "file_path": "/app/main.py", + "action": "modify", + "change_instruction": "Complete code in /workspace.py** Replace the comment mentioned at line 5212 (`blablabla`) with the code from original file /work.py from line 142 to 152", + "original_files": ["/app/core/services/tts.py", "/app/core/services/stt.py", "/app/main.py"], + "updated_files": ["/app/main.py"] + } + ... + ] + ```` + + ----- + + #### **Parameter Breakdown** + * **`file_path`** (string): The path for the file to be changed, created, or deleted. Must begin with a `/`. + * **New files**: Use a valid, non-existent path. + * **Deletions**: Use the path of the file to be removed. + * **`action`** (string): The operation on the file. Must be one of: `"create"`, `"delete"`, `"move"`, or `"modify"`. + * `"create"`: Creates a new file from scratch. + * `"delete"`: Deletes the entire file. + * `"move"`: This action renames or moves a file to a new path. It does not perform any code changes. The change_instruction for this action must explicitly state the new file path, which should be wrapped in backticks (``). + Example: "change_instruction": "Move the file to `/new/path/file.py`." + * `"modify"`: Makes partial code changes to an existing file, including inserting, deleting, or replacing lines of code. + * **`change_instruction`** (string): A clear and specific instruction for the code changer. + * **New files**: Briefly describe the file's purpose. + * **Deletions**: State the intent to delete the file. + * **`original_files`** (list of strings): Paths to pre-existing files needed for read-only context. This allows the AI to understand the change instruction based on the original files. This list should reference files from `retrieved_paths_with_content`. Use `[]` if no context is needed. Paths must begin with a `/`. + * **`updated_files`** (list of strings): Paths to files previously modified in the current session. This allows the AI to understand the changes made so far and handle incremental updates. Use this for referencing changes from earlier steps. Use `[]` if no previous changes are relevant. Paths must begin with a `/`. + ----- + + **Execution Note:** The list represents a stateful, ordered sequence of operations. Each subsequent step operates on the results of the previous ones. + + * `original_files`: This parameter provides a consistent, baseline view of the project's files before any modifications. It is essential for steps that require the original file content as a reference. + * `updated_files`: This parameter provides the **cumulative state** of the project after all prior steps have completed. It should be used to make sequential changes that depend on the output of previous operations. For stateless or independent operations (e.g., creating a new file from scratch), this parameter is not required. + Try your best to add those two fields if possible. + + + ### 3. Output Format + + Return exactly one JSON object: + + ```json + { + "reasoning": "A detailed explanation of why the decision was made.", + "decision": "Either 'complete' or 'modify'.", + "answer": "If 'complete', an empty string. If 'modify', the new execution plan." + } + ``` + """ + + original_question = dspy.InputField(desc="The user's initial question or request.") + execution_plan = dspy.InputField(desc="The high-level plan that was executed.") + final_code_changes = dspy.InputField(desc="A JSON list of the final modified files and their content.") + original_files = dspy.InputField(desc="A JSON list of the original, unmodified files.") + + reasoning = dspy.OutputField( + desc="A step-by-step reasoning process explaining the decision. If the decision is to modify, explain what went wrong and how the new plan addresses it." + ) + decision = dspy.OutputField( + desc="The decision type for the response. Must be 'complete' or 'modify'." + ) + answer = dspy.OutputField( + desc=( + "If `decision` is 'complete', this field should be an empty string.\n" + "If `decision` is 'modify', this field should be a JSON-formatted list of objects representing the new code change instructions." + ) + ) + +class CodeReviewer(dspy.Module): + """ + A pipeline to review and validate code changes against an original request and plan. + """ + + def __init__(self): + super().__init__() + self.reviewer = dspy.ChainOfThought(CodeReviewerSignature) + + async def forward( + self, + original_question: str, + execution_plan: str, + final_code_changes: List[Dict[str, Any]], + original_files: List[Dict[str, Any]] + ) -> Tuple[str, str, str]: + + # Convert dictionaries to JSON strings for the model + final_code_changes_json = json.dumps(final_code_changes) + original_files_json = json.dumps(original_files) + + # Generate the review + prediction = await self.reviewer.acall( + original_question=original_question, + execution_plan=execution_plan, + final_code_changes=final_code_changes_json, + original_files=original_files_json + ) + + # Return the decision, reasoning, and any new plan + return prediction.decision, prediction.reasoning, prediction.answer \ No newline at end of file diff --git a/ai-hub/app/core/pipelines/question_decider.py b/ai-hub/app/core/pipelines/question_decider.py index d9ae328..fadbc84 100644 --- a/ai-hub/app/core/pipelines/question_decider.py +++ b/ai-hub/app/core/pipelines/question_decider.py @@ -85,7 +85,12 @@ ### **High-Level Plan** - The `answer` field must contain a **high-level strategy plan** for the proposed code changes. This plan should be broken down into a series of **specific, actionable instructions**. Each instruction must represent an independent, testable step. This ensures that the changes are modular and easy to follow. + The `answer` field must contain a **high-level strategy plan** for the proposed code changes. This plan should be broken down into a series of **specific, actionable instructions**, presented as a numbered list. + + * Each instruction must be a **discrete, testable step**. This ensures the changes are modular and easy to follow. + * The instructions for creating a new file should be a separate, explicit step that includes the exact, executable code or content to be added. Avoid high-level descriptions; instead, provide a detailed, step-by-step guide for an entry-level developer. For example, specify: "Add a function named sqrt() that accepts a string input and returns a string array. Clearly define the parameters, expected output, and the logic required to cover scenarios 'a', 'b', 'c', and 'd'." * Your sequential steps must eventually form a complete, shippable code solution eventually. Do not use 'to-do' notes or placeholders in early steps, but did not complete those to-dos in later steps. Every step should contribute to the final, functional code. + * Your proposed plan must be fully completed and implemented by the provided steps. Do not create placeholders or incomplete tasks in one step without following through to implement the full logic in a later step. + * The number of steps should be balanced based on the complexity of the code. Avoid breaking the plan into too many fine-grained steps, but also avoid combining a massive change into a single step. Aim for a logical, well-paced sequence that can be followed step-by-step. **Example Plan Breakdown:** @@ -135,7 +140,18 @@ * **`updated_files`** (list of strings): Paths to files previously modified in the current session. This allows the AI to understand the changes made so far and handle incremental updates. Use this for referencing changes from earlier steps. Use `[]` if no previous changes are relevant. Paths must begin with a `/`. ----- - **Execution Note**: The order of objects in the list is crucial. Each step in the list has access to the changes made in all preceding steps. + **Execution Note:** The list represents a stateful, ordered sequence of operations. Each subsequent step operates on the results of the previous ones. + + * `original_files`: This parameter provides a consistent, baseline view of the project's files before any modifications. It is essential for steps that require the original file content as a reference. + * `updated_files`: This parameter provides the **cumulative state** of the project after all prior steps have completed. It should be used to make sequential changes that depend on the output of previous operations. For stateless or independent operations (e.g., creating a new file from scratch), this parameter is not required. + Try your best to add those two fields if possible. + + #### **Operational Constraints** + The format for each step is limited to modifying a single file's content at a time. This means that a single operation, such as moving code between two different files, is not possible. + Instead, you must handle this type of change as a two-part process: + 1. **Replicate** the code in the new file as one step. + 2. **Delete** the original code from the source file in a subsequent step. + This approach circumvents the single-file limitation and allows for multi-file changes. ### `decision='files'` When more files are needed to fulfill the user's request, use this decision to retrieve them. This decision is suitable for a subset of files. diff --git a/ai-hub/app/core/services/utils/code_change.py b/ai-hub/app/core/services/utils/code_change.py index 78d0fe3..fc73b9f 100644 --- a/ai-hub/app/core/services/utils/code_change.py +++ b/ai-hub/app/core/services/utils/code_change.py @@ -9,6 +9,7 @@ from typing import Dict, List, Any, Optional, Tuple from app.core.providers.factory import get_llm_provider from app.core.pipelines.code_changer import CodeRagCodeChanger +from app.core.pipelines.code_reviewer import CodeReviewer from fastapi import WebSocket logger = logging.getLogger(__name__) @@ -17,7 +18,7 @@ A helper class to process and manage a sequence of code change instructions. """ - def __init__(self, db: Session, provider_name: str, input_data: str, reasoning: str,request_id: uuid.UUID): + def __init__(self, db: Session, provider_name: str, original_question: str,input_data: str, reasoning: str,request_id: uuid.UUID): """ Initializes the CodeChangeHelper, parsing the input and setting up dependencies. @@ -30,7 +31,9 @@ self.input_data = input_data self.reasoning = reasoning self.llm_provider = get_llm_provider(provider_name) + self.original_question = original_question self.code_changer = CodeRagCodeChanger() + self.code_reviewer = CodeReviewer() self.parsed_data: List[Dict[str, Any]] = [] self.updated_files: Dict[str, Dict[str, str]] = {} @@ -130,28 +133,20 @@ } await websocket.send_text(json.dumps(client_log)) - async def _handle_intermediate_chat_message(self, websocket: WebSocket): - # This list will hold all parts of the Markdown message. - md_content_parts = [ - "**AI-Generated Execution Plan:**" # A bold, clear title. + async def _handle_intermediate_chat_message(self, websocket: WebSocket, title: str = "**AI-Generated Execution Plan:**"): + steps_content = [ ] - # Add each change instruction as a numbered list item. - for i, data in enumerate(self.parsed_data): - # Use f-string to create numbered list items with proper indentation. - md_content_parts.append(f"{i+1}. {data['change_instruction']}") + # # Add each change instruction as a numbered list item. + # for i, data in enumerate(self.parsed_data): + # # Use f-string to create numbered list items with proper indentation. + # steps_content.append(f"{i+1}. {data['change_instruction']}") - # Add a final message with a smaller, right-aligned format. - # Markdown doesn't have a native "small" or "right-align" feature. - # A common workaround is to use HTML. - md_content_parts.append("\n*Start executing...*") - - # Join the list with newlines to form a single Markdown string. - formatted_content = "\n".join(md_content_parts) client_log: Dict[str, Any] = { "type": "code_change", - "content": formatted_content, + "content": title, + "steps": self.parsed_data, "reasoning": self.reasoning, "done": False, } @@ -185,14 +180,24 @@ } return result - async def process(self, websocket: WebSocket) -> Dict[str, Dict[str, str]]: + async def _review_changes(self, final_code_changes: List[Dict[str, Any]]) -> Tuple[str, str, str]: + with dspy.context(lm=self.llm_provider): + decision, reasoning, answer = await self.code_reviewer.forward( + original_question=self.original_question, + execution_plan=self.input_data, + final_code_changes=final_code_changes, + original_files=[{"file_path": k, "content": v} for k, v in self.original_files.items()] + ) + return decision, reasoning, answer + + async def process(self, websocket: WebSocket, round :int = 0 ,title:str = "**AI-Generated Execution Plan:**") -> Dict[str, Dict[str, str]]: """ Executes all code change instructions in sequence. Returns: A dictionary of all updated files with their content and reasoning. """ - await self._handle_intermediate_chat_message(websocket) + await self._handle_intermediate_chat_message(websocket, title) for item in self.parsed_data: action = item.get("action") filepath = item.get("file_path") @@ -215,7 +220,27 @@ response = await self._process_ai_question(item) self.updated_files[filepath] = response reasoning = response.get("reasoning", "") - dspy.inspect_history(n=1) + # dspy.inspect_history(n=1) + if reasoning: await self._handle_thinking_log(websocket, reasoning) - return await self._post_process() \ No newline at end of file + if round <=5: + # Review the changes after all steps are processed + final_code_changes_list = [ + {"file_path": path, "content": data["content"]} + for path, data in self.updated_files.items() + ] + + decision, review_reasoning, new_plan = await self._review_changes(final_code_changes_list) + + await self._handle_thinking_log(websocket, f"Reviewed the code change and think the code is {decision}, reasoning: {review_reasoning}") + + if decision == "modify": + logger.info("Code review failed. Initiating a new modification plan.") + self.input_data = new_plan + self._parse_input_data() + self.reasoning = review_reasoning + # Recursively call process to execute the new plan + await self.process(websocket, round=round+1, title="**New AI-Generated Execution Plan:**") + + return await self._post_process() diff --git a/ai-hub/app/core/services/workspace.py b/ai-hub/app/core/services/workspace.py index b40548c..0d68978 100644 --- a/ai-hub/app/core/services/workspace.py +++ b/ai-hub/app/core/services/workspace.py @@ -619,9 +619,10 @@ # Use the LLM to make a decision with dspy.context(lm=get_llm_provider(provider_name="gemini")): crqd = CodeRagQuestionDecider() + original_question= context_data.get("question", "") try: raw_answer_text, reasoning, decision = await crqd( - question=context_data.get("question", ""), + question=original_question, history=session.messages, retrieved_data=context_data ) @@ -685,7 +686,7 @@ try: # The input_data is a JSON string of code change instructions - cch = CodeChangeHelper(db=self.db, provider_name="gemini", input_data=raw_answer_text, reasoning = reasoning,request_id= uuid.UUID(request_id)) + cch = CodeChangeHelper(db=self.db, provider_name="gemini", original_question=original_question, input_data=raw_answer_text, reasoning = reasoning,request_id= uuid.UUID(request_id)) # Use the CodeChangeHelper to process all code changes final_changes = await cch.process(websocket=websocket) diff --git a/ui/client-app/src/components/ChatWindow.css b/ui/client-app/src/components/ChatWindow.css index 0e6c37e..be77113 100644 --- a/ui/client-app/src/components/ChatWindow.css +++ b/ui/client-app/src/components/ChatWindow.css @@ -1,86 +1,23 @@ -code { - /* font-family: 'Courier New', Courier, monospace; */ +/* Inline code blocks */ + code { background-color: #f4f4f4; color: #333; padding: 2px 4px; border-radius: 4px; font-size: 90%; font-weight: bold; - } +} - pre { +/* Pre-formatted code blocks */ + pre { font-family: 'Courier New', monospace; background-color: #f4f4f4; padding: 1em; border-radius: 5px; border: 1px solid #ddd; overflow-x: auto; - white-space: pre-wrap; /* This helps with wrapping long lines */ - word-wrap: break-word; /* Prevents overflow */ + white-space: pre-wrap; + word-wrap: break-word; line-height: 1.5; color: #333; - } - -/* - This CSS file provides styles for an ordered list to make it - look more like a step-by-step guide with custom numbering, - styled to match the provided file list component. -*/ - -/* Container for the list */ -ol { - list-style: none; - padding: 0; - counter-reset: step-counter; - margin: 1rem 0; - font-family: 'Inter', sans-serif; } - -/* Style for each list item, simulating the file list item's look */ -ol li { - background-color: #f7f9fc; - /* border: 1px solid #e0e6ed; */ - border-radius: 8px; - padding: 1rem; - margin-bottom: 0.75rem; - display: flex; - align-items: flex-start; - gap: 1rem; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - transition: transform 0.2s ease, box-shadow 0.2s ease; - cursor: pointer; /* Added a cursor pointer to make it feel clickable */ -} - -/* Add a hover effect to match the file list */ -ol li:hover { - background-color: #e6e9ef; /* Adjusted hover color to be slightly darker */ - /* transform: translateY(-2px); */ - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -/* The magic for the step prefix using a pseudo-element */ -ol li::before { - font-size: 0.9rem; - font-weight: bold; - color: #3b82f6; /* Used the vibrant blue from the icon */ - flex-shrink: 0; -} - -/* Styling for the text content within the list item */ -ol li p { - margin: 0; - font-size: 0.9rem; - line-height: 1.5; - color: #333; -} - - -.d2h-code-line, -.d2h-code-side-line { - position: static !important; -} - -/* Optional: Prevent sticky file headers */ -.d2h-file-header { - position: static !important; -} \ No newline at end of file diff --git a/ui/client-app/src/components/ChatWindow.js b/ui/client-app/src/components/ChatWindow.js index 4e37863..325dce7 100644 --- a/ui/client-app/src/components/ChatWindow.js +++ b/ui/client-app/src/components/ChatWindow.js @@ -3,6 +3,7 @@ import './ChatWindow.css'; import FileListComponent from "./FileList"; import DiffViewer from "./DiffViewer"; +import CodeChangePlan from "./CodeChangePlan"; // Individual message component const ChatMessage = ({ message }) => { @@ -47,6 +48,9 @@ {message.code_changes && ( )} + {message.steps && ( + + )} {selectedFile && } ); diff --git a/ui/client-app/src/components/CodeChangePlan.css b/ui/client-app/src/components/CodeChangePlan.css new file mode 100644 index 0000000..b8fccea --- /dev/null +++ b/ui/client-app/src/components/CodeChangePlan.css @@ -0,0 +1,105 @@ +/* Container for the list */ +.code-change-plan { + list-style: none; + padding: 0; + margin: 1rem 0; + font-family: 'Inter', sans-serif; + counter-reset: step-counter; +} + +/* Style for each list item */ +.code-change-plan-step { + background-color: #f7f9fc; + border: 1px solid #e0e6ed; + border-radius: 8px; + padding: 1rem; + margin-bottom: 0.75rem; + display: flex; + align-items: flex-start; + gap: 1rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; + cursor: pointer; + position: relative; /* Required for absolute tooltip */ + counter-increment: step-counter; +} + +/* Add a hover effect */ +.code-change-plan-step:hover { + background-color: #e6e9ef; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +/* Step prefix before each item */ +.code-change-plan-step::before { + content: "Step " counter(step-counter) "."; + font-size: 0.9rem; + font-weight: bold; + color: #3b82f6; + flex-shrink: 0; + margin-right: 0.5rem; +} + +/* Text content */ +.code-change-plan-step p { + margin: 0; + font-size: 0.9rem; + line-height: 1.5; + color: #333; + flex: 1; +} + +/* --- Updated Tooltip Styles --- */ +.step-details-popdown { + position: absolute; + left: 100%; + top: 0; + margin-left: 10px; + padding: 10px; + background-color: #ffffff; /* Lighter background, perhaps white or a very light grey */ + color: #333; /* Darker text for contrast on light background */ + border: 1px solid #e0e6ed; /* Border matching list items */ + border-radius: 8px; /* Matching border radius */ + white-space: nowrap; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); /* Softer, slightly larger shadow */ + z-index: 10; + display: flex; + flex-direction: column; + gap: 5px; +} + +.tooltip-action { + display: flex; + align-items: center; + gap: 8px; + font-weight: bold; + color: #333; /* Ensure action text is dark for readability */ +} + +.tooltip-file-path { + font-family: monospace; + font-size: 0.9em; + color: #555; /* Slightly darker grey for filepath */ + border-top: 1px solid #e0e6ed; /* Lighter border for separation */ + padding-top: 5px; + margin-top: 5px; +} + +/* Icon colors remain the same for clear visual cues */ +.create-icon { + color: #28a745; /* Green for create */ +} + +.delete-icon { + color: #dc3545; /* Red for delete */ +} + +.modify-icon { + color: #ffc107; /* Yellow for modify */ +} + +.code-change-plan { + overflow: visible; + position: relative; /* ensures tooltip is positioned correctly */ +} diff --git a/ui/client-app/src/components/CodeChangePlan.js b/ui/client-app/src/components/CodeChangePlan.js new file mode 100644 index 0000000..379a26b --- /dev/null +++ b/ui/client-app/src/components/CodeChangePlan.js @@ -0,0 +1,89 @@ +import { createPortal } from 'react-dom'; +import { useRef, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import './CodeChangePlan.css'; +import { FaPlus, FaTrash, FaEdit } from 'react-icons/fa'; + +// Tooltip component +const StepDetails = ({ stepItem, position }) => { + const style = { + position: 'absolute', + top: position?.top ?? 0, + left: position?.left ?? 0, + }; + + return ( +
+
+ {stepItem.action === 'create' && } + {stepItem.action === 'delete' && } + {stepItem.action === 'modify' && } + + {stepItem.action === 'create' + ? 'Create File' + : stepItem.action === 'delete' + ? 'Delete File' + : stepItem.action === 'modify' + ? 'Modify File' + : stepItem.action} + +
+
{stepItem.file_path}
+
+ ); +}; + +// Main component +const CodeChangePlan = ({ steps }) => { + const [hoveredStepIndex, setHoveredStepIndex] = useState(null); + const [tooltipPosition, setTooltipPosition] = useState(null); + const stepRefs = useRef([]); + + const handleMouseEnter = (index) => { + const stepElement = stepRefs.current[index]; + if (stepElement) { + const rect = stepElement.getBoundingClientRect(); + // const tooltipPadding = 10; + + setTooltipPosition({ + top: rect.bottom + window.scrollY + 6, // 6px spacing + left: rect.left + window.scrollX, + }); + } + setHoveredStepIndex(index); + }; + + const handleMouseLeave = () => { + setHoveredStepIndex(null); + setTooltipPosition(null); + }; + + return ( + <> +
    + {steps.map((step, index) => ( +
  • handleMouseEnter(index)} + onMouseLeave={handleMouseLeave} + ref={(el) => (stepRefs.current[index] = el)} + > + {step.change_instruction} +
  • + ))} +
+ + {hoveredStepIndex !== null && + createPortal( + , + document.body + )} + + ); +}; + +export default CodeChangePlan; diff --git a/ui/client-app/src/hooks/useCodeAssistant.js b/ui/client-app/src/hooks/useCodeAssistant.js index 90075f0..a2225f9 100644 --- a/ui/client-app/src/hooks/useCodeAssistant.js +++ b/ui/client-app/src/hooks/useCodeAssistant.js @@ -35,6 +35,7 @@ isUser: false, text: message.content, code_changes: message.code_changes, + steps: message.steps, reasoning: message.reasoning }]); if (message.done === true){