Skip to content

Commit

Permalink
feat(frontend): Feat frontend code review (#112)
Browse files Browse the repository at this point in the history
adding code review with monaco editior and file structure tree

https://jam.dev/c/3df072a0-b9db-497a-b251-b167636332b8

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Launched an interactive chat interface and a modern code editor
integrated with a file explorer and hierarchical file viewer.
- Introduced new endpoints that support seamless file content updates
and project file retrieval.
- Added new components for managing project-related state and file
structure visualization.
- Implemented a new tabbed interface for better organization of content.
  - Introduced a sidebar interface for managing chat functionalities.
  
- **UI/UX Enhancements**
- Redesigned the sidebar and tabs for improved responsiveness and easier
interaction.
- Refined spacing in key forms for a cleaner and more accessible layout.
  - Enhanced the layout and functionality of the chat sidebar.

- **Chores**
- Updated configuration settings with new module aliases and added
essential dependencies to enhance overall functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Nahuel Chen <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Sma1lboy <[email protected]>
  • Loading branch information
4 people authored Feb 5, 2025
1 parent 498f337 commit 00f2df6
Show file tree
Hide file tree
Showing 21 changed files with 1,263 additions and 192 deletions.
3 changes: 2 additions & 1 deletion codefox-common/src/common-path.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as path from 'path';

import fsExtra from 'fs-extra';
import { cwd } from 'process';
const { existsSync, mkdirSync, promises, writeFileSync } = fsExtra;

// Constants for base directories
const APP_NAME = 'codefox';
// TODO: hack way to get the root directory of the workspace

const WORKSPACE_ROOT = path.resolve(path.join(__dirname, '..', '..', '..'));
const WORKSPACE_ROOT = path.resolve(cwd(), '..');

const ROOT_DIR = path.join(WORKSPACE_ROOT, `.${APP_NAME}`);

Expand Down
5 changes: 4 additions & 1 deletion frontend/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
4 changes: 4 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@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",
Expand All @@ -32,10 +33,12 @@
"@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",
"clsx": "^2.1.1",
"codefox-common": "workspace:*",
"emoji-mart": "^5.6.0",
"framer-motion": "^11.5.6",
"graphql": "^16.9.0",
Expand All @@ -46,6 +49,7 @@
"react": "^18.3.1",
"react-activity-calendar": "^2.7.8",
"react-code-blocks": "^0.1.6",
"react-complex-tree": "^2.4.6",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.9",
"react-hook-form": "^7.53.0",
Expand Down
147 changes: 147 additions & 0 deletions frontend/src/app/(main)/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// app/page.tsx or components/Home.tsx
'use client';
import React, { 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() {
// Initialize state, refs, and custom hooks
const urlParams = new URLSearchParams(window.location.search);
const [chatId, setChatId] = useState('');
const [messages, setMessages] = useState<any[]>([]);
const [input, setInput] = useState('');
const formRef = useRef<HTMLFormElement>(null);

const { models } = useModels();
const [selectedModel, setSelectedModel] = useState(models[0] || 'gpt-4o');

const { refetchChats } = useChatList();

//TODO: adding project id from .codefox/projects
const [projectId, setProjectId] = useState(
'2025-02-02-dfca4698-6e9b-4aab-9fcb-98e9526e5f21'
);
const [filePath, setFilePath] = useState('frontend/vite.config.ts');

// Apollo query to fetch chat history
useQuery(GET_CHAT_HISTORY, {
variables: { chatId },
onCompleted: (data) => {
if (data?.getChatHistory) {
setMessages(data.getChatHistory);
}
},
onError: () => {
toast.error('Failed to load chat history');
},
});

// Custom hook for handling chat streaming
const { loadingSubmit, handleSubmit, handleInputChange, stop } =
useChatStream({
chatId,
input,
setInput,
setMessages,
selectedModel,
});

// Callback to clear the chat ID
const cleanChatId = () => setChatId('');

// Callback to update chat ID based on URL parameters and refresh the chat list
const updateChatId = useCallback(() => {
const params = new URLSearchParams(window.location.search);
setChatId(params.get('id') || '');
refetchChats();
}, [refetchChats]);

// Callback to switch to the settings view
const updateSetting = () => setChatId(EventEnum.SETTING);

// Effect to initialize chat ID and refresh the chat list based on URL parameters
useEffect(() => {
setChatId(urlParams.get('id') || '');
refetchChats();
}, [urlParams, refetchChats]);

// Effect to add and remove global event listeners
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]);

// Render the settings view if chatId indicates settings mode
if (chatId === EventEnum.SETTING) {
return (
<div className="h-full w-full flex items-center justify-center">
<EditUsernameForm />
</div>
);
}

// Render the main layout
return (
<ResizablePanelGroup direction="horizontal" className="h-full w-full">
<ResizablePanel
defaultSize={50}
minSize={20}
maxSize={80}
className="h-full"
>
<ChatContent
chatId={chatId}
setSelectedModel={setSelectedModel}
messages={messages}
input={input}
handleInputChange={handleInputChange}
handleSubmit={handleSubmit}
loadingSubmit={loadingSubmit}
stop={stop}
formRef={formRef}
setInput={setInput}
setMessages={setMessages}
/>
</ResizablePanel>

<ResizableHandle withHandle className="hidden md:flex" />

{projectId ? (
<ResizablePanel
defaultSize={50}
minSize={20}
maxSize={80}
className="h-full overflow-auto"
>
<ProjectContext.Provider
value={{ projectId, setProjectId, filePath, setFilePath }}
>
<CodeEngine />
</ProjectContext.Provider>
</ResizablePanel>
) : (
<h1>Forgot to input project id</h1>
)}
</ResizablePanelGroup>
);
}
90 changes: 36 additions & 54 deletions frontend/src/app/(main)/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// components/MainLayout.tsx
'use client';

import React, { useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
import { usePathname } from 'next/navigation';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable';
import { SidebarProvider } from '@/components/ui/sidebar';
import { ChatSideBar } from '@/components/sidebar';
import { useChatList } from '../hooks/useChatList';
import { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import CustomSidebar from '@/components/sidebar';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';

export default function MainLayout({
children,
Expand All @@ -15,8 +18,8 @@ export default function MainLayout({
}) {
const [isCollapsed, setIsCollapsed] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const defaultLayout = [30, 160];
const navCollapsedSize = 10;
const defaultLayout = [25, 75]; // [sidebar, main]
const navCollapsedSize = 5;
const {
chats,
loading,
Expand All @@ -36,63 +39,42 @@ export default function MainLayout({
};
checkScreenWidth();
window.addEventListener('resize', checkScreenWidth);
return () => {
window.removeEventListener('resize', checkScreenWidth);
};
return () => window.removeEventListener('resize', checkScreenWidth);
}, []);

console.log(`${isCollapsed}, ${isMobile}`);

return (
<main className="flex h-[calc(100dvh)] flex-col items-center">
<ResizablePanelGroup
direction="horizontal"
autoSaveId="main-layout"
onLayout={(sizes: number[]) => {
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"
>
<SidebarProvider>
<ResizablePanel
defaultSize={defaultLayout[0]}
collapsedSize={navCollapsedSize}
collapsible={true}
minSize={isMobile ? 4 : 12}
maxSize={isMobile ? 10 : 16}
onCollapse={() => {
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-[200px]'
)}
>
{loading ? (
<div className="flex justify-center items-center">Loading...</div>
) : error ? (
<div className="flex justify-center items-center text-red-500">
Error: {error.message}
</div>
) : (
<CustomSidebar
isCollapsed={isCollapsed}
isMobile={isMobile}
chatListUpdated={chatListUpdated}
setChatListUpdated={setChatListUpdated}
chats={chats}
loading={loading}
error={error}
onRefetch={refetchChats}
/>
)}
</ResizablePanel>
<ChatSideBar
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
isMobile={isMobile}
chatListUpdated={chatListUpdated}
setChatListUpdated={setChatListUpdated}
chats={chats}
loading={loading}
error={error}
onRefetch={refetchChats}
/>

<ResizablePanel
className="h-full w-full flex justify-center"
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 <MainLayout>{children}</MainLayout>;
}
Loading

0 comments on commit 00f2df6

Please sign in to comment.