From 24d3e70563da6dd884e007fa2568c7af1423e26c Mon Sep 17 00:00:00 2001 From: Nahuel Chen Date: Sun, 2 Feb 2025 19:38:37 -0600 Subject: [PATCH 1/9] feat: updating setting bars and fixs some small bugs --- frontend/components.json | 5 ++++- frontend/package.json | 3 +++ frontend/src/components/chat/chat-bottombar.tsx | 2 +- frontend/src/components/detail-settings.tsx | 2 +- frontend/src/components/sidebar.tsx | 4 ++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/components.json b/frontend/components.json index b6f843f2..7d57192b 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -12,6 +12,9 @@ }, "aliases": { "components": "@/components", - "utils": "@/lib/utils" + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" } } diff --git a/frontend/package.json b/frontend/package.json index d594a50a..38a4fa9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,12 +15,14 @@ "generate:watch": "graphql-codegen --watch" }, "dependencies": { + "codefox-common": "workspace:*", "@apollo/client": "^3.11.8", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@hookform/resolvers": "^3.9.0", "@langchain/community": "^0.3.1", "@langchain/core": "^0.3.3", + "@monaco-editor/react": "^4.6.0", "@nestjs/common": "^10.4.6", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.4", @@ -32,6 +34,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", "@types/dom-speech-recognition": "^0.0.4", "class-variance-authority": "^0.7.1", diff --git a/frontend/src/components/chat/chat-bottombar.tsx b/frontend/src/components/chat/chat-bottombar.tsx index 70852fa5..829d2949 100644 --- a/frontend/src/components/chat/chat-bottombar.tsx +++ b/frontend/src/components/chat/chat-bottombar.tsx @@ -84,7 +84,7 @@ export default function ChatBottombar({ }, []); return ( -
+
diff --git a/frontend/src/components/detail-settings.tsx b/frontend/src/components/detail-settings.tsx index 3814aeda..6bd461d4 100644 --- a/frontend/src/components/detail-settings.tsx +++ b/frontend/src/components/detail-settings.tsx @@ -33,7 +33,7 @@ export default function DetailSettings() { Settings - + diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 5a2c2fb0..3de6f94e 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -33,7 +33,7 @@ interface SidebarProps { onRefetch: () => void; } -function CustomSidebar({ +function ChatsSiderBar({ isCollapsed, isMobile, chatListUpdated, @@ -134,7 +134,7 @@ function CustomSidebar({ ); } -export default memo(CustomSidebar, (prevProps, nextProps) => { +export default memo(ChatsSiderBar, (prevProps, nextProps) => { return ( prevProps.isCollapsed === nextProps.isCollapsed && prevProps.isMobile === nextProps.isMobile && From 7fee9e3683d65ea085f2b62298c07f8688dd694c Mon Sep 17 00:00:00 2001 From: Nahuel Chen Date: Sun, 2 Feb 2025 19:39:28 -0600 Subject: [PATCH 2/9] feat: adding code review with editor and file structure --- frontend/src/app/(main)/MainLayout.tsx | 46 ++++- frontend/src/app/(main)/layout.tsx | 8 +- frontend/src/app/api/file/route.ts | 91 ++++++++++ frontend/src/app/api/project/route.ts | 69 ++++++++ frontend/src/app/context/projectContext.tsx | 37 ++++ frontend/src/components/code-display.tsx | 184 ++++++++++++++++++++ frontend/src/components/file-structure.tsx | 109 ++++++++++++ frontend/src/components/fileSidebar.tsx | 96 ++++++++++ frontend/src/components/ui/tabs.tsx | 55 ++++++ frontend/src/utils/file_reader.ts | 110 ++++++++++++ 10 files changed, 796 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/api/file/route.ts create mode 100644 frontend/src/app/api/project/route.ts create mode 100644 frontend/src/app/context/projectContext.tsx create mode 100644 frontend/src/components/code-display.tsx create mode 100644 frontend/src/components/file-structure.tsx create mode 100644 frontend/src/components/fileSidebar.tsx create mode 100644 frontend/src/components/ui/tabs.tsx create mode 100644 frontend/src/utils/file_reader.ts diff --git a/frontend/src/app/(main)/MainLayout.tsx b/frontend/src/app/(main)/MainLayout.tsx index 4bdac043..b4cd69e5 100644 --- a/frontend/src/app/(main)/MainLayout.tsx +++ b/frontend/src/app/(main)/MainLayout.tsx @@ -2,11 +2,19 @@ import React, { useEffect, useState } from 'react'; import { cn } from '@/lib/utils'; -import { usePathname } from 'next/navigation'; import { useChatList } from '../hooks/useChatList'; -import { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; -import CustomSidebar from '@/components/sidebar'; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from '@/components/ui/resizable'; +import ChatSiderBar from '@/components/sidebar'; import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; +import { CodeDisplayer } from '@/components/code-display'; +import { Project } from '@/graphql/type'; +import FileStructure from '@/components/file-structure'; +import { ProjectProvider } from '../context/projectContext'; +import FileSidebar from '@/components/file-structure'; export default function MainLayout({ children, @@ -15,6 +23,7 @@ export default function MainLayout({ }) { const [isCollapsed, setIsCollapsed] = useState(false); const [isMobile, setIsMobile] = useState(false); + const projectId = '2025-01-31-f9b3465a-1bd0-4a56-b042-46864953d870'; const defaultLayout = [30, 160]; const navCollapsedSize = 10; const { @@ -30,6 +39,18 @@ export default function MainLayout({ document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(isCollapsed)}; path=/; max-age=604800`; }, [isCollapsed]); + useEffect(() => { + async function fetchFiles() { + const response = await fetch(`/api/project?id=${projectId}`); + const data = await response.json(); + if (data.files) { + setProjectFiles(data.files); + } + } + + fetchFiles(); + }, [projectId]); + useEffect(() => { const checkScreenWidth = () => { setIsMobile(window.innerWidth <= 1023); @@ -47,6 +68,7 @@ export default function MainLayout({
{ document.cookie = `react-resizable-panels:layout=${JSON.stringify( sizes @@ -71,7 +93,7 @@ export default function MainLayout({ }} className={cn( 'transition-all duration-300 ease-in-out', - isCollapsed ? 'min-w-[50px] md:min-w-[70px]' : 'md:min-w-[200px]' + isCollapsed ? 'min-w-[50px] md:min-w-[70px]' : 'md:min-w-[20%]' )} > {loading ? ( @@ -81,7 +103,7 @@ export default function MainLayout({ Error: {error.message}
) : ( - {children} + + + + + + ); } +function setProjectFiles(files: any) { + throw new Error('Function not implemented.'); +} diff --git a/frontend/src/app/(main)/layout.tsx b/frontend/src/app/(main)/layout.tsx index 179c2625..18c2253f 100644 --- a/frontend/src/app/(main)/layout.tsx +++ b/frontend/src/app/(main)/layout.tsx @@ -3,6 +3,8 @@ import { Inter } from 'next/font/google'; import MainLayout from './MainLayout'; import { SidebarProvider } from '@/components/ui/sidebar'; +import { getProjectPath, getProjectsDir, getRootDir } from 'codefox-common'; +import { FileReader } from '@/utils/file_reader'; const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { @@ -17,10 +19,10 @@ export const viewport: Viewport = { userScalable: false, }; -export default function Layout({ +export default async function Layout({ children, -}: Readonly<{ +}: { children: React.ReactNode; -}>) { +}) { return {children}; } diff --git a/frontend/src/app/api/file/route.ts b/frontend/src/app/api/file/route.ts new file mode 100644 index 00000000..f646a21b --- /dev/null +++ b/frontend/src/app/api/file/route.ts @@ -0,0 +1,91 @@ +// app/api/file/route.ts +import { NextResponse } from 'next/server'; +import { FileReader } from '@/utils/file_reader'; +import { promises as fs } from 'fs'; +import path from 'path'; + +export async function POST(req: Request) { + console.log('🚀 [API] Received POST request to update file'); + + try { + const { filePath, newContent } = await req.json(); + + if (!filePath || !newContent) { + console.error('[API] Missing required parameters'); + return NextResponse.json( + { error: "Missing 'filePath' or 'newContent'" }, + { status: 400 } + ); + } + const reader = FileReader.getInstance(); + reader.updateFile(filePath, newContent); + + console.log('[API] File updated successfully'); + return NextResponse.json({ + message: 'File updated successfully', + filePath, + }); + } catch (error) { + console.error('[API] Error updating file:', error); + return NextResponse.json( + { error: 'Failed to update file' }, + { status: 500 } + ); + } +} + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const filePath = searchParams.get('path'); + + if (!filePath) { + return NextResponse.json( + { error: "Missing 'path' parameter" }, + { status: 400 } + ); + } + const reader = FileReader.getInstance(); + const content = await reader.readFileContent(filePath); + const fileType = getFileType(filePath); + const res = NextResponse.json({ filePath, content, type: fileType }); + console.log(res); + return res; + } catch (error) { + return NextResponse.json({ error: 'Failed to read file' }, { status: 500 }); + } +} + +function getFileType(filePath: string): string { + const extension = filePath.split('.').pop()?.toLowerCase() || ''; + + const typeMap: { [key: string]: string } = { + txt: 'text', + md: 'markdown', + json: 'json', + js: 'javascript', + ts: 'typescript', + html: 'html', + css: 'css', + scss: 'scss', + xml: 'xml', + csv: 'csv', + yml: 'yaml', + yaml: 'yaml', + jpg: 'image', + jpeg: 'image', + png: 'image', + gif: 'image', + svg: 'vector', + webp: 'image', + mp4: 'video', + mp3: 'audio', + wav: 'audio', + pdf: 'pdf', + zip: 'archive', + tar: 'archive', + gz: 'archive', + }; + + return typeMap[extension] || 'unknown'; +} diff --git a/frontend/src/app/api/project/route.ts b/frontend/src/app/api/project/route.ts new file mode 100644 index 00000000..fc5b1118 --- /dev/null +++ b/frontend/src/app/api/project/route.ts @@ -0,0 +1,69 @@ +// app/api/project/route.ts +import { NextResponse } from 'next/server'; +import { FileReader } from '@/utils/file_reader'; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const projectId = searchParams.get('id'); + + if (!projectId) { + return NextResponse.json({ error: 'Missing projectId' }, { status: 400 }); + } + + try { + const res = await fetchFileStructure(projectId); + console.log(res); + return NextResponse.json({ res }); + } catch (error) { + return NextResponse.json( + { error: 'Failed to read project files' }, + { status: 500 } + ); + } +} + +async function fetchFileStructure(projectId) { + const reader = FileReader.getInstance(); + const res = await reader.getAllPaths(projectId); + + const projectPrefix = res[0].split('/')[0] + '/'; + + const cleanedPaths = res.map((path) => path.replace(projectPrefix, '')); + + function buildTree(paths) { + const tree = {}; + + paths.forEach((path) => { + const parts = path.split('/'); + let node = tree; + + parts.forEach((part, index) => { + if (!node[part]) { + node[part] = index === parts.length - 1 ? null : {}; + } + node = node[part]; + }); + }); + + return tree; + } + + function convertTreeToList(tree) { + return Object.keys(tree).map((name) => { + if (tree[name]) { + return { + name, + type: 'folder', + children: convertTreeToList(tree[name]), + }; + } else { + return { name, type: 'file' }; + } + }); + } + + const tree = buildTree(cleanedPaths); + const fileStructure = convertTreeToList(tree); + + return fileStructure; +} diff --git a/frontend/src/app/context/projectContext.tsx b/frontend/src/app/context/projectContext.tsx new file mode 100644 index 00000000..5c4108ad --- /dev/null +++ b/frontend/src/app/context/projectContext.tsx @@ -0,0 +1,37 @@ +'use client'; +import { + createContext, + Dispatch, + SetStateAction, + useContext, + useState, +} from 'react'; +interface ProjectContextType { + projectId: string; + setProjectId: Dispatch>; + filePath: string | null; + setFilePath: Dispatch>; +} + +const ProjectContext = createContext({ + projectId: '', + setProjectId: () => {}, + filePath: null, + setFilePath: () => {}, +}); + +export const useProject = () => useContext(ProjectContext); +export const ProjectProvider = ({ children }) => { + const [projectId, setProjectId] = useState( + '2025-01-31-f9b3465a-1bd0-4a56-b042-46864953d870' + ); + const [filePath, setFilePath] = useState('frontend/vite.config.ts'); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/components/code-display.tsx b/frontend/src/components/code-display.tsx new file mode 100644 index 00000000..ceb5f72e --- /dev/null +++ b/frontend/src/components/code-display.tsx @@ -0,0 +1,184 @@ +import { useProject } from '@/app/context/projectContext'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { useRef } from 'react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import Editor from '@monaco-editor/react'; +import { useEffect, useState } from 'react'; +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; +import FileStructure from './file-structure'; +import { motion } from 'framer-motion'; +export function CodeDisplayer() { + const editorRef = useRef(null); + + const { projectId, filePath } = useProject(); + const [preCode, setPrecode] = useState('// some coaamment'); + const [newCode, setCode] = useState('// some coaamment'); + const [saving, setSaving] = useState(false); + const [type, setType] = useState('javascript'); + const [isLoading, setIsLoading] = useState(false); + const [editor, setEditor] = useState(null); + const [isCollapsed, setIsCollapsed] = useState(false); // 默认折叠状态 + const handleEditorMount = (editor) => { + editorRef.current = editor; + + editor.getDomNode().style.position = 'absolute'; + }; + useEffect(() => { + async function getCode() { + try { + setIsLoading(true); + const res = await fetch( + `/api/file?path=${encodeURIComponent(`${projectId}/${filePath}`)}` + ).then((res) => res.json()); + console.log(res.content); + setCode(res.content); + setPrecode(res.content); + setType(res.type); + + setIsLoading(false); + } catch (error) { + console.error(error.message); + } + } + + getCode(); + }, [filePath, projectId]); + + const handleReset = () => { + console.log('Reset!'); + setCode(preCode); + console.log(editorRef.current?.getValue()); + editorRef.current?.setValue(preCode); + console.log(preCode); + console.log(editorRef.current?.getValue()); + setSaving(false); + }; + + const updateCode = async (value) => { + try { + const response = await fetch('/api/file', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + filePath: projectId + '/' + filePath, + newContent: JSON.stringify(value), + }), + }); + + const data = await response.json(); + } catch (error) { + /* empty */ + } + }; + + const handleSave = () => { + console.log('Saved!'); + setSaving(false); + setPrecode(newCode); + updateCode(newCode); + }; + + const updateSavingStatus = async (value, event) => { + setCode(value); + setSaving(true); + }; + return ( + + + Code + Password + + + + + + + + + + + {saving && } + + + + + + Password + + Change your password here. After saving, you'll be logged out. + + + +
+ + +
+
+ + +
+
+ + + +
+
+
+ ); +} + +const SaveChangesBar = ({ saving, onSave, onReset }) => { + return ( + saving && ( +
+ + Unsaved Changes + + +
+ ) + ); +}; diff --git a/frontend/src/components/file-structure.tsx b/frontend/src/components/file-structure.tsx new file mode 100644 index 00000000..56d5581b --- /dev/null +++ b/frontend/src/components/file-structure.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + FileIcon, + ChevronDownIcon, + ChevronRightIcon, +} from '@radix-ui/react-icons'; +import { useProject } from '@/app/context/projectContext'; + +interface FileNodeType { + name: string; + type: 'file' | 'folder'; + children?: FileNodeType[]; +} + +const FileNode = ({ + node, + fullPath, +}: { + node: FileNodeType; + fullPath: string; +}) => { + const [isOpen, setIsOpen] = useState(false); + const { setFilePath, filePath } = useProject(); + + const toggleOpen = () => { + if (node.type === 'folder') setIsOpen(!isOpen); + }; + + const handleChangeFile = () => { + if (node.type === 'file') setFilePath(fullPath); + }; + + return ( +
+ {node.type === 'folder' ? ( +
+ {isOpen ? ( + + ) : ( + + )} + {node.name} +
+ ) : ( +
+ {node.name} +
+ )} + + {isOpen && node.children && ( +
+ {node.children.map((child) => ( + + ))} +
+ )} +
+ ); +}; + +export default function FileStructure({ + isCollapsed, +}: { + isCollapsed: boolean; +}) { + const { projectId, filePath } = useProject(); + const [fileStructure, setStructure] = useState([]); + + useEffect(() => { + async function fetchFiles() { + try { + const response = await fetch(`/api/project?id=${projectId}`); + const data = await response.json(); + console.log('Fetched file structure:', data.res); + setStructure(data.res || []); + } catch (error) { + console.error('Error fetching file structure:', error); + } + } + + fetchFiles(); + }, [projectId]); + + return ( +
+
+

File Explorer

+ {filePath &&
{filePath}
} + {fileStructure.map((node) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/components/fileSidebar.tsx b/frontend/src/components/fileSidebar.tsx new file mode 100644 index 00000000..fe3a1ada --- /dev/null +++ b/frontend/src/components/fileSidebar.tsx @@ -0,0 +1,96 @@ +'use client'; +import { Button } from '@/components/ui/button'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { SquarePen } from 'lucide-react'; +import SidebarSkeleton from './sidebar-skeleton'; +import UserSettings from './user-settings'; +import { SideBarItem } from './sidebar-item'; +import { Chat } from '@/graphql/type'; +import { EventEnum } from './enum'; +import { + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarTrigger, + Sidebar, + SidebarRail, + SidebarFooter, +} from './ui/sidebar'; +import { cn } from '@/lib/utils'; + +export default function FileSidebar({ isCollapsed, isMobile, loading }) { + const [isSimple, setIsSimple] = useState(false); + const [currentChatid, setCurrentChatid] = useState(''); + const handleNewChat = useCallback(() => { + window.history.replaceState({}, '', '/'); + setCurrentChatid(''); + const event = new Event(EventEnum.NEW_CHAT); + window.dispatchEvent(event); + }, []); + + if (loading) return ; + // if (error) { + // console.error('Error loading chats:', error); + // return null; + // } + console.log(`${isCollapsed}, ${isMobile}, ${isSimple}`); + + return ( +
+ + setIsSimple(!isSimple)} + > + + + + + + + + + + + + + +
+ ); +} diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 00000000..933c7a10 --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/frontend/src/utils/file_reader.ts b/frontend/src/utils/file_reader.ts new file mode 100644 index 00000000..f8299635 --- /dev/null +++ b/frontend/src/utils/file_reader.ts @@ -0,0 +1,110 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { getProjectsDir } from 'codefox-common'; + +export class FileReader { + private static instance: FileReader; + private basePath: string; + + private constructor() { + let baseDir = getProjectsDir(); + + if (baseDir.includes('/.next/')) { + baseDir = baseDir.replace(/\/\.next[^\/]*/, ''); + } + + if (baseDir.includes('/frontend/server/.codefox/')) { + baseDir = baseDir.replace('/frontend/server/.codefox/', '/.codefox/'); + } + + this.basePath = path.resolve(baseDir); + console.log('FileReader initialized with basePath:', this.basePath); + } + + public static getInstance(): FileReader { + if (!FileReader.instance) { + FileReader.instance = new FileReader(); + } + return FileReader.instance; + } + + public async getAllPaths(projectId: string): Promise { + const projectPath = path.resolve(this.basePath, projectId); + return this.readDirectory(projectPath); + } + + public async getAllShallowPaths(): Promise { + return this.readShallowDirectory(this.basePath); + } + + public async readFileContent(filePath: string): Promise { + const fullPath = path.join(this.basePath, filePath); + console.log('📄 Reading file:', fullPath); + + try { + return await fs.readFile(fullPath, 'utf-8'); + } catch (err) { + console.error(`Error reading file: ${fullPath}`, err); + throw new Error(`Failed to read file: ${fullPath}`); + } + } + + private async readDirectory(dir: string): Promise { + let filePaths: string[] = []; + + try { + const items = await fs.readdir(dir, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(dir, item.name); + const relativePath = path.relative(this.basePath, fullPath); + + filePaths.push(relativePath); + + if (item.isDirectory()) { + filePaths = filePaths.concat(await this.readDirectory(fullPath)); + } + } + } catch (err) { + console.error(`Error reading directory: ${dir}`, err); + } + return filePaths; + } + + private async readShallowDirectory(dir: string): Promise { + const filePaths: string[] = []; + + try { + const items = await fs.readdir(dir, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(dir, item.name); + filePaths.push(path.relative(this.basePath, fullPath)); + } + } catch (err) { + console.error(`Error reading directory: ${dir}`, err); + } + return filePaths; + } + + public async updateFile(filePath: string, newContent: string): Promise { + if (filePath.includes('..')) { + console.error('[FileReader] Invalid file path detected:', filePath); + throw new Error('Invalid file path'); + } + + const fullPath = path.join(this.basePath, filePath); + console.log(`📝 [FileReader] Updating file: ${fullPath}`); + + try { + const content = JSON.parse(newContent); + console.log(content); + await fs.writeFile(fullPath, content, 'utf-8'); + + console.log('[FileReader] File updated successfully'); + } catch (err) { + console.error(`[FileReader] Error updating file: ${fullPath}`, err); + throw new Error(`Failed to update file: ${fullPath}`); + } + } +} From 7fe3f0c72d5c327513416773552f7af44c5071fa Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 01:41:30 +0000 Subject: [PATCH 3/9] [autofix.ci] apply automated fixes --- frontend/src/components/code-display.tsx | 73 ++++++++++++---------- frontend/src/components/file-structure.tsx | 14 ++--- frontend/src/components/ui/tabs.tsx | 30 ++++----- pnpm-lock.yaml | 66 +++++++++++++++++++ 4 files changed, 129 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/code-display.tsx b/frontend/src/components/code-display.tsx index ceb5f72e..703d04c2 100644 --- a/frontend/src/components/code-display.tsx +++ b/frontend/src/components/code-display.tsx @@ -31,7 +31,7 @@ export function CodeDisplayer() { const handleEditorMount = (editor) => { editorRef.current = editor; - editor.getDomNode().style.position = 'absolute'; + editor.getDomNode().style.position = 'absolute'; }; useEffect(() => { async function getCode() { @@ -100,38 +100,47 @@ export function CodeDisplayer() { Password - - - + + + - - - - - {saving && } - + + + + + {saving && ( + + )} + diff --git a/frontend/src/components/file-structure.tsx b/frontend/src/components/file-structure.tsx index 56d5581b..d35c24a9 100644 --- a/frontend/src/components/file-structure.tsx +++ b/frontend/src/components/file-structure.tsx @@ -97,13 +97,13 @@ export default function FileStructure({ return (
-
-

File Explorer

- {filePath &&
{filePath}
} - {fileStructure.map((node) => ( - - ))} -
+
+

File Explorer

+ {filePath &&
{filePath}
} + {fileStructure.map((node) => ( + + ))} +
); } diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx index 933c7a10..5ef355c3 100644 --- a/frontend/src/components/ui/tabs.tsx +++ b/frontend/src/components/ui/tabs.tsx @@ -1,11 +1,11 @@ -"use client" +'use client'; -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import * as React from 'react'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -const Tabs = TabsPrimitive.Root +const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< React.ElementRef, @@ -14,13 +14,13 @@ const TabsList = React.forwardRef< -)) -TabsList.displayName = TabsPrimitive.List.displayName +)); +TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< React.ElementRef, @@ -29,13 +29,13 @@ const TabsTrigger = React.forwardRef< -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, @@ -44,12 +44,12 @@ const TabsContent = React.forwardRef< -)) -TabsContent.displayName = TabsPrimitive.Content.displayName +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49c65487..fd623ae7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,9 @@ importers: '@langchain/core': specifier: ^0.3.3 version: 0.3.33(openai@4.80.0) + '@monaco-editor/react': + specifier: ^4.6.0 + version: 4.6.0(monaco-editor@0.52.2)(react-dom@18.3.1)(react@18.3.1) '@nestjs/common': specifier: ^10.4.6 version: 10.4.15(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -364,6 +367,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.1.1 version: 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.6 version: 1.1.7(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) @@ -376,6 +382,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + codefox-common: + specifier: workspace:* + version: link:../codefox-common emoji-mart: specifier: ^5.6.0 version: 5.6.0 @@ -6359,6 +6368,28 @@ packages: react: 18.3.1 dev: false + /@monaco-editor/loader@1.4.0(monaco-editor@0.52.2): + resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} + peerDependencies: + monaco-editor: '>= 0.21.0 < 1' + dependencies: + monaco-editor: 0.52.2 + state-local: 1.0.7 + dev: false + + /@monaco-editor/react@4.6.0(monaco-editor@0.52.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@monaco-editor/loader': 1.4.0(monaco-editor@0.52.2) + monaco-editor: 0.52.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@nestjs/apollo@12.2.2(@apollo/server@4.11.3)(@nestjs/common@10.4.15)(@nestjs/core@10.4.15)(@nestjs/graphql@12.2.2)(graphql@16.10.0): resolution: {integrity: sha512-gsDqSfsmTSvF0k3XaRESRgM3uE/YFO+59txCsq7T1EadDOVOuoF3zVQiFmi6D50Rlnqohqs63qjjf46mgiiXgQ==} peerDependencies: @@ -7925,6 +7956,33 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-tabs@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-tooltip@1.1.7(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==} peerDependencies: @@ -17160,6 +17218,10 @@ packages: hasBin: true dev: false + /monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + dev: false + /motion-dom@11.18.1: resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} dependencies: @@ -20807,6 +20869,10 @@ packages: escape-string-regexp: 2.0.0 dev: true + /state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + dev: false + /statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} From dbd5b73b4721f34f1cc76cb6504b8b0ced3dd177 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Sun, 2 Feb 2025 23:03:12 -0600 Subject: [PATCH 4/9] refactor: rename file_reader.ts to file-reader.ts for consistency refactor: update import paths for FileReader to use hyphenated naming convention --- frontend/src/app/(main)/layout.tsx | 2 +- frontend/src/app/api/file/route.ts | 2 +- frontend/src/app/api/project/route.ts | 2 +- frontend/src/utils/{file_reader.ts => file-reader.ts} | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) rename frontend/src/utils/{file_reader.ts => file-reader.ts} (98%) diff --git a/frontend/src/app/(main)/layout.tsx b/frontend/src/app/(main)/layout.tsx index 18c2253f..21e6582d 100644 --- a/frontend/src/app/(main)/layout.tsx +++ b/frontend/src/app/(main)/layout.tsx @@ -4,7 +4,7 @@ import MainLayout from './MainLayout'; import { SidebarProvider } from '@/components/ui/sidebar'; import { getProjectPath, getProjectsDir, getRootDir } from 'codefox-common'; -import { FileReader } from '@/utils/file_reader'; +import { FileReader } from '@/utils/file-reader'; const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { diff --git a/frontend/src/app/api/file/route.ts b/frontend/src/app/api/file/route.ts index f646a21b..9247b5d9 100644 --- a/frontend/src/app/api/file/route.ts +++ b/frontend/src/app/api/file/route.ts @@ -1,6 +1,6 @@ // app/api/file/route.ts import { NextResponse } from 'next/server'; -import { FileReader } from '@/utils/file_reader'; +import { FileReader } from '@/utils/file-reader'; import { promises as fs } from 'fs'; import path from 'path'; diff --git a/frontend/src/app/api/project/route.ts b/frontend/src/app/api/project/route.ts index fc5b1118..c6b51774 100644 --- a/frontend/src/app/api/project/route.ts +++ b/frontend/src/app/api/project/route.ts @@ -1,6 +1,6 @@ // app/api/project/route.ts import { NextResponse } from 'next/server'; -import { FileReader } from '@/utils/file_reader'; +import { FileReader } from '@/utils/file-reader'; export async function GET(req: Request) { const { searchParams } = new URL(req.url); diff --git a/frontend/src/utils/file_reader.ts b/frontend/src/utils/file-reader.ts similarity index 98% rename from frontend/src/utils/file_reader.ts rename to frontend/src/utils/file-reader.ts index f8299635..7403fe63 100644 --- a/frontend/src/utils/file_reader.ts +++ b/frontend/src/utils/file-reader.ts @@ -13,6 +13,7 @@ export class FileReader { baseDir = baseDir.replace(/\/\.next[^\/]*/, ''); } + // TODO: don't use / directly if (baseDir.includes('/frontend/server/.codefox/')) { baseDir = baseDir.replace('/frontend/server/.codefox/', '/.codefox/'); } From eb36713b59cf6ca10ca814ff1a5033bfdc5f89fc Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Mon, 3 Feb 2025 00:54:57 -0600 Subject: [PATCH 5/9] feat: refactor project context and update file structure components --- frontend/src/app/(main)/Home.tsx | 135 ++++++++ frontend/src/app/(main)/MainLayout.tsx | 118 ++----- frontend/src/app/(main)/page.tsx | 110 +------ frontend/src/app/context/projectContext.tsx | 37 --- .../src/components/chat/chat-bottombar.tsx | 2 +- frontend/src/components/chat/chat.tsx | 2 +- frontend/src/components/code-display.tsx | 193 ------------ .../components/code-engine/code-engine.tsx | 295 ++++++++++++++++++ .../code-engine/file-explorer-button.tsx | 56 ++++ .../{ => code-engine}/file-structure.tsx | 32 +- .../components/code-engine/project-context.ts | 15 + .../src/components/edit-username-form.tsx | 2 +- frontend/src/components/sidebar.tsx | 57 ++-- 13 files changed, 583 insertions(+), 471 deletions(-) create mode 100644 frontend/src/app/(main)/Home.tsx delete mode 100644 frontend/src/app/context/projectContext.tsx delete mode 100644 frontend/src/components/code-display.tsx create mode 100644 frontend/src/components/code-engine/code-engine.tsx create mode 100644 frontend/src/components/code-engine/file-explorer-button.tsx rename frontend/src/components/{ => code-engine}/file-structure.tsx (72%) create mode 100644 frontend/src/components/code-engine/project-context.ts diff --git a/frontend/src/app/(main)/Home.tsx b/frontend/src/app/(main)/Home.tsx new file mode 100644 index 00000000..29456a6f --- /dev/null +++ b/frontend/src/app/(main)/Home.tsx @@ -0,0 +1,135 @@ +// app/page.tsx 或 components/Home.tsx +'use client'; +import React, { + createContext, + useEffect, + useState, + useRef, + useCallback, +} from 'react'; +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from '@/components/ui/resizable'; +import { CodeEngine } from '@/components/code-engine/code-engine'; +import { GET_CHAT_HISTORY } from '@/graphql/request'; +import { useQuery } from '@apollo/client'; +import { toast } from 'sonner'; +import { EventEnum } from '@/components/enum'; +import { useModels } from '../hooks/useModels'; +import { useChatList } from '../hooks/useChatList'; +import { useChatStream } from '../hooks/useChatStream'; +import EditUsernameForm from '@/components/edit-username-form'; +import ChatContent from '@/components/chat/chat'; +import { ProjectContext } from '@/components/code-engine/project-context'; + +export default function Home() { + const urlParams = new URLSearchParams(window.location.search); + const [chatId, setChatId] = useState(''); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const formRef = useRef(null); + + const { models } = useModels(); + const [selectedModel, setSelectedModel] = useState(models[0] || 'gpt-4o'); + const { refetchChats } = useChatList(); + + useEffect(() => { + setChatId(urlParams.get('id') || ''); + refetchChats(); + }, [urlParams, refetchChats]); + + useQuery(GET_CHAT_HISTORY, { + variables: { chatId }, + onCompleted: (data) => { + if (data?.getChatHistory) { + setMessages(data.getChatHistory); + } + }, + onError: () => { + toast.error('Failed to load chat history'); + }, + }); + + const cleanChatId = () => setChatId(''); + const updateChatId = useCallback(() => { + const params = new URLSearchParams(window.location.search); + setChatId(params.get('id') || ''); + refetchChats(); + }, [refetchChats]); + const updateSetting = () => setChatId(EventEnum.SETTING); + + useEffect(() => { + window.addEventListener(EventEnum.CHAT, updateChatId); + window.addEventListener(EventEnum.NEW_CHAT, cleanChatId); + window.addEventListener(EventEnum.SETTING, updateSetting); + return () => { + window.removeEventListener(EventEnum.CHAT, updateChatId); + window.removeEventListener(EventEnum.NEW_CHAT, cleanChatId); + window.removeEventListener(EventEnum.SETTING, updateSetting); + }; + }, [updateChatId]); + + const { loadingSubmit, handleSubmit, handleInputChange, stop } = + useChatStream({ + chatId, + input, + setInput, + setMessages, + selectedModel, + }); + + const [projectId, setProjectId] = useState( + '2025-02-02-dfca4698-6e9b-4aab-9fcb-98e9526e5f21' + ); + const [filePath, setFilePath] = useState('frontend/vite.config.ts'); + + if (chatId === EventEnum.SETTING) { + return ( +
+ +
+ ); + } + + return ( + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/app/(main)/MainLayout.tsx b/frontend/src/app/(main)/MainLayout.tsx index b4cd69e5..36ded8e1 100644 --- a/frontend/src/app/(main)/MainLayout.tsx +++ b/frontend/src/app/(main)/MainLayout.tsx @@ -1,20 +1,15 @@ +// components/MainLayout.tsx 'use client'; - import React, { useEffect, useState } from 'react'; import { cn } from '@/lib/utils'; -import { useChatList } from '../hooks/useChatList'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from '@/components/ui/resizable'; -import ChatSiderBar from '@/components/sidebar'; -import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; -import { CodeDisplayer } from '@/components/code-display'; -import { Project } from '@/graphql/type'; -import FileStructure from '@/components/file-structure'; -import { ProjectProvider } from '../context/projectContext'; -import FileSidebar from '@/components/file-structure'; +import { SidebarProvider } from '@/components/ui/sidebar'; +import { ChatSideBar } from '@/components/sidebar'; +import { useChatList } from '../hooks/useChatList'; export default function MainLayout({ children, @@ -23,9 +18,8 @@ export default function MainLayout({ }) { const [isCollapsed, setIsCollapsed] = useState(false); const [isMobile, setIsMobile] = useState(false); - const projectId = '2025-01-31-f9b3465a-1bd0-4a56-b042-46864953d870'; - const defaultLayout = [30, 160]; - const navCollapsedSize = 10; + const defaultLayout = [25, 75]; // [sidebar, main] + const navCollapsedSize = 5; const { chats, loading, @@ -39,103 +33,57 @@ export default function MainLayout({ document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(isCollapsed)}; path=/; max-age=604800`; }, [isCollapsed]); - useEffect(() => { - async function fetchFiles() { - const response = await fetch(`/api/project?id=${projectId}`); - const data = await response.json(); - if (data.files) { - setProjectFiles(data.files); - } - } - - fetchFiles(); - }, [projectId]); - useEffect(() => { const checkScreenWidth = () => { setIsMobile(window.innerWidth <= 1023); }; checkScreenWidth(); window.addEventListener('resize', checkScreenWidth); - return () => { - window.removeEventListener('resize', checkScreenWidth); - }; + return () => window.removeEventListener('resize', checkScreenWidth); }, []); - console.log(`${isCollapsed}, ${isMobile}`); - return (
{ - document.cookie = `react-resizable-panels:layout=${JSON.stringify( - sizes - )}; path=/; max-age=604800`; + const sidebarSize = sizes[0]; + const isNowCollapsed = sidebarSize < 10; + setIsCollapsed(isNowCollapsed); + + if (isNowCollapsed && sizes.length > 1) { + const newSizes = [navCollapsedSize, 100 - navCollapsedSize]; + document.cookie = `react-resizable-panels:layout=${JSON.stringify(newSizes)}; path=/; max-age=604800`; + return newSizes; + } + + document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}; path=/; max-age=604800`; + return sizes; }} - className="h-screen items-stretch" + className="h-screen items-stretch w-full" > - { - console.log(`setting collapse to T`); - // setIsCollapsed(true); - }} - onExpand={() => { - console.log(`setting collapse to F`); - // setIsCollapsed(false); - }} - className={cn( - 'transition-all duration-300 ease-in-out', - isCollapsed ? 'min-w-[50px] md:min-w-[70px]' : 'md:min-w-[20%]' - )} - > - {loading ? ( -
Loading...
- ) : error ? ( -
- Error: {error.message} -
- ) : ( - - )} -
+ - - {children} - - - - - + {children}
); } -function setProjectFiles(files: any) { - throw new Error('Function not implemented.'); -} diff --git a/frontend/src/app/(main)/page.tsx b/frontend/src/app/(main)/page.tsx index 09708d9d..1ca9146d 100644 --- a/frontend/src/app/(main)/page.tsx +++ b/frontend/src/app/(main)/page.tsx @@ -1,109 +1,5 @@ -'use client'; +import Home from './Home'; -import { - SetStateAction, - useCallback, - useEffect, - useRef, - useState, -} from 'react'; -import { Message } from '@/components/types'; -import { useModels } from '../hooks/useModels'; -import ChatContent from '@/components/chat/chat'; -import { useChatStream } from '../hooks/useChatStream'; -import { GET_CHAT_HISTORY } from '@/graphql/request'; -import { useQuery } from '@apollo/client'; -import { toast } from 'sonner'; -import { useChatList } from '../hooks/useChatList'; -import { EventEnum } from '@/components/enum'; -import DetailSettings from '@/components/detail-settings'; -import EditUsernameForm from '@/components/edit-username-form'; - -export default function Home() { - let urlParams = new URLSearchParams(window.location.search); - const [chatId, setChatId] = useState(''); - // Core message states - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(''); - const formRef = useRef(null); - - const { models } = useModels(); - const [selectedModel, setSelectedModel] = useState( - models[0] || 'gpt-4o' - ); - - const { refetchChats } = useChatList(); - - useEffect(() => { - setChatId(urlParams.get('id') || ''); - refetchChats(); - console.log(`update ${urlParams.get('id')}`); - }, [urlParams]); - - useQuery(GET_CHAT_HISTORY, { - variables: { chatId }, - onCompleted: (data) => { - if (data?.getChatHistory) { - setMessages(data.getChatHistory); - } - }, - onError: (error) => { - toast.error('Failed to load chat history'); - }, - }); - - const cleanChatId = () => { - setChatId(''); - }; - const updateChatId = useCallback(() => { - urlParams = new URLSearchParams(window.location.search); - setChatId(urlParams.get('id') || ''); - refetchChats(); - }, []); - const updateSetting = () => { - setChatId(EventEnum.SETTING); - }; - - useEffect(() => { - window.addEventListener(EventEnum.CHAT, updateChatId); - window.addEventListener(EventEnum.NEW_CHAT, cleanChatId); - window.addEventListener(EventEnum.SETTING, updateSetting); - - return () => { - window.removeEventListener(EventEnum.CHAT, updateChatId); - window.removeEventListener(EventEnum.NEW_CHAT, cleanChatId); - window.removeEventListener(EventEnum.SETTING, updateSetting); - }; - }, [updateChatId]); - - const { loadingSubmit, handleSubmit, handleInputChange, stop } = - useChatStream({ - chatId, - input, - setInput, - setMessages, - selectedModel, - }); - - return ( - <> - {chatId === EventEnum.SETTING.toString() ? ( - - ) : ( - - )} - - ); +export default function Page() { + return ; } diff --git a/frontend/src/app/context/projectContext.tsx b/frontend/src/app/context/projectContext.tsx deleted file mode 100644 index 5c4108ad..00000000 --- a/frontend/src/app/context/projectContext.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; -import { - createContext, - Dispatch, - SetStateAction, - useContext, - useState, -} from 'react'; -interface ProjectContextType { - projectId: string; - setProjectId: Dispatch>; - filePath: string | null; - setFilePath: Dispatch>; -} - -const ProjectContext = createContext({ - projectId: '', - setProjectId: () => {}, - filePath: null, - setFilePath: () => {}, -}); - -export const useProject = () => useContext(ProjectContext); -export const ProjectProvider = ({ children }) => { - const [projectId, setProjectId] = useState( - '2025-01-31-f9b3465a-1bd0-4a56-b042-46864953d870' - ); - const [filePath, setFilePath] = useState('frontend/vite.config.ts'); - - return ( - - {children} - - ); -}; diff --git a/frontend/src/components/chat/chat-bottombar.tsx b/frontend/src/components/chat/chat-bottombar.tsx index 829d2949..70852fa5 100644 --- a/frontend/src/components/chat/chat-bottombar.tsx +++ b/frontend/src/components/chat/chat-bottombar.tsx @@ -84,7 +84,7 @@ export default function ChatBottombar({ }, []); return ( -
+
diff --git a/frontend/src/components/chat/chat.tsx b/frontend/src/components/chat/chat.tsx index f7486870..ec703de7 100644 --- a/frontend/src/components/chat/chat.tsx +++ b/frontend/src/components/chat/chat.tsx @@ -52,7 +52,7 @@ export default function ChatContent({ // setEditContent(''); // }; return ( -
+
diff --git a/frontend/src/components/code-display.tsx b/frontend/src/components/code-display.tsx deleted file mode 100644 index 703d04c2..00000000 --- a/frontend/src/components/code-display.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { useProject } from '@/app/context/projectContext'; -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { useRef } from 'react'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import Editor from '@monaco-editor/react'; -import { useEffect, useState } from 'react'; -import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; -import FileStructure from './file-structure'; -import { motion } from 'framer-motion'; -export function CodeDisplayer() { - const editorRef = useRef(null); - - const { projectId, filePath } = useProject(); - const [preCode, setPrecode] = useState('// some coaamment'); - const [newCode, setCode] = useState('// some coaamment'); - const [saving, setSaving] = useState(false); - const [type, setType] = useState('javascript'); - const [isLoading, setIsLoading] = useState(false); - const [editor, setEditor] = useState(null); - const [isCollapsed, setIsCollapsed] = useState(false); // 默认折叠状态 - const handleEditorMount = (editor) => { - editorRef.current = editor; - - editor.getDomNode().style.position = 'absolute'; - }; - useEffect(() => { - async function getCode() { - try { - setIsLoading(true); - const res = await fetch( - `/api/file?path=${encodeURIComponent(`${projectId}/${filePath}`)}` - ).then((res) => res.json()); - console.log(res.content); - setCode(res.content); - setPrecode(res.content); - setType(res.type); - - setIsLoading(false); - } catch (error) { - console.error(error.message); - } - } - - getCode(); - }, [filePath, projectId]); - - const handleReset = () => { - console.log('Reset!'); - setCode(preCode); - console.log(editorRef.current?.getValue()); - editorRef.current?.setValue(preCode); - console.log(preCode); - console.log(editorRef.current?.getValue()); - setSaving(false); - }; - - const updateCode = async (value) => { - try { - const response = await fetch('/api/file', { - method: 'POST', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - filePath: projectId + '/' + filePath, - newContent: JSON.stringify(value), - }), - }); - - const data = await response.json(); - } catch (error) { - /* empty */ - } - }; - - const handleSave = () => { - console.log('Saved!'); - setSaving(false); - setPrecode(newCode); - updateCode(newCode); - }; - - const updateSavingStatus = async (value, event) => { - setCode(value); - setSaving(true); - }; - return ( - - - Code - Password - - - - - - - - - - - {saving && ( - - )} - - - - - - Password - - Change your password here. After saving, you'll be logged out. - - - -
- - -
-
- - -
-
- - - -
-
-
- ); -} - -const SaveChangesBar = ({ saving, onSave, onReset }) => { - return ( - saving && ( -
- - Unsaved Changes - - -
- ) - ); -}; diff --git a/frontend/src/components/code-engine/code-engine.tsx b/frontend/src/components/code-engine/code-engine.tsx new file mode 100644 index 00000000..49a0d711 --- /dev/null +++ b/frontend/src/components/code-engine/code-engine.tsx @@ -0,0 +1,295 @@ +'use client'; + +import { useContext, useRef, useEffect, useState } from 'react'; +import Editor from '@monaco-editor/react'; +import { motion } from 'framer-motion'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; +import FileStructure, { FileNodeType } from './file-structure'; +import { ProjectContext } from './project-context'; + +// Import icon components for header tabs and explorer toggle +import { + Eye, + Code as CodeIcon, + Terminal, + GitFork, + Share2, + Copy, + ChevronLeft, + ChevronRight, +} from 'lucide-react'; +import FileExplorerButton from './file-explorer-button'; +import { useTheme } from 'next-themes'; + +export function CodeEngine() { + const editorRef = useRef(null); + const { projectId, filePath } = useContext(ProjectContext); + const [preCode, setPrecode] = useState('// some comment'); + const [newCode, setCode] = useState('// some comment'); + const [saving, setSaving] = useState(false); + const [type, setType] = useState('javascript'); + const [isLoading, setIsLoading] = useState(false); + const [isExplorerCollapsed, setIsExplorerCollapsed] = useState(false); + const [fileStructureData, setFileStructureData] = useState( + [] + ); + const theme = useTheme(); + const [activeTab, setActiveTab] = useState<'preview' | 'code' | 'console'>( + 'code' + ); + + // Handle mounting of the editor. + const handleEditorMount = (editorInstance) => { + editorRef.current = editorInstance; + // Set the editor DOM node's position for layout control. + editorInstance.getDomNode().style.position = 'absolute'; + }; + + // Fetch file content when filePath or projectId changes. + useEffect(() => { + async function getCode() { + try { + setIsLoading(true); + const res = await fetch( + `/api/file?path=${encodeURIComponent(`${projectId}/${filePath}`)}` + ).then((res) => res.json()); + setCode(res.content); + setPrecode(res.content); + setType(res.type); + setIsLoading(false); + } catch (error: any) { + console.error(error.message); + } + } + getCode(); + }, [filePath, projectId]); + + // Fetch file structure when projectId changes. + useEffect(() => { + async function fetchFiles() { + try { + const response = await fetch(`/api/project?id=${projectId}`); + const data = await response.json(); + setFileStructureData(data.res || []); + } catch (error) { + console.error('Error fetching file structure:', error); + } + } + fetchFiles(); + }, [projectId]); + + const handleReset = () => { + setCode(preCode); + editorRef.current?.setValue(preCode); + setSaving(false); + }; + + const updateCode = async (value) => { + try { + const response = await fetch('/api/file', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + filePath: `${projectId}/${filePath}`, + newContent: JSON.stringify(value), + }), + }); + await response.json(); + } catch (error) { + console.error(error); + } + }; + + const handleSave = () => { + setSaving(false); + setPrecode(newCode); + updateCode(newCode); + }; + + const updateSavingStatus = (value) => { + setCode(value); + setSaving(true); + }; + + return ( +
+ {/* Header Bar */} +
+ {/* Left Section: Tab triggers and explorer toggle */} +
+ + + + {/* {activeTab === 'code' && ( + + )} */} +
+ {/* Right Section: Icon buttons and text buttons */} +
+ {/* Icon Buttons */} +
+ + + +
+ {/* Text Buttons */} +
+ + + +
+
+
+ + {/* Content Area */} +
+ {activeTab === 'code' ? ( + <> + {/* File Explorer Panel (collapsible) */} + + + +
+ +
+ + ) : activeTab === 'preview' ? ( +
Preview Content (Mock)
+ ) : activeTab === 'console' ? ( +
Console Content (Mock)
+ ) : null} +
+ + {/* Save Changes Bar */} + {saving && ( + + )} + + {/* close explored bar */} + {activeTab === 'code' && ( + + )} +
+ ); +} + +const SaveChangesBar = ({ saving, onSave, onReset }) => { + return ( + saving && ( +
+ + Unsaved Changes + + +
+ ) + ); +}; diff --git a/frontend/src/components/code-engine/file-explorer-button.tsx b/frontend/src/components/code-engine/file-explorer-button.tsx new file mode 100644 index 00000000..12cc4b04 --- /dev/null +++ b/frontend/src/components/code-engine/file-explorer-button.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { Button } from '@/components/ui/button'; + +const FileExplorerButton = ({ + isExplorerCollapsed, + setIsExplorerCollapsed, +}: { + isExplorerCollapsed: boolean; + setIsExplorerCollapsed: (value: boolean) => void; +}) => { + return ( +
+ + + + + + +

{isExplorerCollapsed ? 'Open File Tree' : 'Close File Tree'}

+
+
+
+
+ ); +}; + +export default FileExplorerButton; diff --git a/frontend/src/components/file-structure.tsx b/frontend/src/components/code-engine/file-structure.tsx similarity index 72% rename from frontend/src/components/file-structure.tsx rename to frontend/src/components/code-engine/file-structure.tsx index d35c24a9..73d0af0b 100644 --- a/frontend/src/components/file-structure.tsx +++ b/frontend/src/components/code-engine/file-structure.tsx @@ -1,14 +1,14 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { FileIcon, ChevronDownIcon, ChevronRightIcon, } from '@radix-ui/react-icons'; -import { useProject } from '@/app/context/projectContext'; +import { ProjectContext } from './project-context'; -interface FileNodeType { +export interface FileNodeType { name: string; type: 'file' | 'folder'; children?: FileNodeType[]; @@ -22,7 +22,7 @@ const FileNode = ({ fullPath: string; }) => { const [isOpen, setIsOpen] = useState(false); - const { setFilePath, filePath } = useProject(); + const { setFilePath, filePath } = useContext(ProjectContext); const toggleOpen = () => { if (node.type === 'folder') setIsOpen(!isOpen); @@ -74,33 +74,19 @@ const FileNode = ({ export default function FileStructure({ isCollapsed, + filePath, + data, }: { + filePath: string; isCollapsed: boolean; + data: FileNodeType[]; }) { - const { projectId, filePath } = useProject(); - const [fileStructure, setStructure] = useState([]); - - useEffect(() => { - async function fetchFiles() { - try { - const response = await fetch(`/api/project?id=${projectId}`); - const data = await response.json(); - console.log('Fetched file structure:', data.res); - setStructure(data.res || []); - } catch (error) { - console.error('Error fetching file structure:', error); - } - } - - fetchFiles(); - }, [projectId]); - return (

File Explorer

{filePath &&
{filePath}
} - {fileStructure.map((node) => ( + {data.map((node) => ( ))}
diff --git a/frontend/src/components/code-engine/project-context.ts b/frontend/src/components/code-engine/project-context.ts new file mode 100644 index 00000000..5b8891d7 --- /dev/null +++ b/frontend/src/components/code-engine/project-context.ts @@ -0,0 +1,15 @@ +import { createContext } from 'react'; + +export interface ProjectContextType { + projectId: string; + setProjectId: React.Dispatch>; + filePath: string | null; + setFilePath: React.Dispatch>; +} + +export const ProjectContext = createContext({ + projectId: '', + setProjectId: () => {}, + filePath: null, + setFilePath: () => {}, +}); diff --git a/frontend/src/components/edit-username-form.tsx b/frontend/src/components/edit-username-form.tsx index 03b5cfc0..128a06dd 100644 --- a/frontend/src/components/edit-username-form.tsx +++ b/frontend/src/components/edit-username-form.tsx @@ -85,7 +85,7 @@ export default function EditUsernameForm() { setName(e.currentTarget.value); }; return ( -
+

User Settings

diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 3de6f94e..76d23f22 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -1,8 +1,7 @@ 'use client'; import { Button } from '@/components/ui/button'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -import { memo, useCallback, useEffect, useState } from 'react'; +import { memo, useCallback, useState } from 'react'; import { SquarePen } from 'lucide-react'; import SidebarSkeleton from './sidebar-skeleton'; import UserSettings from './user-settings'; @@ -12,7 +11,6 @@ import { EventEnum } from './enum'; import { SidebarContent, SidebarGroup, - SidebarGroupLabel, SidebarGroupContent, SidebarTrigger, Sidebar, @@ -23,6 +21,7 @@ import { cn } from '@/lib/utils'; interface SidebarProps { isCollapsed: boolean; + setIsCollapsed: (value: boolean) => void; // Parent setter to update collapse state isMobile: boolean; currentChatId?: string; chatListUpdated: boolean; @@ -33,8 +32,9 @@ interface SidebarProps { onRefetch: () => void; } -function ChatsSiderBar({ +export function ChatSideBar({ isCollapsed, + setIsCollapsed, isMobile, chatListUpdated, setChatListUpdated, @@ -43,8 +43,10 @@ function ChatsSiderBar({ error, onRefetch, }: SidebarProps) { - const [isSimple, setIsSimple] = useState(false); + // Use a local state only for the currently selected chat. const [currentChatid, setCurrentChatid] = useState(''); + + // Handler for starting a new chat. const handleNewChat = useCallback(() => { window.history.replaceState({}, '', '/'); setCurrentChatid(''); @@ -57,7 +59,13 @@ function ChatsSiderBar({ console.error('Error loading chats:', error); return null; } - console.log(`${isCollapsed}, ${isMobile}, ${isSimple}`); + + console.log( + 'ChatSideBar state: isCollapsed:', + isCollapsed, + 'currentChatid:', + currentChatid + ); return (
+ {/* Toggle button: Clicking this will toggle the collapse state */} setIsSimple(!isSimple)} - > + className="lg:flex items-center justify-center cursor-pointer p-2 ml-3.5 mt-2" + onClick={() => setIsCollapsed(!isCollapsed)} + /> + {loading ? 'Loading...' - : !isSimple && + : !isCollapsed && chats.map((chat) => ( + - + - + setIsCollapsed(!isCollapsed)} + isSimple={false} + />
); } -export default memo(ChatsSiderBar, (prevProps, nextProps) => { +export default memo(ChatSideBar, (prevProps, nextProps) => { return ( prevProps.isCollapsed === nextProps.isCollapsed && prevProps.isMobile === nextProps.isMobile && From 5c8f3c85359d405bc53a6ee3314f3699a55c8347 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Mon, 3 Feb 2025 10:40:25 -0600 Subject: [PATCH 6/9] fix: add border to FileExplorerButton for improved visibility --- frontend/src/components/code-engine/file-explorer-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/code-engine/file-explorer-button.tsx b/frontend/src/components/code-engine/file-explorer-button.tsx index 12cc4b04..7c6c6a4a 100644 --- a/frontend/src/components/code-engine/file-explorer-button.tsx +++ b/frontend/src/components/code-engine/file-explorer-button.tsx @@ -22,7 +22,7 @@ const FileExplorerButton = ({