Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Enhance file handling and code editing functionality #2646

Merged
merged 29 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
665d260
refactor: Enhance file handling and code editing functionality
PierrunoYT Jun 26, 2024
fe7ebbe
Merge branch 'refactor/enhance-file-handling-and-editing' of https://…
PierrunoYT Jun 26, 2024
2b0c00f
Added Docstrings back
PierrunoYT Jun 26, 2024
c6c8a05
Fix
PierrunoYT Jun 26, 2024
19cbb81
Merge branch 'main' into refactor/enhance-file-handling-and-editing
PierrunoYT Jun 26, 2024
0023260
Merge branch 'main' into refactor/enhance-file-handling-and-editing
PierrunoYT Jun 27, 2024
ca6b4ac
Add internationalization for 'File saved successfully' message
PierrunoYT Jun 27, 2024
90176be
Merge branch 'refactor/enhance-file-handling-and-editing' of https://…
PierrunoYT Jun 27, 2024
0fd564e
Add toast notifications for error handling in fileService
PierrunoYT Jun 27, 2024
3951cbf
Add file path safety check and improve error handling in file services
PierrunoYT Jun 27, 2024
e10a235
Add docstrings to listen.py
PierrunoYT Jun 27, 2024
4f3c56f
Revert exclude_list formatting and add docstrings in listen.py
PierrunoYT Jun 27, 2024
cdf66eb
Merge branch 'main' into refactor/enhance-file-handling-and-editing
PierrunoYT Jun 27, 2024
6e4b170
Merge branch 'main' into refactor/enhance-file-handling-and-editing
PierrunoYT Jun 27, 2024
59fa355
Merge branch 'main' into refactor/enhance-file-handling-and-editing
PierrunoYT Jun 27, 2024
453f645
made code reviewable
tobitege Jun 30, 2024
ebcbf7d
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jun 30, 2024
fb610cc
fixed ruff issues
tobitege Jun 30, 2024
fe22dba
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jun 30, 2024
3411938
Update listen.py docstrings
tobitege Jun 30, 2024
6be5729
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jul 1, 2024
eee8a17
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jul 1, 2024
dccae4f
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jul 1, 2024
3bcb973
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jul 1, 2024
ba0160e
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jul 1, 2024
e4db390
final tweaks
tobitege Jul 1, 2024
1da375a
Merge branch 'main' into refactor/enhance-file-handling-and-editing
amanape Jul 2, 2024
419756c
re-added encodedURIComponent in selectFile
tobitege Jul 2, 2024
2c374f7
Merge branch 'main' into refactor/enhance-file-handling-and-editing
tobitege Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 124 additions & 31 deletions frontend/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
import React, { useMemo, useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import Editor, { Monaco } from "@monaco-editor/react";
import { Tab, Tabs } from "@nextui-org/react";
import { Tab, Tabs, Button } from "@nextui-org/react";
import { VscCode, VscSave, VscCheck } from "react-icons/vsc";
import type { editor } from "monaco-editor";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { VscCode } from "react-icons/vsc";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import FileExplorer from "./file-explorer/FileExplorer";
import { setCode } from "#/state/codeSlice";
import toast from "#/utils/toast";
import { saveFile } from "#/services/fileService";
import AgentState from "#/types/AgentState";

function CodeEditor(): JSX.Element {
const { t } = useTranslation();
const dispatch = useDispatch();
const code = useSelector((state: RootState) => state.code.code);
const activeFilepath = useSelector((state: RootState) => state.code.path);
const agentState = useSelector((state: RootState) => state.agent.curAgentState);
const [lastSaved, setLastSaved] = useState<string | null>(null);
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
const [showSaveNotification, setShowSaveNotification] = useState(false);

const selectedFileName = useMemo(() => {
const paths = activeFilepath.split("/");
return paths[paths.length - 1];
}, [activeFilepath]);

const handleEditorDidMount = (
const isEditingAllowed = useMemo(() => {
return agentState === AgentState.PAUSED ||
agentState === AgentState.FINISHED ||
agentState === AgentState.AWAITING_USER_INPUT;
}, [agentState]);

useEffect(() => {
let timer: NodeJS.Timeout;
if (saveStatus === 'saved') {
timer = setTimeout(() => setSaveStatus('idle'), 3000);
}
return () => clearTimeout(timer);
}, [saveStatus]);

const handleEditorDidMount = useCallback((
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => {
// 定义一个自定义主题 - English: Define a custom theme
monaco: Monaco
): void => {
monaco.editor.defineTheme("my-theme", {
base: "vs-dark",
inherit: true,
Expand All @@ -33,50 +55,121 @@ function CodeEditor(): JSX.Element {
},
});

// 应用自定义主题 - English: apply custom theme
monaco.editor.setTheme("my-theme");
}, []);

const handleEditorChange = useCallback((value: string | undefined): void => {
if (value !== undefined && isEditingAllowed) {
dispatch(setCode(value));
setSaveStatus('idle');
}
}, [dispatch, isEditingAllowed]);

const handleSave = useCallback(async (): Promise<void> => {
if (saveStatus === 'saving' || !isEditingAllowed) return;

setSaveStatus('saving');
setLastSaved(null);

try {
await saveFile(activeFilepath, code);
const now = new Date().toLocaleTimeString();
setLastSaved(now);
setSaveStatus('saved');
setShowSaveNotification(true);
setTimeout(() => setShowSaveNotification(false), 2000);
toast.success("File saved successfully!", "Save Successful");
console.log(`File "${selectedFileName}" has been saved.`);
} catch (error) {
console.error("Error saving file:", error);
setSaveStatus('error');
if (error instanceof Error) {
toast.error(`Failed to save file: ${error.message}`, "Save Error");
} else {
toast.error("An unknown error occurred while saving the file", "Save Error");
}
}
}, [saveStatus, activeFilepath, code, selectedFileName, isEditingAllowed]);

const getSaveButtonColor = () => {
switch (saveStatus) {
case 'saving': return 'bg-yellow-600';
case 'saved': return 'bg-green-600';
case 'error': return 'bg-red-600';
default: return 'bg-blue-600';
}
};

return (
<div className="flex h-full w-full bg-neutral-900 transition-all duration-500 ease-in-out">
<div className="flex h-full w-full bg-neutral-900 transition-all duration-500 ease-in-out relative">
<FileExplorer />
<div className="flex flex-col min-h-0 w-full">
<Tabs
disableCursorAnimation
classNames={{
base: "border-b border-divider border-neutral-600 mb-4",
tabList:
"w-full relative rounded-none bg-neutral-900 p-0 border-divider",
cursor: "w-full bg-neutral-600 rounded-none",
tab: "max-w-fit px-4 h-[36px]",
tabContent: "group-data-[selected=true]:text-white",
}}
aria-label="Options"
>
<Tab
key={selectedFileName.toLocaleLowerCase()}
title={selectedFileName}
/>
</Tabs>
<div className="flex justify-between items-center border-b border-neutral-600 mb-4">
<Tabs
disableCursorAnimation
classNames={{
base: "w-full",
tabList:
"w-full relative rounded-none bg-neutral-900 p-0 border-divider",
cursor: "w-full bg-neutral-600 rounded-none",
tab: "max-w-fit px-4 h-[36px]",
tabContent: "group-data-[selected=true]:text-white",
}}
aria-label="Options"
>
<Tab
key={selectedFileName.toLowerCase()}
title={selectedFileName || "No file selected"}
/>
</Tabs>
{selectedFileName && (
<div className="flex items-center mr-2">
<Button
onClick={handleSave}
className={`${getSaveButtonColor()} text-white transition-colors duration-300 mr-2`}
size="sm"
startContent={<VscSave />}
disabled={saveStatus === 'saving' || !isEditingAllowed}
>
{saveStatus === 'saving' ? "Saving..." : "Save"}
</Button>
{lastSaved && (
<span className="text-xs text-gray-400">
Last saved: {lastSaved}
</span>
)}
</div>
)}
</div>
<div className="flex grow items-center justify-center">
{selectedFileName === "" ? (
{!selectedFileName ? (
<div className="flex flex-col items-center text-neutral-400">
<VscCode size={100} />
{t(I18nKey.CODE_EDITOR$EMPTY_MESSAGE)}
</div>
) : (
<Editor
height="100%"
path={selectedFileName.toLocaleLowerCase()}
path={selectedFileName.toLowerCase()}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure tbh

defaultValue=""
value={code}
onMount={handleEditorDidMount}
onChange={handleEditorChange}
options={{ readOnly: !isEditingAllowed }}
/>
)}
</div>
</div>
{showSaveNotification && (
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2">
<div className="bg-green-500 text-white px-4 py-2 rounded-lg flex items-center justify-center animate-pulse">
<VscCheck className="mr-2 text-xl" />
<span>File saved successfully</span>
PierrunoYT marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
)}
</div>
);
}

export default CodeEditor;
export default CodeEditor;
60 changes: 47 additions & 13 deletions frontend/src/services/fileService.ts
tobitege marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,23 +1,57 @@
import { request } from "./api";

export async function selectFile(file: string): Promise<string> {
const data = await request(`/api/select-file?file=${file}`);
return data.code as string;
try {
const data = await request(`/api/select-file?file=${encodeURIComponent(file)}`);
if (typeof data.code !== 'string') {
throw new Error('Invalid response format: code is not a string');
}
return data.code;
} catch (error) {
console.error('Error selecting file:', error);
throw error;
}
}

export async function uploadFiles(files: FileList) {
const formData = new FormData();
for (let i = 0; i < files.length; i += 1) {
formData.append("files", files[i]);
}
export async function uploadFiles(files: FileList): Promise<void> {
try {
const formData = new FormData();
Array.from(files).forEach(file => formData.append("files", file));

await request("/api/upload-files", {
method: "POST",
body: formData,
});
await request("/api/upload-files", {
method: "POST",
body: formData,
});
} catch (error) {
console.error('Error uploading files:', error);
throw error;
PierrunoYT marked this conversation as resolved.
Show resolved Hide resolved
}
}

export async function listFiles(path: string = "/"): Promise<string[]> {
const data = await request(`/api/list-files?path=${path}`);
return data as string[];
try {
const data = await request(`/api/list-files?path=${encodeURIComponent(path)}`);
if (!Array.isArray(data)) {
throw new Error('Invalid response format: data is not an array');
}
return data;
} catch (error) {
console.error('Error listing files:', error);
throw error;
}
}

export async function saveFile(filePath: string, content: string): Promise<void> {
try {
PierrunoYT marked this conversation as resolved.
Show resolved Hide resolved
await request("/api/save-file", {
method: "POST",
body: JSON.stringify({ filePath, content }),
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error('Error saving file:', error);
throw error;
}
}
Loading