From 763e534550a25f7829777bd9208305591797bdeb Mon Sep 17 00:00:00 2001 From: pengyu Date: Sun, 23 Mar 2025 16:37:54 -0400 Subject: [PATCH 01/19] finish toast part remove let chatbar handle polling --- .../chat/code-engine/code-engine.tsx | 8 ++++ .../chat/code-engine/project-context.tsx | 39 ++++++++++++++++++- frontend/src/components/sidebar.tsx | 9 ++--- frontend/src/providers/BaseProvider.tsx | 8 ++-- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/chat/code-engine/code-engine.tsx b/frontend/src/components/chat/code-engine/code-engine.tsx index 71de736d..1155c2bb 100644 --- a/frontend/src/components/chat/code-engine/code-engine.tsx +++ b/frontend/src/components/chat/code-engine/code-engine.tsx @@ -46,7 +46,15 @@ export function CodeEngine({ const [isCompleting, setIsCompleting] = useState(false); // 添加一个ref来持久跟踪项目状态,避免重新渲染时丢失 const isProjectLoadedRef = useRef(false); + const context = useContext(ProjectContext); + if (!context) throw new Error('Must be used inside ProjectProvider'); + const { setRecentlyCompletedProjectId } = context; + useEffect(() => { + if (projectCompleted) { + setRecentlyCompletedProjectId(curProject?.id || localProject?.id); + } + }, [projectCompleted]); // 在组件挂载时从localStorage检查项目是否已完成 useEffect(() => { try { diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index 213755e2..5e4bc0e6 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -50,6 +50,10 @@ export interface ProjectContextType { takeProjectScreenshot: (projectId: string, url: string) => Promise; refreshProjects: () => Promise; editorRef?: React.MutableRefObject; + recentlyCompletedProjectId: string | null; + setRecentlyCompletedProjectId: (id: string | null) => void; + chatId: string | null; + setChatId: (chatId: string | null) => void; } export const ProjectContext = createContext( @@ -105,7 +109,10 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [filePath, setFilePath] = useState(null); const [isLoading, setIsLoading] = useState(false); const editorRef = useRef(null); - + const [recentlyCompletedProjectId, setRecentlyCompletedProjectId] = useState< + string | null + >(null); + const [chatId, setChatId] = useState(null); interface ChatProjectCacheEntry { project: Project | null; timestamp: number; @@ -142,6 +149,24 @@ export function ProjectProvider({ children }: { children: ReactNode }) { pendingOperations.current.clear(); }; }, []); + // Poll project data every 5 seconds + useEffect(() => { + if (!chatId) return; + let stopped = false; + + const interval = setInterval(async () => { + const project = await pollChatProject(chatId); + if (project?.projectPath) { + setCurProject(project); + clearInterval(interval); + stopped = true; + } + }, 5000); + + return () => { + if (!stopped) clearInterval(interval); + }; + }, [chatId]); // Function to clean expired cache entries const cleanCache = useCallback(() => { @@ -912,6 +937,8 @@ export function ProjectProvider({ children }: { children: ReactNode }) { takeProjectScreenshot, refreshProjects, editorRef, + chatId, + setChatId, }), [ projects, @@ -928,11 +955,19 @@ export function ProjectProvider({ children }: { children: ReactNode }) { takeProjectScreenshot, refreshProjects, editorRef, + chatId, + setChatId, ] ); return ( - + {children} ); diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 620570ea..e3c8244c 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -96,20 +96,17 @@ function ChatSideBarComponent({ }: SidebarProps) { const router = useRouter(); const [currentChatid, setCurrentChatid] = useState(''); - const { setCurProject, pollChatProject } = useContext(ProjectContext); + const { setChatId } = useContext(ProjectContext); const handleChatSelect = useCallback( (chatId: string) => { setCurrentChatid(chatId); router.push(`/chat?id=${chatId}`); - setCurProject(null); - pollChatProject(chatId).then((p) => { - setCurProject(p); - }); + setChatId(chatId); const event = new Event(EventEnum.CHAT); window.dispatchEvent(event); }, - [router, setCurProject, pollChatProject] + [router] ); if (loading) return ; diff --git a/frontend/src/providers/BaseProvider.tsx b/frontend/src/providers/BaseProvider.tsx index b5e928bd..900e0370 100644 --- a/frontend/src/providers/BaseProvider.tsx +++ b/frontend/src/providers/BaseProvider.tsx @@ -4,11 +4,8 @@ import dynamic from 'next/dynamic'; import { ThemeProvider } from 'next-themes'; import { Toaster } from 'sonner'; import { AuthProvider } from './AuthProvider'; -import { - ProjectContext, - ProjectProvider, -} from '@/components/chat/code-engine/project-context'; - +import { ProjectProvider } from '@/components/chat/code-engine/project-context'; +import GlobalToastListener from '@/components/global-toast-listener'; const DynamicApolloProvider = dynamic(() => import('./DynamicApolloProvider'), { ssr: false, // disables SSR for the ApolloProvider }); @@ -23,6 +20,7 @@ export function BaseProviders({ children }: ProvidersProps) { + {children} From b1d14c1b6b437993b23dcf5faeb2b4ac4fc69d36 Mon Sep 17 00:00:00 2001 From: pengyu Date: Tue, 25 Mar 2025 10:16:43 -0400 Subject: [PATCH 02/19] implement toast component --- .../src/components/global-toast-listener.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 frontend/src/components/global-toast-listener.tsx diff --git a/frontend/src/components/global-toast-listener.tsx b/frontend/src/components/global-toast-listener.tsx new file mode 100644 index 00000000..c87aea56 --- /dev/null +++ b/frontend/src/components/global-toast-listener.tsx @@ -0,0 +1,23 @@ +// GlobalToastListener.tsx +'use client'; +import { useContext, useEffect } from 'react'; +import { toast } from 'sonner'; // 或你使用的 toast 库 +import { ProjectContext } from './chat/code-engine/project-context'; + +const GlobalToastListener = () => { + const { recentlyCompletedProjectId, setRecentlyCompletedProjectId } = + useContext(ProjectContext); + + useEffect(() => { + if (recentlyCompletedProjectId) { + toast.success('Project is ready! 🎉'); + + // 可选:重置,避免重复 toast + setRecentlyCompletedProjectId(null); + } + }, [recentlyCompletedProjectId]); + + return null; // 不渲染任何内容,只是监听 +}; + +export default GlobalToastListener; From 384b55211357139e9a2c48b6cd4928cd05b716a7 Mon Sep 17 00:00:00 2001 From: pengyu Date: Wed, 26 Mar 2025 15:19:59 -0400 Subject: [PATCH 03/19] finished notification --- frontend/src/app/(main)/page.tsx | 21 ++++- .../src/components/chat-page-navigation.tsx | 15 +++ .../chat/code-engine/project-context.tsx | 93 ++++++++++++------- .../src/components/global-toast-listener.tsx | 70 +++++++++++--- .../src/components/project-ready-toast.tsx | 57 ++++++++++++ frontend/src/components/root/prompt-form.tsx | 41 ++++++-- frontend/src/components/sidebar-item.tsx | 18 +++- frontend/src/components/sidebar.tsx | 11 +-- 8 files changed, 258 insertions(+), 68 deletions(-) create mode 100644 frontend/src/components/chat-page-navigation.tsx create mode 100644 frontend/src/components/project-ready-toast.tsx diff --git a/frontend/src/app/(main)/page.tsx b/frontend/src/app/(main)/page.tsx index 60f91e01..0578d863 100644 --- a/frontend/src/app/(main)/page.tsx +++ b/frontend/src/app/(main)/page.tsx @@ -13,6 +13,7 @@ import { SignUpModal } from '@/components/sign-up-modal'; import { useRouter } from 'next/navigation'; import { logger } from '../log/logger'; import { AuroraText } from '@/components/magicui/aurora-text'; + export default function HomePage() { // States for AuthChoiceModal const [showAuthChoice, setShowAuthChoice] = useState(false); @@ -22,9 +23,10 @@ export default function HomePage() { const promptFormRef = useRef(null); const { isAuthorized } = useAuthContext(); - const { createProjectFromPrompt, isLoading } = useContext(ProjectContext); + const { createProjectFromPrompt, isLoading, setRecentlyCompletedProjectId } = + useContext(ProjectContext); - const handleSubmit = async () => { + const handleSubmit = async (): Promise => { if (!promptFormRef.current) return; const { message, isPublic, model } = promptFormRef.current.getPromptData(); @@ -34,12 +36,25 @@ export default function HomePage() { const chatId = await createProjectFromPrompt(message, isPublic, model); promptFormRef.current.clearMessage(); - router.push(`/chat?id=${chatId}`); + if (chatId) { + setRecentlyCompletedProjectId(chatId); + return chatId; + } } catch (error) { logger.error('Error creating project:', error); } }; + // useEffect(() => { + // if (!chatId) return; + + // const interval = setInterval(() => { + // pollChatProject(chatId).catch((error) => { + // logger.error('Polling error in HomePage:', error); + // }); + // }, 6000); + // return () => clearInterval(interval); + // }, [chatId, pollChatProject]); return (
diff --git a/frontend/src/components/chat-page-navigation.tsx b/frontend/src/components/chat-page-navigation.tsx new file mode 100644 index 00000000..d718daca --- /dev/null +++ b/frontend/src/components/chat-page-navigation.tsx @@ -0,0 +1,15 @@ +import { EventEnum } from '@/const/EventEnum'; +import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; + +export const redirectChatPage = ( + chatId: string, + setCurrentChatid: (id: string) => void, + setChatId: (id: string) => void, + router: AppRouterInstance +) => { + setCurrentChatid(chatId); + setChatId(chatId); + router.push(`/chat?id=${chatId}`); + const event = new Event(EventEnum.CHAT); + window.dispatchEvent(event); +}; diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index 5e4bc0e6..490a502f 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -36,7 +36,7 @@ export interface ProjectContextType { prompt: string, isPublic: boolean, model?: string - ) => Promise; + ) => Promise; forkProject: (projectId: string) => Promise; setProjectPublicStatus: ( projectId: string, @@ -109,10 +109,25 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [filePath, setFilePath] = useState(null); const [isLoading, setIsLoading] = useState(false); const editorRef = useRef(null); - const [recentlyCompletedProjectId, setRecentlyCompletedProjectId] = useState< - string | null - >(null); + const [recentlyCompletedProjectIdRaw, setRecentlyCompletedProjectIdRaw] = + useState(() => + typeof window !== 'undefined' + ? localStorage.getItem('pendingChatId') + : null + ); + + // setter:更新 state + localStorage + const setRecentlyCompletedProjectId = (id: string | null) => { + if (id) { + localStorage.setItem('pendingChatId', id); + } else { + localStorage.removeItem('pendingChatId'); + } + setRecentlyCompletedProjectIdRaw(id); + }; const [chatId, setChatId] = useState(null); + const [pollTime, setPollTime] = useState(Date.now()); + const [isCreateButtonClicked, setIsCreateButtonClicked] = useState(false); interface ChatProjectCacheEntry { project: Project | null; timestamp: number; @@ -149,24 +164,6 @@ export function ProjectProvider({ children }: { children: ReactNode }) { pendingOperations.current.clear(); }; }, []); - // Poll project data every 5 seconds - useEffect(() => { - if (!chatId) return; - let stopped = false; - - const interval = setInterval(async () => { - const project = await pollChatProject(chatId); - if (project?.projectPath) { - setCurProject(project); - clearInterval(interval); - stopped = true; - } - }, 5000); - - return () => { - if (!stopped) clearInterval(interval); - }; - }, [chatId]); // Function to clean expired cache entries const cleanCache = useCallback(() => { @@ -709,12 +706,12 @@ export function ProjectProvider({ children }: { children: ReactNode }) { prompt: string, isPublic: boolean, model = 'gpt-4o-mini' - ): Promise => { + ): Promise => { if (!prompt.trim()) { if (isMounted.current) { toast.error('Please enter a project description'); } - return false; + throw new Error('Invalid prompt'); } try { @@ -739,21 +736,28 @@ export function ProjectProvider({ children }: { children: ReactNode }) { }, }, }); - - return result.data.createProject.id; + const createdChat = result.data?.createProject; + if (createdChat?.id) { + setChatId(createdChat.id); + setIsCreateButtonClicked(true); + localStorage.setItem('pendingChatId', createdChat.id); + return createdChat.id; + } else { + throw new Error('Project creation failed: no chatId'); + } } catch (error) { logger.error('Error creating project:', error); if (isMounted.current) { toast.error('Failed to create project from prompt'); } - return false; + throw error; } finally { if (isMounted.current) { setIsLoading(false); } } }, - [createProject] + [createProject, setChatId] ); // New function to fork a project @@ -918,6 +922,28 @@ export function ProjectProvider({ children }: { children: ReactNode }) { ] ); + useEffect(() => { + const interval = setInterval(() => { + setPollTime(Date.now()); // 每6秒更新时间,触发下面的 useEffect + }, 6000); + + return () => clearInterval(interval); + }, []); + + // Poll project data every 5 seconds + useEffect(() => { + if (!chatId) return; + + const fetch = async () => { + try { + await pollChatProject(chatId); + } catch (error) { + logger.error('Polling error:', error); + } + }; + + fetch(); + }, [pollTime, chatId, isCreateButtonClicked, pollChatProject]); const contextValue = useMemo( () => ({ projects, @@ -939,6 +965,8 @@ export function ProjectProvider({ children }: { children: ReactNode }) { editorRef, chatId, setChatId, + recentlyCompletedProjectId: recentlyCompletedProjectIdRaw, + setRecentlyCompletedProjectId, }), [ projects, @@ -957,17 +985,12 @@ export function ProjectProvider({ children }: { children: ReactNode }) { editorRef, chatId, setChatId, + recentlyCompletedProjectIdRaw, ] ); return ( - + {children} ); diff --git a/frontend/src/components/global-toast-listener.tsx b/frontend/src/components/global-toast-listener.tsx index c87aea56..b9fc30f5 100644 --- a/frontend/src/components/global-toast-listener.tsx +++ b/frontend/src/components/global-toast-listener.tsx @@ -1,23 +1,71 @@ -// GlobalToastListener.tsx 'use client'; -import { useContext, useEffect } from 'react'; -import { toast } from 'sonner'; // 或你使用的 toast 库 +import { useContext, useEffect, useRef } from 'react'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; import { ProjectContext } from './chat/code-engine/project-context'; +import { logger } from '@/app/log/logger'; +import { ProjectReadyToast } from './project-ready-toast'; const GlobalToastListener = () => { - const { recentlyCompletedProjectId, setRecentlyCompletedProjectId } = - useContext(ProjectContext); + const { + recentlyCompletedProjectId, + setRecentlyCompletedProjectId, + pollChatProject, + setChatId, + } = useContext(ProjectContext); + const router = useRouter(); + const intervalRef = useRef(null); + + // optional: if you use this in your logic + const setCurrentChatid = (id: string) => {}; // or import your actual setter useEffect(() => { - if (recentlyCompletedProjectId) { - toast.success('Project is ready! 🎉'); + if (!recentlyCompletedProjectId) return; + + const checkProjectReady = async () => { + try { + const project = await pollChatProject(recentlyCompletedProjectId); + + if (project?.projectPath) { + toast.custom( + (t) => ( + toast.dismiss(t)} + router={router} + setCurrentChatid={setCurrentChatid} + setChatId={setChatId} + /> + ), + { + duration: 30000, + } + ); + + setRecentlyCompletedProjectId(null); + + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + } else { + logger.debug('Project not ready yet, will retry...'); + } + } catch (err) { + logger.error('Error polling project status:', err); + } + }; + + intervalRef.current = setInterval(checkProjectReady, 5000); - // 可选:重置,避免重复 toast - setRecentlyCompletedProjectId(null); - } + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; }, [recentlyCompletedProjectId]); - return null; // 不渲染任何内容,只是监听 + return null; }; export default GlobalToastListener; diff --git a/frontend/src/components/project-ready-toast.tsx b/frontend/src/components/project-ready-toast.tsx new file mode 100644 index 00000000..72dfcdc2 --- /dev/null +++ b/frontend/src/components/project-ready-toast.tsx @@ -0,0 +1,57 @@ +'use client'; +import { motion } from 'framer-motion'; +import { X } from 'lucide-react'; +import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; +import { redirectChatPage } from './chat-page-navigation'; + +interface ProjectReadyToastProps { + chatId: string; + close: () => void; + router: AppRouterInstance; + setCurrentChatid: (id: string) => void; + setChatId: (id: string) => void; +} + +export const ProjectReadyToast = ({ + chatId, + close, + router, + setCurrentChatid, + setChatId, +}: ProjectReadyToastProps) => { + return ( + + {/* Close button */} + + + {/* Title */} +

+ 🎉 Project is ready! +

+ + {/* Centered Button */} +
+ +
+
+ ); +}; diff --git a/frontend/src/components/root/prompt-form.tsx b/frontend/src/components/root/prompt-form.tsx index 68a9c1c2..73c31c0f 100644 --- a/frontend/src/components/root/prompt-form.tsx +++ b/frontend/src/components/root/prompt-form.tsx @@ -1,6 +1,13 @@ 'use client'; -import { useState, forwardRef, useImperativeHandle, useEffect } from 'react'; +import { + useState, + forwardRef, + useImperativeHandle, + useEffect, + useContext, + useCallback, +} from 'react'; import { SendIcon, Sparkles, Globe, Lock, Loader2, Cpu } from 'lucide-react'; import Typewriter from 'typewriter-effect'; import { @@ -22,6 +29,10 @@ import { useModels } from '@/hooks/useModels'; import { gql, useMutation } from '@apollo/client'; import { logger } from '@/app/log/logger'; +import { useRouter } from 'next/navigation'; +import { ProjectContext } from '../chat/code-engine/project-context'; +import { redirectChatPage } from '../chat-page-navigation'; + export interface PromptFormRef { getPromptData: () => { message: string; @@ -33,7 +44,8 @@ export interface PromptFormRef { interface PromptFormProps { isAuthorized: boolean; - onSubmit: () => void; + onSubmit: () => Promise; + // onChatCreated: (chatId: string) => void; onAuthRequired: () => void; isLoading?: boolean; } @@ -54,9 +66,12 @@ export const PromptForm = forwardRef( 'public' ); const [isEnhanced, setIsEnhanced] = useState(false); - const [isFocused, setIsFocused] = useState(false); // 追踪 textarea focus + const [isFocused, setIsFocused] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); - + const router = useRouter(); + const [currentChatid, setCurrentChatid] = useState(''); + const { setChatId, setRecentlyCompletedProjectId } = + useContext(ProjectContext); const { selectedModel, setSelectedModel, @@ -79,14 +94,26 @@ export const PromptForm = forwardRef( } ); - const handleSubmit = () => { + const handleSubmit = useCallback(async () => { if (isLoading || isRegenerating) return; if (!isAuthorized) { onAuthRequired(); } else { - onSubmit(); + const chatId = await onSubmit(); + if (chatId) { + setRecentlyCompletedProjectId(chatId); + redirectChatPage(chatId, setCurrentChatid, setChatId, router); + } } - }; + }, [ + isAuthorized, + isLoading, + isRegenerating, + onAuthRequired, + onSubmit, + setChatId, + router, + ]); const handleMagicEnhance = () => { if (isLoading || isRegenerating) return; diff --git a/frontend/src/components/sidebar-item.tsx b/frontend/src/components/sidebar-item.tsx index 45d0e14f..4c2aa44a 100644 --- a/frontend/src/components/sidebar-item.tsx +++ b/frontend/src/components/sidebar-item.tsx @@ -19,10 +19,11 @@ import { cn } from '@/lib/utils'; import { useMutation } from '@apollo/client'; import { MoreHorizontal, Trash2 } from 'lucide-react'; import { useRouter } from 'next/navigation'; -import { memo, useState } from 'react'; +import { memo, useContext, useState } from 'react'; import { toast } from 'sonner'; import { EventEnum } from '../const/EventEnum'; import { logger } from '@/app/log/logger'; +import { ProjectContext } from './chat/code-engine/project-context'; interface SideBarItemProps { id: string; @@ -42,7 +43,8 @@ function SideBarItemComponent({ const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); const router = useRouter(); - + const { recentlyCompletedProjectId } = useContext(ProjectContext); + const isGenerating = id === recentlyCompletedProjectId; const isSelected = currentChatId === id; const variant = isSelected ? 'secondary' : 'ghost'; @@ -93,8 +95,16 @@ function SideBarItemComponent({ onClick={handleChatClick} >
-
- {title || 'New Chat'} +
+
+ + {title || 'New Chat'} +
diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index e3c8244c..876abbe5 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -26,9 +26,8 @@ import { motion } from 'framer-motion'; import { logger } from '@/app/log/logger'; import { useChatList } from '@/hooks/useChatList'; import { cn } from '@/lib/utils'; -import { PlusIcon } from 'lucide-react'; import { HomeIcon } from '@radix-ui/react-icons'; - +import { redirectChatPage } from './chat-page-navigation'; interface SidebarProps { setIsModalOpen: (value: boolean) => void; isCollapsed: boolean; @@ -100,13 +99,9 @@ function ChatSideBarComponent({ const handleChatSelect = useCallback( (chatId: string) => { - setCurrentChatid(chatId); - router.push(`/chat?id=${chatId}`); - setChatId(chatId); - const event = new Event(EventEnum.CHAT); - window.dispatchEvent(event); + redirectChatPage(chatId, setCurrentChatid, setChatId, router); }, - [router] + [router, setChatId] ); if (loading) return ; From f4eceeadf5e06a7e357f4ecc9f0298988df17158 Mon Sep 17 00:00:00 2001 From: pengyu Date: Sat, 29 Mar 2025 18:11:01 -0400 Subject: [PATCH 04/19] adjust the bugs for sending getchatdetail twice --- .../chat/code-engine/code-engine.tsx | 73 ++++---- .../src/components/global-toast-listener.tsx | 57 +++++-- .../src/components/project-ready-toast.tsx | 60 ++++--- frontend/src/components/sidebar-item.tsx | 157 ++++++++++-------- 4 files changed, 203 insertions(+), 144 deletions(-) diff --git a/frontend/src/components/chat/code-engine/code-engine.tsx b/frontend/src/components/chat/code-engine/code-engine.tsx index 1155c2bb..9afe17cd 100644 --- a/frontend/src/components/chat/code-engine/code-engine.tsx +++ b/frontend/src/components/chat/code-engine/code-engine.tsx @@ -71,39 +71,27 @@ export function CodeEngine({ } }, [chatId]); - // Poll for project if needed using chatId useEffect(() => { - // 如果项目已经完成,跳过轮询 - if (projectCompleted || isProjectLoadedRef.current) { - return; - } - - if (!curProject && chatId && !projectLoading) { - const loadProjectFromChat = async () => { - try { - setIsLoading(true); - const project = await pollChatProject(chatId); - if (project) { - setLocalProject(project); - // 如果成功加载项目,将状态设置为已完成 - if (project.projectPath) { - setProjectCompleted(true); - isProjectLoadedRef.current = true; - } - } - } catch (error) { - logger.error('Failed to load project from chat:', error); - } finally { - setIsLoading(false); - } - }; + // 如果全局轮询完毕,projectPath 可用了,就完成 loading bar + if ( + curProject?.id === chatId && + curProject?.projectPath && + !projectCompleted && + !isProjectLoadedRef.current + ) { + setProgress(100); + setTimerActive(false); + setIsCompleting(false); + setProjectCompleted(true); + isProjectLoadedRef.current = true; - loadProjectFromChat(); - } else { - setIsLoading(projectLoading); + try { + localStorage.setItem(`project-completed-${chatId}`, 'true'); + } catch (e) { + logger.error('Failed to save project completion status:', e); + } } - }, [chatId, curProject, projectLoading, pollChatProject, projectCompleted]); - + }, [curProject?.projectPath, chatId, projectCompleted]); // Use either curProject from context or locally polled project const activeProject = curProject || localProject; @@ -148,6 +136,9 @@ export function CodeEngine({ !isFileStructureLoading; if (shouldFetchFiles) { + setIsLoading(false); + setProjectCompleted(true); + isProjectLoadedRef.current = true; fetchFiles(); } }, [ @@ -451,12 +442,22 @@ export function CodeEngine({ )}
-

- {progress === 100 - ? 'Project ready!' - : projectLoading - ? 'Loading project...' - : `Initializing project (${progress}%)`} +

+ {progress === 100 ? ( + Project ready! + ) : ( + <> + {estimateTime > 0 ? ( + + Preparing your project (about 5-6 minutes)… + + ) : ( + + Still working on it... thanks for your patience 🙏 + + )} + + )}

diff --git a/frontend/src/components/global-toast-listener.tsx b/frontend/src/components/global-toast-listener.tsx index b9fc30f5..6c8baef4 100644 --- a/frontend/src/components/global-toast-listener.tsx +++ b/frontend/src/components/global-toast-listener.tsx @@ -6,6 +6,28 @@ import { ProjectContext } from './chat/code-engine/project-context'; import { logger } from '@/app/log/logger'; import { ProjectReadyToast } from './project-ready-toast'; +const COMPLETED_CACHE_KEY = 'completedChatIds'; + +const getCompletedFromLocalStorage = (): Set => { + try { + const raw = localStorage.getItem(COMPLETED_CACHE_KEY); + if (raw) { + return new Set(JSON.parse(raw)); + } + } catch (e) { + logger.warn('Failed to read completedChatIds from localStorage'); + } + return new Set(); +}; + +const saveCompletedToLocalStorage = (set: Set) => { + try { + localStorage.setItem(COMPLETED_CACHE_KEY, JSON.stringify(Array.from(set))); + } catch (e) { + logger.warn('Failed to save completedChatIds to localStorage'); + } +}; + const GlobalToastListener = () => { const { recentlyCompletedProjectId, @@ -15,33 +37,36 @@ const GlobalToastListener = () => { } = useContext(ProjectContext); const router = useRouter(); const intervalRef = useRef(null); + const completedIdsRef = useRef>(getCompletedFromLocalStorage()); - // optional: if you use this in your logic - const setCurrentChatid = (id: string) => {}; // or import your actual setter + const setCurrentChatid = (id: string) => {}; useEffect(() => { - if (!recentlyCompletedProjectId) return; + const chatId = recentlyCompletedProjectId; - const checkProjectReady = async () => { + if (!chatId || completedIdsRef.current.has(chatId)) return; + + intervalRef.current = setInterval(async () => { try { - const project = await pollChatProject(recentlyCompletedProjectId); + const project = await pollChatProject(chatId); if (project?.projectPath) { toast.custom( (t) => ( toast.dismiss(t)} router={router} setCurrentChatid={setCurrentChatid} setChatId={setChatId} /> ), - { - duration: 30000, - } + { duration: 10000 } ); + completedIdsRef.current.add(chatId); + saveCompletedToLocalStorage(completedIdsRef.current); + setRecentlyCompletedProjectId(null); if (intervalRef.current) { @@ -49,19 +74,15 @@ const GlobalToastListener = () => { intervalRef.current = null; } } else { - logger.debug('Project not ready yet, will retry...'); + logger.debug(`Chat ${chatId} not ready yet...`); } - } catch (err) { - logger.error('Error polling project status:', err); + } catch (e) { + logger.error('pollChatProject error:', e); } - }; - - intervalRef.current = setInterval(checkProjectReady, 5000); + }, 6000); return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + if (intervalRef.current) clearInterval(intervalRef.current); }; }, [recentlyCompletedProjectId]); diff --git a/frontend/src/components/project-ready-toast.tsx b/frontend/src/components/project-ready-toast.tsx index 72dfcdc2..f16243dd 100644 --- a/frontend/src/components/project-ready-toast.tsx +++ b/frontend/src/components/project-ready-toast.tsx @@ -21,36 +21,58 @@ export const ProjectReadyToast = ({ }: ProjectReadyToastProps) => { return ( - {/* Close button */} - - - {/* Title */} -

- 🎉 Project is ready! -

+ {/* Left: Icon + Text */} +
+
+ + + +
+ Project is ready! +
- {/* Centered Button */} -
+ {/* Right: Open Chat + Close */} +
+
); diff --git a/frontend/src/components/sidebar-item.tsx b/frontend/src/components/sidebar-item.tsx index 4c2aa44a..d324b67d 100644 --- a/frontend/src/components/sidebar-item.tsx +++ b/frontend/src/components/sidebar-item.tsx @@ -24,6 +24,7 @@ import { toast } from 'sonner'; import { EventEnum } from '../const/EventEnum'; import { logger } from '@/app/log/logger'; import { ProjectContext } from './chat/code-engine/project-context'; +import { motion } from 'framer-motion'; interface SideBarItemProps { id: string; @@ -85,91 +86,105 @@ function SideBarItemComponent({ }; return ( - - - - { - setIsDropdownOpen(false); - setIsDialogOpen(true); - }} - > + + + - - - - - - Delete chat? - - Are you sure you want to delete this chat? This action cannot be - undone. - -
- - -
-
-
- - + + + + + + + Delete chat? + + Are you sure you want to delete this chat? This action cannot be + undone. + +
+ + +
+
+
+ + + ); } From d3cc6b23f0c943d99366d17b28622b860337cae1 Mon Sep 17 00:00:00 2001 From: pengyu Date: Sun, 30 Mar 2025 22:43:57 -0400 Subject: [PATCH 05/19] add my project and community project --- .../src/components/root/projects-section.tsx | 123 ++++++++++++++---- 1 file changed, 95 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/root/projects-section.tsx b/frontend/src/components/root/projects-section.tsx index 9a06e5a2..09dc0fc8 100644 --- a/frontend/src/components/root/projects-section.tsx +++ b/frontend/src/components/root/projects-section.tsx @@ -1,58 +1,125 @@ +'use client'; + import { useQuery } from '@apollo/client'; import { FETCH_PUBLIC_PROJECTS } from '@/graphql/request'; import { ExpandableCard } from './expand-card'; +import { useContext, useState } from 'react'; +import { ProjectContext } from '../chat/code-engine/project-context'; +import { redirectChatPage } from '../chat-page-navigation'; +import { Button } from '@/components/ui/button'; +import { RotateCwIcon } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useAuthContext } from '@/providers/AuthProvider'; export function ProjectsSection() { - // Execute the GraphQL query with provided variables + const [view, setView] = useState<'my' | 'community'>('my'); + + const { user } = useAuthContext(); + const username = user?.username || ''; + const { setChatId } = useContext(ProjectContext); + const [currentChatid, setCurrentChatid] = useState(''); + const router = useRouter(); + const { data, loading, error } = useQuery(FETCH_PUBLIC_PROJECTS, { - // Make sure strategy matches the backend definition (e.g., 'latest' or 'trending') - variables: { input: { size: 10, strategy: 'latest' } }, + variables: { input: { size: 100, strategy: 'latest' } }, + }); + + const allProjects = data?.fetchPublicProjects || []; + + // 筛选我的项目 vs 社区项目 + const filteredProjects = allProjects.filter((project) => { + const projectUsername = project.user?.username || ''; + return view === 'my' + ? projectUsername === username + : projectUsername !== username; }); - const fetchedProjects = data?.fetchPublicProjects || []; - - // Transform fetched data to match the component's expected format - const transformedProjects = fetchedProjects.map((project) => ({ - id: project.id, - name: project.projectName, - path: project.projectPath, - createDate: project.createdAt - ? new Date(project.createdAt).toISOString().split('T')[0] - : '2025-01-01', - author: project.user?.username || 'Unknown', - forkNum: project.subNumber || 0, - image: - project.photoUrl || `https://picsum.photos/500/250?random=${project.id}`, - })); + const transformedProjects = filteredProjects.map((project) => { + const isReady = Boolean(project.projectPath); + return { + id: project.id, + name: project.projectName, + path: project.projectPath, + isReady, + createDate: project.createdAt + ? new Date(project.createdAt).toISOString().split('T')[0] + : '2025-01-01', + author: project.user?.username || 'Unknown', + forkNum: project.subNumber || 0, + image: + project.photoUrl || + `https://picsum.photos/500/250?random=${project.id}`, + }; + }); + + const handleOpenChat = (chatId: string) => { + redirectChatPage(chatId, setCurrentChatid, setChatId, router); + }; return (
- {/* Header and "View All" button always visible */} + {/* Header with View Toggle */}

- Featured Projects + {view === 'my' ? 'My Projects' : 'Community Projects'}

- +
+ + +
+ {/* Content */} {loading ? (
Loading...
) : error ? ( -
Error: {error.message}
+
+ Error: {error.message} +
) : ( -
+ <> {transformedProjects.length > 0 ? ( - +
+ {transformedProjects.map((project) => + view === 'my' && !project.isReady ? ( +
+ +

+ Generating project... +

+ +
+ ) : ( + + ) + )} +
) : ( - // Show message when no projects are available
No projects available.
)} -
+ )}
From 175bbc783f9c45b1e4e503ff1e78eca2081d4456 Mon Sep 17 00:00:00 2001 From: pengyu Date: Tue, 1 Apr 2025 16:51:22 -0400 Subject: [PATCH 06/19] finish all the logical part --- .../chat/code-engine/project-context.tsx | 82 +++++++-- .../src/components/global-toast-listener.tsx | 10 +- frontend/src/components/root/expand-card.tsx | 46 +++-- .../src/components/root/projects-section.tsx | 163 +++++++++++++----- frontend/src/graphql/request.ts | 2 + 5 files changed, 223 insertions(+), 80 deletions(-) diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index 490a502f..71a9b9ae 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -54,6 +54,14 @@ export interface ProjectContextType { setRecentlyCompletedProjectId: (id: string | null) => void; chatId: string | null; setChatId: (chatId: string | null) => void; + pendingProjects: Project[]; + setPendingProjects: React.Dispatch>; + refetchPublicProjects: () => Promise; + setRefetchPublicProjects: React.Dispatch< + React.SetStateAction<() => Promise> + >; + tempLoadingProjectId: string | null; + setTempLoadingProjectId: React.Dispatch>; } export const ProjectContext = createContext( @@ -102,13 +110,14 @@ const checkUrlStatus = async ( export function ProjectProvider({ children }: { children: ReactNode }) { const router = useRouter(); - const { isAuthorized } = useAuthContext(); + const { isAuthorized, user } = useAuthContext(); const [projects, setProjects] = useState([]); const [curProject, setCurProject] = useState(undefined); const [projectLoading, setProjectLoading] = useState(true); const [filePath, setFilePath] = useState(null); const [isLoading, setIsLoading] = useState(false); const editorRef = useRef(null); + const [pendingProjects, setPendingProjects] = useState([]); const [recentlyCompletedProjectIdRaw, setRecentlyCompletedProjectIdRaw] = useState(() => typeof window !== 'undefined' @@ -128,6 +137,9 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [chatId, setChatId] = useState(null); const [pollTime, setPollTime] = useState(Date.now()); const [isCreateButtonClicked, setIsCreateButtonClicked] = useState(false); + const [tempLoadingProjectId, setTempLoadingProjectId] = useState< + string | null + >(null); interface ChatProjectCacheEntry { project: Project | null; timestamp: number; @@ -153,7 +165,9 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const MAX_RETRIES = 30; const CACHE_TTL = 5 * 60 * 1000; // 5 minutes TTL for cache const SYNC_DEBOUNCE_TIME = 1000; // 1 second debounce for sync operations - + const [refetchPublicProjects, setRefetchPublicProjects] = useState< + () => Promise + >(() => async () => {}); // Mounted ref to prevent state updates after unmount const isMounted = useRef(true); @@ -708,18 +722,13 @@ export function ProjectProvider({ children }: { children: ReactNode }) { model = 'gpt-4o-mini' ): Promise => { if (!prompt.trim()) { - if (isMounted.current) { - toast.error('Please enter a project description'); - } + toast.error('Please enter a project description'); throw new Error('Invalid prompt'); } try { - if (isMounted.current) { - setIsLoading(true); - } + setIsLoading(true); - // Default packages based on typical web project needs const defaultPackages = [ { name: 'react', version: '^18.2.0' }, { name: 'next', version: '^13.4.0' }, @@ -732,32 +741,29 @@ export function ProjectProvider({ children }: { children: ReactNode }) { description: prompt, packages: defaultPackages, public: isPublic, - model: model, + model, }, }, }); + const createdChat = result.data?.createProject; if (createdChat?.id) { setChatId(createdChat.id); setIsCreateButtonClicked(true); localStorage.setItem('pendingChatId', createdChat.id); + setTempLoadingProjectId(createdChat.id); return createdChat.id; } else { throw new Error('Project creation failed: no chatId'); } } catch (error) { - logger.error('Error creating project:', error); - if (isMounted.current) { - toast.error('Failed to create project from prompt'); - } + toast.error('Failed to create project from prompt'); throw error; } finally { - if (isMounted.current) { - setIsLoading(false); - } + setIsLoading(false); } }, - [createProject, setChatId] + [createProject, setChatId, user] ); // New function to fork a project @@ -868,6 +874,30 @@ export function ProjectProvider({ children }: { children: ReactNode }) { retryCount: retries, }); + // First update the project list to ensure it exists in allProjects + setProjects((prev) => { + const exists = prev.find((p) => p.id === project.id); + return exists ? prev : [...prev, project]; + }); + + // Then more aggressively clean up pending projects + setPendingProjects((prev) => { + const filtered = prev.filter((p) => p.id !== project.id); + if (filtered.length !== prev.length) { + logger.info( + `Removed project ${project.id} from pending projects` + ); + } + return filtered; + }); + + // Then trigger the public projects refetch + await refetchPublicProjects(); + console.log( + '[pollChatProject] refetchPublicProjects triggered after project is ready:', + project.id + ); + // Trigger state sync if needed if ( now - projectSyncState.current.lastSyncTime >= @@ -885,6 +915,9 @@ export function ProjectProvider({ children }: { children: ReactNode }) { }); } + if (isMounted.current) { + setTempLoadingProjectId(null); + } return project; } } catch (error) { @@ -919,6 +952,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { MAX_RETRIES, CACHE_TTL, SYNC_DEBOUNCE_TIME, + refetchPublicProjects, ] ); @@ -967,6 +1001,12 @@ export function ProjectProvider({ children }: { children: ReactNode }) { setChatId, recentlyCompletedProjectId: recentlyCompletedProjectIdRaw, setRecentlyCompletedProjectId, + pendingProjects, + setPendingProjects, + refetchPublicProjects, + setRefetchPublicProjects, + tempLoadingProjectId, + setTempLoadingProjectId, }), [ projects, @@ -986,6 +1026,12 @@ export function ProjectProvider({ children }: { children: ReactNode }) { chatId, setChatId, recentlyCompletedProjectIdRaw, + pendingProjects, + setPendingProjects, + refetchPublicProjects, + setRefetchPublicProjects, + tempLoadingProjectId, + setTempLoadingProjectId, ] ); diff --git a/frontend/src/components/global-toast-listener.tsx b/frontend/src/components/global-toast-listener.tsx index 6c8baef4..d59a35e3 100644 --- a/frontend/src/components/global-toast-listener.tsx +++ b/frontend/src/components/global-toast-listener.tsx @@ -1,4 +1,5 @@ 'use client'; + import { useContext, useEffect, useRef } from 'react'; import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; @@ -34,6 +35,9 @@ const GlobalToastListener = () => { setRecentlyCompletedProjectId, pollChatProject, setChatId, + refreshProjects, + refetchPublicProjects, + setTempLoadingProjectId, } = useContext(ProjectContext); const router = useRouter(); const intervalRef = useRef(null); @@ -43,14 +47,15 @@ const GlobalToastListener = () => { useEffect(() => { const chatId = recentlyCompletedProjectId; - if (!chatId || completedIdsRef.current.has(chatId)) return; intervalRef.current = setInterval(async () => { try { const project = await pollChatProject(chatId); - if (project?.projectPath) { + await refreshProjects(); + await refetchPublicProjects(); // 🚀 确保刷新公共项目视图 + setTempLoadingProjectId(null); toast.custom( (t) => ( { completedIdsRef.current.add(chatId); saveCompletedToLocalStorage(completedIdsRef.current); - setRecentlyCompletedProjectId(null); if (intervalRef.current) { diff --git a/frontend/src/components/root/expand-card.tsx b/frontend/src/components/root/expand-card.tsx index 0ca16999..8a09a30e 100644 --- a/frontend/src/components/root/expand-card.tsx +++ b/frontend/src/components/root/expand-card.tsx @@ -1,4 +1,5 @@ 'use client'; + import Image from 'next/image'; import React, { useContext, useEffect, useRef, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; @@ -6,12 +7,13 @@ import { X } from 'lucide-react'; import { ProjectContext } from '../chat/code-engine/project-context'; import { URL_PROTOCOL_PREFIX } from '@/utils/const'; import { logger } from '@/app/log/logger'; +import { Button } from '@/components/ui/button'; -export function ExpandableCard({ projects }) { +export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { const [active, setActive] = useState(null); const [iframeUrl, setIframeUrl] = useState(''); const ref = useRef(null); - const { getWebUrl, takeProjectScreenshot } = useContext(ProjectContext); + const { getWebUrl } = useContext(ProjectContext); const cachedUrls = useRef(new Map()); useEffect(() => { @@ -29,7 +31,13 @@ export function ExpandableCard({ projects }) { window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [active]); + const handleCardClick = async (project) => { + if (isGenerating && onOpenChat) { + onOpenChat(); + return; + } + setActive(project); setIframeUrl(''); if (cachedUrls.current.has(project.id)) { @@ -46,6 +54,7 @@ export function ExpandableCard({ projects }) { logger.error('Error fetching project URL:', error); } }; + return ( <> @@ -55,10 +64,7 @@ export function ExpandableCard({ projects }) { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - transition={{ - duration: 0.3, - ease: [0.4, 0, 0.2, 1], - }} + transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }} className="fixed inset-0 backdrop-blur-[2px] bg-black/20 h-full w-full z-50" style={{ willChange: 'opacity' }} /> @@ -118,15 +124,7 @@ export function ExpandableCard({ projects }) { { - const data = await getWebUrl(project.path); - - logger.info(project.image); - const url = `${URL_PROTOCOL_PREFIX}://${data.domain}`; - setIframeUrl(url); - handleCardClick(project); - setActive(project); - }} + onClick={() => handleCardClick(project)} className="group cursor-pointer" > {project.name} - View Project + {isGenerating ? 'Generating...' : 'View Project'} @@ -168,6 +166,20 @@ export function ExpandableCard({ projects }) { > {project.author} + {isGenerating && onOpenChat && ( +
+ +
+ )} ))} diff --git a/frontend/src/components/root/projects-section.tsx b/frontend/src/components/root/projects-section.tsx index 09dc0fc8..4c8527b9 100644 --- a/frontend/src/components/root/projects-section.tsx +++ b/frontend/src/components/root/projects-section.tsx @@ -3,52 +3,120 @@ import { useQuery } from '@apollo/client'; import { FETCH_PUBLIC_PROJECTS } from '@/graphql/request'; import { ExpandableCard } from './expand-card'; -import { useContext, useState } from 'react'; +import { useContext, useEffect, useState, useMemo } from 'react'; import { ProjectContext } from '../chat/code-engine/project-context'; import { redirectChatPage } from '../chat-page-navigation'; import { Button } from '@/components/ui/button'; -import { RotateCwIcon } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useAuthContext } from '@/providers/AuthProvider'; +import { Project } from '../chat/project-modal'; export function ProjectsSection() { const [view, setView] = useState<'my' | 'community'>('my'); - const { user } = useAuthContext(); - const username = user?.username || ''; - const { setChatId } = useContext(ProjectContext); + const { + setChatId, + pendingProjects, + setPendingProjects, + setRefetchPublicProjects, + } = useContext(ProjectContext); const [currentChatid, setCurrentChatid] = useState(''); + const [publicRefreshCounter, setPublicRefreshCounter] = useState(0); // ✅ NEW const router = useRouter(); - const { data, loading, error } = useQuery(FETCH_PUBLIC_PROJECTS, { + const { data, loading, error, refetch } = useQuery(FETCH_PUBLIC_PROJECTS, { variables: { input: { size: 100, strategy: 'latest' } }, + fetchPolicy: 'network-only', }); + const { tempLoadingProjectId } = useContext(ProjectContext); + + useEffect(() => { + setRefetchPublicProjects(() => async () => { + setPublicRefreshCounter((prev) => prev + 1); + return await refetch(); + }); + }, [refetch, setRefetchPublicProjects]); + + useEffect(() => { + refetch(); + }, [publicRefreshCounter]); const allProjects = data?.fetchPublicProjects || []; - // 筛选我的项目 vs 社区项目 - const filteredProjects = allProjects.filter((project) => { - const projectUsername = project.user?.username || ''; - return view === 'my' - ? projectUsername === username - : projectUsername !== username; - }); + useEffect(() => { + const realProjectMap = new Map( + allProjects.map((p) => [p.id, p]) + ); + + setPendingProjects((prev) => { + const newPending = prev.filter((p) => { + const real = realProjectMap.get(p.id); + console.log('[Check Pending]', { + pendingId: p.id, + pendingName: p.projectName, + real: real ?? '❌ not found', + projectPath: real?.projectPath ?? 'N/A', + }); + return !real || !real.projectPath; + }); + + return newPending.length === prev.length ? prev : newPending; + }); + }, [allProjects, pendingProjects, setPendingProjects]); + + useEffect(() => { + console.log( + '[Effect] All realProjects updated:', + allProjects.map((p) => ({ id: p.id, path: p.projectPath })) + ); + }, [allProjects]); + + const mergedProjects = useMemo(() => { + const map = new Map(); + + pendingProjects.forEach((p) => { + map.set(p.id, { + ...p, + isReady: Boolean(p.projectPath), + _source: 'pending', + }); + }); + + allProjects.forEach((p) => { + map.set(p.id, { + ...p, + isReady: Boolean(p.projectPath), + _source: 'real', + }); + }); + + return Array.from(map.values()); + }, [pendingProjects, allProjects]); + + const filteredProjects = useMemo(() => { + if (!user?.id) return view === 'my' ? [] : mergedProjects; + + return mergedProjects.filter( + (project) => + view === 'my' ? project.userId === user.id : project.userId !== user.id // community 只要不是自己即可 + ); + }, [mergedProjects, user?.id, view]); const transformedProjects = filteredProjects.map((project) => { - const isReady = Boolean(project.projectPath); return { id: project.id, name: project.projectName, path: project.projectPath, - isReady, + isReady: project.isReady, createDate: project.createdAt ? new Date(project.createdAt).toISOString().split('T')[0] : '2025-01-01', author: project.user?.username || 'Unknown', forkNum: project.subNumber || 0, - image: - project.photoUrl || - `https://picsum.photos/500/250?random=${project.id}`, + image: project.isReady + ? project.photoUrl || + `https://picsum.photos/500/250?random=${project.id}` + : '/placeholder-black.png', }; }); @@ -59,7 +127,6 @@ export function ProjectsSection() { return (
- {/* Header with View Toggle */}

{view === 'my' ? 'My Projects' : 'Community Projects'} @@ -80,7 +147,6 @@ export function ProjectsSection() {

- {/* Content */} {loading ? (
Loading...
) : error ? ( @@ -89,30 +155,43 @@ export function ProjectsSection() {
) : ( <> + {view === 'my' && tempLoadingProjectId && ( + + redirectChatPage( + tempLoadingProjectId, + setCurrentChatid, + setChatId, + router + ) + } + /> + )} + {transformedProjects.length > 0 ? (
- {transformedProjects.map((project) => - view === 'my' && !project.isReady ? ( -
- -

- Generating project... -

- -
- ) : ( - - ) - )} + {transformedProjects.map((project) => ( + handleOpenChat(project.id)} + /> + ))}
) : (
diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index 85659e2a..87ca35b9 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -24,6 +24,7 @@ export const FETCH_PUBLIC_PROJECTS = gql` projectName projectPath createdAt + userId user { username } @@ -99,6 +100,7 @@ export const DELETE_CHAT = gql` export const GET_USER_INFO = gql` query me { me { + id, username email avatarUrl From 35526093b80ae97f319095ea9b2e9b9528dfef32 Mon Sep 17 00:00:00 2001 From: pengyu Date: Sat, 5 Apr 2025 21:42:41 -0400 Subject: [PATCH 07/19] my project works --- .../chat/code-engine/code-engine.tsx | 202 +++++++----------- .../chat/code-engine/project-context.tsx | 84 ++++++-- frontend/src/components/root/expand-card.tsx | 2 +- .../src/components/root/projects-section.tsx | 139 ++++++------ frontend/src/components/sidebar-item.tsx | 12 +- frontend/src/providers/BaseProvider.tsx | 2 +- 6 files changed, 211 insertions(+), 230 deletions(-) diff --git a/frontend/src/components/chat/code-engine/code-engine.tsx b/frontend/src/components/chat/code-engine/code-engine.tsx index 9afe17cd..79868027 100644 --- a/frontend/src/components/chat/code-engine/code-engine.tsx +++ b/frontend/src/components/chat/code-engine/code-engine.tsx @@ -20,8 +20,13 @@ export function CodeEngine({ isProjectReady?: boolean; projectId?: string; }) { - const { curProject, projectLoading, pollChatProject, editorRef } = - useContext(ProjectContext); + const { + curProject, + projectLoading, + pollChatProject, + editorRef, + setRecentlyCompletedProjectId, + } = useContext(ProjectContext); const [localProject, setLocalProject] = useState(null); const [isLoading, setIsLoading] = useState(true); const [filePath, setFilePath] = useState(null); @@ -37,25 +42,20 @@ export function CodeEngine({ >({}); const projectPathRef = useRef(null); - const [progress, setProgress] = useState(0); // 从0%开始 - const [estimateTime, setEstimateTime] = useState(6 * 60); // 保留估计时间 + const [progress, setProgress] = useState(0); + const [estimateTime, setEstimateTime] = useState(6 * 60); const [timerActive, setTimerActive] = useState(false); - const initialTime = 6 * 60; // 初始总时间(6分钟) + const initialTime = 6 * 60; const [projectCompleted, setProjectCompleted] = useState(false); - // 添加一个状态来跟踪完成动画 const [isCompleting, setIsCompleting] = useState(false); - // 添加一个ref来持久跟踪项目状态,避免重新渲染时丢失 const isProjectLoadedRef = useRef(false); - const context = useContext(ProjectContext); - if (!context) throw new Error('Must be used inside ProjectProvider'); - const { setRecentlyCompletedProjectId } = context; useEffect(() => { if (projectCompleted) { setRecentlyCompletedProjectId(curProject?.id || localProject?.id); } }, [projectCompleted]); - // 在组件挂载时从localStorage检查项目是否已完成 + useEffect(() => { try { const savedCompletion = localStorage.getItem( @@ -67,12 +67,11 @@ export function CodeEngine({ setProgress(100); } } catch (e) { - // 忽略localStorage错误 + logger.error('Failed to load project completion status:', e); } }, [chatId]); useEffect(() => { - // 如果全局轮询完毕,projectPath 可用了,就完成 loading bar if ( curProject?.id === chatId && curProject?.projectPath && @@ -92,10 +91,40 @@ export function CodeEngine({ } } }, [curProject?.projectPath, chatId, projectCompleted]); - // Use either curProject from context or locally polled project + + useEffect(() => { + if (projectCompleted || isProjectLoadedRef.current) return; + + if (!curProject && chatId && !projectLoading) { + const loadProjectFromChat = async () => { + try { + setIsLoading(true); + const project = await pollChatProject(chatId); + if (project) { + if (project.projectPath) { + setLocalProject(project); + setProjectCompleted(true); + isProjectLoadedRef.current = true; + fetchFiles(); + } else { + setLocalProject(project); + } + } + } catch (error) { + logger.error('Failed to load project from chat:', error); + } finally { + setIsLoading(false); + } + }; + + loadProjectFromChat(); + } else { + setIsLoading(projectLoading); + } + }, [chatId, curProject, projectLoading, pollChatProject, projectCompleted]); + const activeProject = curProject || localProject; - // Update projectPathRef when project changes useEffect(() => { if (activeProject?.projectPath) { projectPathRef.current = activeProject.projectPath; @@ -104,22 +133,16 @@ export function CodeEngine({ async function fetchFiles() { const projectPath = activeProject?.projectPath || projectPathRef.current; - if (!projectPath) { - return; - } + if (!projectPath) return; try { setIsFileStructureLoading(true); const response = await fetch(`/api/project?path=${projectPath}`); - if (!response.ok) { + if (!response.ok) throw new Error(`Failed to fetch file structure: ${response.status}`); - } const data = await response.json(); - if (data && data.res) { - setFileStructureData(data.res); - } else { - logger.warn('Empty or invalid file structure data received'); - } + if (data && data.res) setFileStructureData(data.res); + else logger.warn('Empty or invalid file structure data received'); } catch (error) { logger.error('Error fetching file structure:', error); } finally { @@ -127,7 +150,6 @@ export function CodeEngine({ } } - // Effect for loading file structure when project is ready useEffect(() => { const shouldFetchFiles = isProjectReady && @@ -135,12 +157,7 @@ export function CodeEngine({ Object.keys(fileStructureData).length === 0 && !isFileStructureLoading; - if (shouldFetchFiles) { - setIsLoading(false); - setProjectCompleted(true); - isProjectLoadedRef.current = true; - fetchFiles(); - } + if (shouldFetchFiles) fetchFiles(); }, [ isProjectReady, activeProject, @@ -148,7 +165,6 @@ export function CodeEngine({ fileStructureData, ]); - // Effect for selecting default file once structure is loaded useEffect(() => { if ( !isFileStructureLoading && @@ -159,10 +175,8 @@ export function CodeEngine({ } }, [isFileStructureLoading, fileStructureData, filePath]); - // Retry mechanism for fetching files if needed useEffect(() => { let retryTimeout; - if ( isProjectReady && activeProject?.projectPath && @@ -174,10 +188,7 @@ export function CodeEngine({ fetchFiles(); }, 3000); } - - return () => { - if (retryTimeout) clearTimeout(retryTimeout); - }; + return () => retryTimeout && clearTimeout(retryTimeout); }, [ isProjectReady, activeProject, @@ -196,22 +207,17 @@ export function CodeEngine({ 'index.html', 'README.md', ]; - for (const defaultFile of defaultFiles) { if (fileStructureData[`root/${defaultFile}`]) { setFilePath(defaultFile); return; } } - const firstFile = Object.entries(fileStructureData).find( ([key, item]) => key.startsWith('root/') && !item.isFolder && key !== 'root/' ); - - if (firstFile) { - setFilePath(firstFile[0].replace('root/', '')); - } + if (firstFile) setFilePath(firstFile[0].replace('root/', '')); } const handleReset = () => { @@ -223,7 +229,6 @@ export function CodeEngine({ const updateCode = async (value) => { const projectPath = activeProject?.projectPath || projectPathRef.current; if (!projectPath || !filePath) return; - try { const response = await fetch('/api/file', { method: 'POST', @@ -234,11 +239,8 @@ export function CodeEngine({ newContent: value, }), }); - - if (!response.ok) { + if (!response.ok) throw new Error(`Failed to update file: ${response.status}`); - } - await response.json(); } catch (error) { logger.error('Error updating file:', error); @@ -283,22 +285,14 @@ export function CodeEngine({ async function getCode() { const projectPath = activeProject?.projectPath || projectPathRef.current; if (!projectPath || !filePath) return; - const file_node = fileStructureData[`root/${filePath}`]; - if (!file_node) return; - - const isFolder = file_node.isFolder; - if (isFolder) return; - + if (!file_node || file_node.isFolder) return; try { const res = await fetch( `/api/file?path=${encodeURIComponent(`${projectPath}/${filePath}`)}` ); - - if (!res.ok) { + if (!res.ok) throw new Error(`Failed to fetch file content: ${res.status}`); - } - const data = await res.json(); setCode(data.content); setPrecode(data.content); @@ -306,16 +300,11 @@ export function CodeEngine({ logger.error('Error loading file content:', error); } } - getCode(); }, [filePath, activeProject, fileStructureData]); - // Determine if we're truly ready to render const showLoader = useMemo(() => { - // 如果项目已经被标记为完成,不再显示加载器 - if (projectCompleted || isProjectLoadedRef.current) { - return false; - } + if (projectCompleted || isProjectLoadedRef.current) return false; return ( !isProjectReady || isLoading || @@ -339,26 +328,23 @@ export function CodeEngine({ setTimerActive(false); setIsCompleting(false); setProjectCompleted(true); - // 同时更新ref以持久记住完成状态 isProjectLoadedRef.current = true; - - // 可选:在完成时将状态保存到localStorage try { localStorage.setItem(`project-completed-${chatId}`, 'true'); } catch (e) { - // 忽略localStorage错误 + logger.error('Failed to save project completion status:', e); } }, 800); }, 500); - return () => clearTimeout(completionTimer); - } else if ( + } + if ( showLoader && !timerActive && !projectCompleted && - !isProjectLoadedRef.current + !isProjectLoadedRef.current && + estimateTime > 1 ) { - // 只有在项目未被标记为完成时才重置 setTimerActive(true); setEstimateTime(initialTime); setProgress(0); @@ -368,36 +354,21 @@ export function CodeEngine({ useEffect(() => { let interval; - if (timerActive) { interval = setInterval(() => { setEstimateTime((prevTime) => { - if (prevTime <= 1) { - return initialTime; - } + if (prevTime <= 1) return 1; const elapsedTime = initialTime - prevTime + 1; - const newProgress = Math.min( - Math.floor((elapsedTime / initialTime) * 100), - 99 + setProgress( + Math.min(Math.floor((elapsedTime / initialTime) * 100), 99) ); - setProgress(newProgress); - return prevTime - 1; }); }, 1000); } - - return () => { - if (interval) clearInterval(interval); - }; + return () => interval && clearInterval(interval); }, [timerActive]); - const formatTime = (seconds) => { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; - }; - return (
-
{(showLoader || isCompleting) && ( @@ -440,31 +410,17 @@ export function CodeEngine({ ) : ( )} -
-

- {progress === 100 ? ( - Project ready! - ) : ( - <> - {estimateTime > 0 ? ( - - Preparing your project (about 5-6 minutes)… - - ) : ( - - Still working on it... thanks for your patience 🙏 - - )} - - )} +

+ {progress === 100 + ? 'Project ready!' + : projectLoading + ? 'Loading project...' + : `Preparing your project (about 5–6 minutes)…`}

-
+ {estimateTime <= 1 && !projectCompleted && ( +

+ Hang tight, almost there... +

+ )}
- - {/* 添加不同阶段的消息 */} - )}
-
{renderTabContent()}
- {saving && }
diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index 71a9b9ae..6ae04464 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -117,29 +117,66 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [filePath, setFilePath] = useState(null); const [isLoading, setIsLoading] = useState(false); const editorRef = useRef(null); - const [pendingProjects, setPendingProjects] = useState([]); + const [pendingProjects, setPendingProjects] = useState(() => { + if (typeof window !== 'undefined') { + try { + const raw = localStorage.getItem('pendingProjects'); + if (raw) { + return JSON.parse(raw) as Project[]; + } + } catch (e) { + logger.warn('Failed to parse pendingProjects from localStorage'); + } + } + return []; + }); + const setRecentlyCompletedProjectId = (id: string | null) => { + if (typeof window !== 'undefined') { + if (id) { + localStorage.setItem('pendingChatId', id); + } else { + localStorage.removeItem('pendingChatId'); + } + } + setRecentlyCompletedProjectIdRaw(id); + }; + const [recentlyCompletedProjectIdRaw, setRecentlyCompletedProjectIdRaw] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('pendingChatId') : null ); - + useEffect(() => { + if (typeof window === 'undefined') return; + try { + localStorage.setItem('pendingProjects', JSON.stringify(pendingProjects)); + } catch (e) { + logger.warn('Failed to store pendingProjects in localStorage'); + } + }, [pendingProjects]); // setter:更新 state + localStorage - const setRecentlyCompletedProjectId = (id: string | null) => { - if (id) { - localStorage.setItem('pendingChatId', id); - } else { - localStorage.removeItem('pendingChatId'); + const setTempLoadingProjectId = (id: string | null) => { + if (typeof window !== 'undefined') { + if (id) { + localStorage.setItem('tempLoadingProjectId', id); + } else { + localStorage.removeItem('tempLoadingProjectId'); + } } - setRecentlyCompletedProjectIdRaw(id); + setTempLoadingProjectIdRaw(id); }; const [chatId, setChatId] = useState(null); const [pollTime, setPollTime] = useState(Date.now()); const [isCreateButtonClicked, setIsCreateButtonClicked] = useState(false); - const [tempLoadingProjectId, setTempLoadingProjectId] = useState< + const [tempLoadingProjectIdRaw, setTempLoadingProjectIdRaw] = useState< string | null - >(null); + >(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('tempLoadingProjectId'); + } + return null; + }); interface ChatProjectCacheEntry { project: Project | null; timestamp: number; @@ -306,6 +343,12 @@ export function ProjectProvider({ children }: { children: ReactNode }) { loadInitialData(); }, [isAuthorized]); + useEffect(() => { + const valid = pendingProjects.filter((p) => !p.projectPath); + if (valid.length !== pendingProjects.length) { + setPendingProjects(valid); + } + }, [pendingProjects]); // Initialization and update effects useEffect(() => { @@ -910,13 +953,20 @@ export function ProjectProvider({ children }: { children: ReactNode }) { // Try to get web URL in background if (isMounted.current && project.projectPath) { - getWebUrl(project.projectPath).catch((error) => { - logger.warn('Background web URL fetch failed:', error); - }); + setCurProject(project); + localStorage.setItem('lastProjectId', project.id); + getWebUrl(project.projectPath) + .then(({ domain }) => { + const baseUrl = `${URL_PROTOCOL_PREFIX}://${domain}`; + takeProjectScreenshot(project.id, baseUrl); + }) + .catch((error) => { + logger.warn('Background web URL fetch failed:', error); + }); } if (isMounted.current) { - setTempLoadingProjectId(null); + setTempLoadingProjectId(null); } return project; } @@ -1005,7 +1055,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { setPendingProjects, refetchPublicProjects, setRefetchPublicProjects, - tempLoadingProjectId, + tempLoadingProjectId: tempLoadingProjectIdRaw, setTempLoadingProjectId, }), [ @@ -1030,8 +1080,8 @@ export function ProjectProvider({ children }: { children: ReactNode }) { setPendingProjects, refetchPublicProjects, setRefetchPublicProjects, - tempLoadingProjectId, - setTempLoadingProjectId, + tempLoadingProjectIdRaw, + setTempLoadingProjectId, ] ); diff --git a/frontend/src/components/root/expand-card.tsx b/frontend/src/components/root/expand-card.tsx index 8a09a30e..7a184ace 100644 --- a/frontend/src/components/root/expand-card.tsx +++ b/frontend/src/components/root/expand-card.tsx @@ -148,7 +148,7 @@ export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { className="absolute inset-0 bg-black/40 flex items-center justify-center" > - {isGenerating ? 'Generating...' : 'View Project'} + {isGenerating ? 'Open Chat' : 'View Project'} diff --git a/frontend/src/components/root/projects-section.tsx b/frontend/src/components/root/projects-section.tsx index 4c8527b9..912c457a 100644 --- a/frontend/src/components/root/projects-section.tsx +++ b/frontend/src/components/root/projects-section.tsx @@ -1,9 +1,9 @@ 'use client'; import { useQuery } from '@apollo/client'; -import { FETCH_PUBLIC_PROJECTS } from '@/graphql/request'; +import { FETCH_PUBLIC_PROJECTS, GET_USER_PROJECTS } from '@/graphql/request'; import { ExpandableCard } from './expand-card'; -import { useContext, useEffect, useState, useMemo } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; import { ProjectContext } from '../chat/code-engine/project-context'; import { redirectChatPage } from '../chat-page-navigation'; import { Button } from '@/components/ui/button'; @@ -14,116 +14,105 @@ import { Project } from '../chat/project-modal'; export function ProjectsSection() { const [view, setView] = useState<'my' | 'community'>('my'); const { user } = useAuthContext(); + const router = useRouter(); + const { setChatId, pendingProjects, setPendingProjects, setRefetchPublicProjects, + tempLoadingProjectId, } = useContext(ProjectContext); + const [currentChatid, setCurrentChatid] = useState(''); - const [publicRefreshCounter, setPublicRefreshCounter] = useState(0); // ✅ NEW - const router = useRouter(); + const [publicRefreshCounter, setPublicRefreshCounter] = useState(0); - const { data, loading, error, refetch } = useQuery(FETCH_PUBLIC_PROJECTS, { + // Fetch both public and user projects + const { + data: publicData, + loading: publicLoading, + error: publicError, + refetch: refetchPublic, + } = useQuery(FETCH_PUBLIC_PROJECTS, { variables: { input: { size: 100, strategy: 'latest' } }, fetchPolicy: 'network-only', }); - const { tempLoadingProjectId } = useContext(ProjectContext); + + const { + data: userData, + loading: userLoading, + error: userError, + refetch: refetchUser, + } = useQuery(GET_USER_PROJECTS, { + fetchPolicy: 'network-only', + }); useEffect(() => { setRefetchPublicProjects(() => async () => { setPublicRefreshCounter((prev) => prev + 1); - return await refetch(); + await refetchPublic(); + return await refetchUser(); }); - }, [refetch, setRefetchPublicProjects]); + }, [refetchPublic, refetchUser, setRefetchPublicProjects]); useEffect(() => { - refetch(); + refetchPublic(); + refetchUser(); }, [publicRefreshCounter]); - const allProjects = data?.fetchPublicProjects || []; + const publicProjects = publicData?.fetchPublicProjects || []; + const userProjects = userData?.getUserProjects || []; useEffect(() => { - const realProjectMap = new Map( - allProjects.map((p) => [p.id, p]) - ); + const realMap = new Map(userProjects.map((p: Project) => [p.id, p])); setPendingProjects((prev) => { - const newPending = prev.filter((p) => { - const real = realProjectMap.get(p.id); - console.log('[Check Pending]', { - pendingId: p.id, - pendingName: p.projectName, - real: real ?? '❌ not found', - projectPath: real?.projectPath ?? 'N/A', - }); + const next = prev.filter((p) => { + const real = realMap.get(p.id) as Project | undefined; return !real || !real.projectPath; }); - - return newPending.length === prev.length ? prev : newPending; + return next.length === prev.length ? prev : next; }); - }, [allProjects, pendingProjects, setPendingProjects]); - - useEffect(() => { - console.log( - '[Effect] All realProjects updated:', - allProjects.map((p) => ({ id: p.id, path: p.projectPath })) - ); - }, [allProjects]); + }, [userProjects, setPendingProjects]); - const mergedProjects = useMemo(() => { - const map = new Map(); + const mergedMyProjects = useMemo(() => { + const map = new Map(); - pendingProjects.forEach((p) => { + pendingProjects.forEach((p) => map.set(p.id, { ...p, - isReady: Boolean(p.projectPath), - _source: 'pending', - }); - }); - - allProjects.forEach((p) => { - map.set(p.id, { - ...p, - isReady: Boolean(p.projectPath), - _source: 'real', - }); - }); + userId: String(p.userId ?? user?.id), + }) + ); + userProjects.forEach((p) => map.set(p.id, p)); return Array.from(map.values()); - }, [pendingProjects, allProjects]); - - const filteredProjects = useMemo(() => { - if (!user?.id) return view === 'my' ? [] : mergedProjects; - - return mergedProjects.filter( - (project) => - view === 'my' ? project.userId === user.id : project.userId !== user.id // community 只要不是自己即可 - ); - }, [mergedProjects, user?.id, view]); - - const transformedProjects = filteredProjects.map((project) => { - return { - id: project.id, - name: project.projectName, - path: project.projectPath, - isReady: project.isReady, - createDate: project.createdAt - ? new Date(project.createdAt).toISOString().split('T')[0] - : '2025-01-01', - author: project.user?.username || 'Unknown', - forkNum: project.subNumber || 0, - image: project.isReady - ? project.photoUrl || - `https://picsum.photos/500/250?random=${project.id}` - : '/placeholder-black.png', - }; - }); + }, [pendingProjects, userProjects, user?.id]); + + const displayProjects = view === 'my' ? mergedMyProjects : publicProjects; + + const transformedProjects = displayProjects.map((project) => ({ + id: project.id, + name: project.projectName || project.title || 'Untitled Project', + path: project.projectPath ?? '', + isReady: !!project.projectPath, + createDate: project.createdAt + ? new Date(project.createdAt).toISOString().split('T')[0] + : 'N/A', + author: project.user?.username || user?.username || 'Unknown', + forkNum: project.subNumber || 0, + image: project.projectPath + ? project.photoUrl || `https://picsum.photos/500/250?random=${project.id}` + : null, + })); const handleOpenChat = (chatId: string) => { redirectChatPage(chatId, setCurrentChatid, setChatId, router); }; + const loading = view === 'my' ? userLoading : publicLoading; + const error = view === 'my' ? userError : publicError; + return (
@@ -162,7 +151,7 @@ export function ProjectsSection() { { id: tempLoadingProjectId, name: 'Generating Project...', - image: '/placeholder-black.png', // 或者 '/loading.gif' 如果你有 + image: null, isReady: false, createDate: new Date().toISOString().split('T')[0], author: user?.username || 'Unknown', diff --git a/frontend/src/components/sidebar-item.tsx b/frontend/src/components/sidebar-item.tsx index d324b67d..e2ac2eb1 100644 --- a/frontend/src/components/sidebar-item.tsx +++ b/frontend/src/components/sidebar-item.tsx @@ -90,11 +90,11 @@ function SideBarItemComponent({ initial={false} animate={{ backgroundColor: isGenerating - ? 'rgba(237, 233, 254, 0.5)' // violet-100 with transparency + ? 'rgba(209, 213, 219, 0.5)' // gray-300 with transparency : 'transparent', }} transition={{ duration: 0.3 }} - className="relative" + className="relative rounded-lg" >
)} diff --git a/frontend/src/components/root/projects-section.tsx b/frontend/src/components/root/projects-section.tsx index 912c457a..b4816abd 100644 --- a/frontend/src/components/root/projects-section.tsx +++ b/frontend/src/components/root/projects-section.tsx @@ -47,6 +47,26 @@ export function ProjectsSection() { fetchPolicy: 'network-only', }); + const publicProjects = publicData?.fetchPublicProjects || []; + const userProjects = userData?.getUserProjects || []; + + // Add effect to listen for project deletion + useEffect(() => { + const handleProjectDelete = () => { + refetchUser(); + refetchPublic(); + // Clean up any deleted projects from pendingProjects + setPendingProjects((prev) => + prev.filter((p) => userProjects.some((up) => up.id === p.id)) + ); + }; + + window.addEventListener('project-deleted', handleProjectDelete); + return () => { + window.removeEventListener('project-deleted', handleProjectDelete); + }; + }, [refetchUser, refetchPublic, setPendingProjects, userProjects]); + useEffect(() => { setRefetchPublicProjects(() => async () => { setPublicRefreshCounter((prev) => prev + 1); @@ -60,9 +80,6 @@ export function ProjectsSection() { refetchUser(); }, [publicRefreshCounter]); - const publicProjects = publicData?.fetchPublicProjects || []; - const userProjects = userData?.getUserProjects || []; - useEffect(() => { const realMap = new Map(userProjects.map((p: Project) => [p.id, p])); @@ -78,12 +95,18 @@ export function ProjectsSection() { const mergedMyProjects = useMemo(() => { const map = new Map(); - pendingProjects.forEach((p) => - map.set(p.id, { - ...p, - userId: String(p.userId ?? user?.id), - }) - ); + // Only add pending projects that are not in userProjects (not yet completed) + pendingProjects + .filter(p => !userProjects.some(up => up.id === p.id)) + .forEach((p) => + map.set(p.id, { + ...p, + userId: String(p.userId ?? user?.id), + createdAt: p.createdAt || new Date().toISOString(), + }) + ); + + // Add all user projects userProjects.forEach((p) => map.set(p.id, p)); return Array.from(map.values()); @@ -91,20 +114,22 @@ export function ProjectsSection() { const displayProjects = view === 'my' ? mergedMyProjects : publicProjects; - const transformedProjects = displayProjects.map((project) => ({ - id: project.id, - name: project.projectName || project.title || 'Untitled Project', - path: project.projectPath ?? '', - isReady: !!project.projectPath, - createDate: project.createdAt - ? new Date(project.createdAt).toISOString().split('T')[0] - : 'N/A', - author: project.user?.username || user?.username || 'Unknown', - forkNum: project.subNumber || 0, - image: project.projectPath - ? project.photoUrl || `https://picsum.photos/500/250?random=${project.id}` - : null, - })); + const transformedProjects = displayProjects + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .map((project) => ({ + id: project.id, + name: project.projectName || project.title || 'Untitled Project', + path: project.projectPath ?? '', + isReady: !!project.projectPath, + createDate: project.createdAt + ? new Date(project.createdAt).toISOString().split('T')[0] + : 'N/A', + author: project.user?.username || user?.username || 'Unknown', + forkNum: project.subNumber || 0, + image: project.projectPath + ? project.photoUrl || `https://picsum.photos/500/250?random=${project.id}` + : null, + })); const handleOpenChat = (chatId: string) => { redirectChatPage(chatId, setCurrentChatid, setChatId, router); @@ -144,7 +169,7 @@ export function ProjectsSection() {
) : ( <> - {view === 'my' && tempLoadingProjectId && ( + {/* {view === 'my' && tempLoadingProjectId && ( - )} + )} */} {transformedProjects.length > 0 ? (
diff --git a/frontend/src/components/sidebar-item.tsx b/frontend/src/components/sidebar-item.tsx index e2ac2eb1..a26dd755 100644 --- a/frontend/src/components/sidebar-item.tsx +++ b/frontend/src/components/sidebar-item.tsx @@ -44,7 +44,7 @@ function SideBarItemComponent({ const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); const router = useRouter(); - const { recentlyCompletedProjectId } = useContext(ProjectContext); + const { recentlyCompletedProjectId, setPendingProjects } = useContext(ProjectContext); const isGenerating = id === recentlyCompletedProjectId; const isSelected = currentChatId === id; const variant = isSelected ? 'secondary' : 'ghost'; @@ -57,6 +57,10 @@ function SideBarItemComponent({ const event = new Event(EventEnum.NEW_CHAT); window.dispatchEvent(event); } + // Remove from pendingProjects + setPendingProjects((prev) => prev.filter((p) => p.id !== id)); + // Dispatch project-deleted event + window.dispatchEvent(new Event('project-deleted')); refetchChats(); }, onError: (error) => { @@ -71,6 +75,11 @@ function SideBarItemComponent({ variables: { chatId: id, }, + update: (cache) => { + // Remove the deleted chat from Apollo cache + cache.evict({ id: `Chat:${id}` }); + cache.gc(); + }, }); setIsDialogOpen(false); } catch (error) { diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index 87ca35b9..9bf98eef 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -128,6 +128,7 @@ export const GET_USER_PROJECTS = gql` userId forkedFromId isDeleted + createdAt projectPackages { id content @@ -206,6 +207,16 @@ export const CREATE_PROJECT = gql` photoUrl userId subNumber + createdAt + updatedAt + isActive + isDeleted + projectPackages { + id + name + version + content + } } } } diff --git a/frontend/src/hooks/useChatList.ts b/frontend/src/hooks/useChatList.ts index 31245a60..354228cf 100644 --- a/frontend/src/hooks/useChatList.ts +++ b/frontend/src/hooks/useChatList.ts @@ -1,12 +1,13 @@ import { useQuery } from '@apollo/client'; import { GET_USER_CHATS } from '@/graphql/request'; import { Chat } from '@/graphql/type'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { useAuthContext } from '@/providers/AuthProvider'; +import { EventEnum } from '@/const/EventEnum'; export function useChatList() { const [chatListUpdated, setChatListUpdated] = useState(false); - const { isAuthorized } = useAuthContext(); + const { isAuthorized, user } = useAuthContext(); const { data: chatData, loading, @@ -25,12 +26,31 @@ export function useChatList() { setChatListUpdated(value); }, []); + // 监听用户变化和新聊天事件 + useEffect(() => { + const handleNewChat = () => { + handleRefetch(); + }; + + window.addEventListener(EventEnum.NEW_CHAT, handleNewChat); + return () => { + window.removeEventListener(EventEnum.NEW_CHAT, handleNewChat); + }; + }, [handleRefetch]); + + // 当用户ID变化时,强制刷新聊天列表 + useEffect(() => { + if (user?.id) { + handleRefetch(); + } + }, [user?.id, handleRefetch]); + const sortedChats = useMemo(() => { const chats = chatData?.getUserChats || []; - return [...chats].sort( - (a: Chat, b: Chat) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); + // Sort chats by createdAt in descending order (newest first) + return [...chats].sort((a, b) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); }, [chatData?.getUserChats]); return { diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index 6c8e4572..0552bcb6 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -139,6 +139,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { ); const logout = useCallback(() => { + // 清除当前用户的数据 + if (user?.id) { + // 清除所有与当前用户相关的 localStorage 数据 + localStorage.removeItem(`completedChatIds_${user.id}`); + localStorage.removeItem(`pendingChatId_${user.id}`); + localStorage.removeItem(`pendingProjects_${user.id}`); + localStorage.removeItem(`tempLoadingProjectId_${user.id}`); + localStorage.removeItem(`lastProjectId_${user.id}`); + + // 清除当前用户的所有项目完成状态 + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(`project-completed-${user.id}-`)) { + localStorage.removeItem(key); + } + } + } + + // 清除认证相关的 localStorage setToken(null); setIsAuthorized(false); setUser(null); From 86c8ebaafbafd85e97746ea4e0657b21402ba8d2 Mon Sep 17 00:00:00 2001 From: pengyu Date: Wed, 9 Apr 2025 22:31:12 -0400 Subject: [PATCH 10/19] current progress --- backend/.env.example | 12 +-- backend/src/project/project.service.ts | 61 ++++++++------- frontend/src/app/api/screenshot/route.ts | 17 ++++- .../src/components/global-project-poller.tsx | 26 ++++++- frontend/src/components/root/expand-card.tsx | 9 ++- .../src/components/root/projects-section.tsx | 76 ++++++++++--------- frontend/src/components/sidebar-item.tsx | 40 +++++++++- frontend/src/graphql/request.ts | 7 ++ 8 files changed, 171 insertions(+), 77 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 59b8b09e..18985977 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -13,13 +13,13 @@ OPENAI_BASE_URI="http://localhost:3001" # S3/Cloudflare R2 Configuration (Optional) # If not provided, local file storage will be used -S3_ACCESS_KEY_ID="your_s3_access_key_id" # Must be 32 characters for Cloudflare R2 -S3_SECRET_ACCESS_KEY="your_s3_secret_access_key" +S3_ACCESS_KEY_ID="6215aeddb7631fc0d6284ddbb63364f8" # Must be 32 characters for Cloudflare R2 +S3_SECRET_ACCESS_KEY="66c6c645f27e72b6bbb1de479163eb454cd4aaee212f5767a660525cb4bd813a" S3_REGION="auto" # Use 'auto' for Cloudflare R2 -S3_BUCKET_NAME="your_bucket_name" -S3_ENDPOINT="https://.r2.cloudflarestorage.com" # Cloudflare R2 endpoint -S3_ACCOUNT_ID="your_cloudflare_account_id" # Your Cloudflare account ID -S3_PUBLIC_URL="https://pub-xxx.r2.dev" # Your R2 public bucket URL +S3_BUCKET_NAME="pengyucdn" +S3_ENDPOINT="https://a85330980c7cb2d6526f81850a64fced.r2.cloudflarestorage.com" # Cloudflare R2 endpoint +S3_ACCOUNT_ID="a85330980c7cb2d6526f81850a64fced" # Your Cloudflare account ID +S3_PUBLIC_URL="https://pub-47e72e8a290d495a81fe0741fd1a2f1a.r2.dev" # Your R2 public bucket URL # mail # Set to false to disable all email functionality diff --git a/backend/src/project/project.service.ts b/backend/src/project/project.service.ts index 018b155a..a2897c36 100644 --- a/backend/src/project/project.service.ts +++ b/backend/src/project/project.service.ts @@ -198,37 +198,27 @@ export class ProjectService { this.logger.debug(`Generated project name: ${projectName}`); } - // Create project entity with "(Generating...)" suffix - const project = new Project(); - project.projectName = `${projectName} (Generating...)`; - project.projectPath = ''; // Will be updated when actual project is generated - project.userId = userId; - project.isPublic = input.public || false; - project.uniqueProjectId = uuidv4(); - project.projectPackages = []; - - // Save project - const savedProject = await this.projectsRepository.save(project); - // Create chat with proper title const defaultChat = await this.chatService.createChatWithMessage(userId, { title: projectName || 'New Project Chat', message: input.description, }); - // Bind chat to project - await this.bindProjectAndChat(savedProject, defaultChat); - - // Perform project creation asynchronously - this.createProjectInBackground(input, projectName, userId, defaultChat, savedProject); + // Perform the rest of project creation asynchronously + this.createProjectInBackground(input, projectName, userId, defaultChat); // Return chat immediately so user can start interacting return defaultChat; } catch (error) { - this.logger.error(`Error creating project: ${error.message}`, error.stack); - throw new InternalServerErrorException( - `Failed to create project: ${error.message}`, + if (error instanceof ProjectRateLimitException) { + throw error.getGraphQLError(); // Throw as a GraphQL error for the client + } + + this.logger.error( + `Error in createProject: ${error.message}`, + error.stack, ); + throw new InternalServerErrorException('Error creating the project.'); } } @@ -238,7 +228,6 @@ export class ProjectService { projectName: string, userId: string, chat: Chat, - project: Project, ): Promise { try { // Build project sequence and execute @@ -249,9 +238,13 @@ export class ProjectService { const context = new BuilderContext(sequence, sequence.id); const projectPath = await context.execute(); - // Update project with actual data - project.projectName = projectName; // Remove "(Generating...)" suffix + // Create project entity and set properties + const project = new Project(); + project.projectName = projectName; project.projectPath = projectPath; + project.userId = userId; + project.isPublic = input.public || false; + project.uniqueProjectId = uuidv4(); // Set project packages try { @@ -260,13 +253,25 @@ export class ProjectService { ); } catch (packageError) { this.logger.error(`Error processing packages: ${packageError.message}`); + // Continue even if packages processing fails project.projectPackages = []; } - // Save updated project + // Save project const savedProject = await this.projectsRepository.save(project); - this.logger.debug(`Project updated: ${savedProject.id}`); + this.logger.debug(`Project created: ${savedProject.id}`); + // Bind chat to project + const bindSuccess = await this.bindProjectAndChat(savedProject, chat); + if (!bindSuccess) { + this.logger.error( + `Failed to bind project and chat: ${savedProject.id} -> ${chat.id}`, + ); + } else { + this.logger.debug( + `Project and chat bound: ${savedProject.id} -> ${chat.id}`, + ); + } } catch (error) { this.logger.error( `Error in background project creation: ${error.message}`, @@ -803,7 +808,7 @@ export class ProjectService { this.logger.log( 'check if the github project exist: ' + project.isSyncedWithGitHub, ); - // 2) Check user's GitHub installation + // 2) Check user’s GitHub installation if (!user.githubInstallationId) { throw new Error('GitHub App not installed for this user'); } @@ -814,7 +819,7 @@ export class ProjectService { ); const userOAuthToken = user.githubAccessToken; - // 4) Create the repo if the project doesn't have it yet + // 4) Create the repo if the project doesn’t have it yet if (!project.githubRepoName || !project.githubOwner) { // Use project.projectName or generate a safe name @@ -853,4 +858,4 @@ export class ProjectService { project.isSyncedWithGitHub = true; return this.projectsRepository.save(project); } -} +} \ No newline at end of file diff --git a/frontend/src/app/api/screenshot/route.ts b/frontend/src/app/api/screenshot/route.ts index ee5569fd..a77785dd 100644 --- a/frontend/src/app/api/screenshot/route.ts +++ b/frontend/src/app/api/screenshot/route.ts @@ -49,7 +49,22 @@ export async function GET(req: Request) { timeout: 60000, // Increased timeout to 60 seconds }); - await new Promise((resolve) => setTimeout(resolve, 2000)); // Waits for 2 seconds + // 等待额外的时间让页面完全渲染 + await page.waitForTimeout(3000); + + // 尝试等待页面上的内容加载,如果失败也继续处理 + try { + // 等待页面上可能存在的主要内容元素 + await Promise.race([ + page.waitForSelector('main', { timeout: 2000 }), + page.waitForSelector('#root', { timeout: 2000 }), + page.waitForSelector('.app', { timeout: 2000 }), + page.waitForSelector('h1', { timeout: 2000 }), + ]); + } catch (waitError) { + // 忽略等待选择器的错误,继续截图 + logger.info('Unable to find common page elements, continuing with screenshot'); + } // Take screenshot const screenshot = await page.screenshot({ diff --git a/frontend/src/components/global-project-poller.tsx b/frontend/src/components/global-project-poller.tsx index 36019f30..85058bb2 100644 --- a/frontend/src/components/global-project-poller.tsx +++ b/frontend/src/components/global-project-poller.tsx @@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'; import { ProjectContext } from './chat/code-engine/project-context'; import { logger } from '@/app/log/logger'; import { ProjectReadyToast } from './project-ready-toast'; +import { URL_PROTOCOL_PREFIX } from '@/utils/const'; const COMPLETED_CACHE_KEY = 'completedChatIds'; @@ -38,6 +39,8 @@ const GlobalToastListener = () => { refreshProjects, refetchPublicProjects, setTempLoadingProjectId, + getWebUrl, + takeProjectScreenshot } = useContext(ProjectContext); const router = useRouter(); const intervalRef = useRef(null); @@ -56,6 +59,26 @@ const GlobalToastListener = () => { await refreshProjects(); await refetchPublicProjects(); setTempLoadingProjectId(null); + + // 确保为项目截图 + try { + if (project.id && project.projectPath) { + logger.info(`Taking screenshot for project ${project.id}`); + // 获取项目URL并进行截图 + const { domain } = await getWebUrl(project.projectPath); + const baseUrl = `${URL_PROTOCOL_PREFIX}://${domain}`; + + // 等待5秒钟让服务完全启动 + logger.info(`Waiting for service to fully start before taking screenshot for project ${project.id}`); + await new Promise(resolve => setTimeout(resolve, 5000)); + + await takeProjectScreenshot(project.id, baseUrl); + logger.info(`Screenshot taken for project ${project.id}`); + } + } catch (screenshotError) { + logger.error('Error taking project screenshot:', screenshotError); + } + toast.custom( (t) => ( { return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; - }, [recentlyCompletedProjectId]); + }, [recentlyCompletedProjectId, pollChatProject, refreshProjects, refetchPublicProjects, + setTempLoadingProjectId, getWebUrl, takeProjectScreenshot, router, setChatId]); return null; }; diff --git a/frontend/src/components/root/expand-card.tsx b/frontend/src/components/root/expand-card.tsx index 9523c175..e3939358 100644 --- a/frontend/src/components/root/expand-card.tsx +++ b/frontend/src/components/root/expand-card.tsx @@ -119,7 +119,7 @@ export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { ) : null} -
+
{projects.map((project) => ( - + {project.name} + {project.author} diff --git a/frontend/src/components/root/projects-section.tsx b/frontend/src/components/root/projects-section.tsx index b4816abd..f2f11f4a 100644 --- a/frontend/src/components/root/projects-section.tsx +++ b/frontend/src/components/root/projects-section.tsx @@ -118,7 +118,7 @@ export function ProjectsSection() { .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .map((project) => ({ id: project.id, - name: project.projectName || project.title || 'Untitled Project', + name: project.projectName || 'Untitled Project', path: project.projectPath ?? '', isReady: !!project.projectPath, createDate: project.createdAt @@ -126,11 +126,46 @@ export function ProjectsSection() { : 'N/A', author: project.user?.username || user?.username || 'Unknown', forkNum: project.subNumber || 0, - image: project.projectPath - ? project.photoUrl || `https://picsum.photos/500/250?random=${project.id}` - : null, + image: project.photoUrl || (project.projectPath + ? `https://picsum.photos/500/250?random=${project.id}` + : null), })); + // 添加临时生成中的项目 + const allProjects = [...transformedProjects]; + + // 添加当前正在加载的项目(如果有且不在已有列表中) + if (view === 'my' && tempLoadingProjectId && !allProjects.some(p => p.id === tempLoadingProjectId)) { + allProjects.unshift({ + id: tempLoadingProjectId, + name: 'Generating Project...', + path: '', + isReady: false, + createDate: new Date().toISOString().split('T')[0], + author: user?.username || 'Unknown', + forkNum: 0, + image: null, + }); + } + + // 添加其他待处理项目 + if (view === 'my') { + pendingProjects + .filter(p => !p.projectPath && p.id !== tempLoadingProjectId && !allProjects.some(proj => proj.id === p.id)) + .forEach(project => { + allProjects.unshift({ + id: project.id, + name: project.projectName || 'Generating Project...', + path: '', + isReady: false, + createDate: project.createdAt || new Date().toISOString().split('T')[0], + author: user?.username || 'Unknown', + forkNum: 0, + image: null, + }); + }); + } + const handleOpenChat = (chatId: string) => { redirectChatPage(chatId, setCurrentChatid, setChatId, router); }; @@ -169,36 +204,9 @@ export function ProjectsSection() {
) : ( <> - {/* {view === 'my' && tempLoadingProjectId && ( - - redirectChatPage( - tempLoadingProjectId, - setCurrentChatid, - setChatId, - router - ) - } - /> - )} */} - - {transformedProjects.length > 0 ? ( -
- {transformedProjects.map((project) => ( + {allProjects.length > 0 ? ( +
+ {allProjects.map((project) => ( { + logger.info('Project deleted successfully'); + }, + onError: (error) => { + logger.error('Error deleting project:', error); + toast.error('Failed to delete associated project'); + }, + }); + const [deleteChat] = useMutation(DELETE_CHAT, { onCompleted: () => { - toast.success('Chat deleted successfully'); + toast.success('Chat and associated project deleted successfully'); if (isSelected) { router.push('/'); const event = new Event(EventEnum.NEW_CHAT); @@ -71,6 +83,12 @@ function SideBarItemComponent({ const handleDeleteChat = async () => { try { + const chatDetailsResult = await getChatDetails({ + variables: { chatId: id } + }); + + const projectId = chatDetailsResult?.data?.getChatDetails?.project?.id; + await deleteChat({ variables: { chatId: id, @@ -81,6 +99,22 @@ function SideBarItemComponent({ cache.gc(); }, }); + + if (projectId) { + try { + await deleteProject({ + variables: { projectId }, + update: (cache) => { + // 清除项目缓存 + cache.evict({ id: `Project:${projectId}` }); + cache.gc(); + } + }); + } catch (projectError) { + logger.error('Error deleting associated project:', projectError); + } + } + setIsDialogOpen(false); } catch (error) { logger.error('Error deleting chat:', error); diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index 9bf98eef..89d7d012 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -267,6 +267,13 @@ export const UPDATE_PROJECT_PHOTO_URL = gql` } `; +// Mutation to delete a project +export const DELETE_PROJECT = gql` + mutation DeleteProject($projectId: String!) { + deleteProject(projectId: $projectId) + } +`; + // Query to get subscribed/forked projects export const GET_SUBSCRIBED_PROJECTS = gql` query GetSubscribedProjects { From 197476949f7d60d1b8320a5488c5ebcd83161e1d Mon Sep 17 00:00:00 2001 From: pengyu Date: Wed, 9 Apr 2025 22:46:17 -0400 Subject: [PATCH 11/19] revert: remove sensitive values from .env.example --- backend/.env.example | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 18985977..59b8b09e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -13,13 +13,13 @@ OPENAI_BASE_URI="http://localhost:3001" # S3/Cloudflare R2 Configuration (Optional) # If not provided, local file storage will be used -S3_ACCESS_KEY_ID="6215aeddb7631fc0d6284ddbb63364f8" # Must be 32 characters for Cloudflare R2 -S3_SECRET_ACCESS_KEY="66c6c645f27e72b6bbb1de479163eb454cd4aaee212f5767a660525cb4bd813a" +S3_ACCESS_KEY_ID="your_s3_access_key_id" # Must be 32 characters for Cloudflare R2 +S3_SECRET_ACCESS_KEY="your_s3_secret_access_key" S3_REGION="auto" # Use 'auto' for Cloudflare R2 -S3_BUCKET_NAME="pengyucdn" -S3_ENDPOINT="https://a85330980c7cb2d6526f81850a64fced.r2.cloudflarestorage.com" # Cloudflare R2 endpoint -S3_ACCOUNT_ID="a85330980c7cb2d6526f81850a64fced" # Your Cloudflare account ID -S3_PUBLIC_URL="https://pub-47e72e8a290d495a81fe0741fd1a2f1a.r2.dev" # Your R2 public bucket URL +S3_BUCKET_NAME="your_bucket_name" +S3_ENDPOINT="https://.r2.cloudflarestorage.com" # Cloudflare R2 endpoint +S3_ACCOUNT_ID="your_cloudflare_account_id" # Your Cloudflare account ID +S3_PUBLIC_URL="https://pub-xxx.r2.dev" # Your R2 public bucket URL # mail # Set to false to disable all email functionality From ac64d52247f35a6e86901a61af7c6a36c463061c Mon Sep 17 00:00:00 2001 From: pengyu Date: Thu, 10 Apr 2025 20:49:12 -0400 Subject: [PATCH 12/19] working with everything but preview and photourl broken --- backend/src/project/dto/project.input.ts | 3 + backend/src/project/project.resolver.ts | 6 +- backend/src/project/project.service.ts | 7 +- frontend/src/app/api/runProject/route.ts | 6 +- frontend/src/app/api/screenshot/route.ts | 146 ++++++++++-------- .../src/components/global-project-poller.tsx | 13 +- frontend/src/components/root/expand-card.tsx | 20 +-- .../src/components/root/projects-section.tsx | 8 +- frontend/src/graphql/schema.gql | 1 + 9 files changed, 116 insertions(+), 94 deletions(-) diff --git a/backend/src/project/dto/project.input.ts b/backend/src/project/dto/project.input.ts index 4dc9c0be..c55e8e18 100644 --- a/backend/src/project/dto/project.input.ts +++ b/backend/src/project/dto/project.input.ts @@ -124,6 +124,9 @@ export class FetchPublicProjectsInputs { @Field() size: number; + + @Field() + currentUserId: string; } @InputType() diff --git a/backend/src/project/project.resolver.ts b/backend/src/project/project.resolver.ts index c1e899cb..d7fdf6d6 100644 --- a/backend/src/project/project.resolver.ts +++ b/backend/src/project/project.resolver.ts @@ -103,12 +103,14 @@ export class ProjectsResolver { const { buffer, mimetype } = await validateAndBufferFile(file); // Call the service with the extracted buffer and mimetype - return this.projectService.updateProjectPhotoUrl( + const project1= await this.projectService.updateProjectPhotoUrl( userId, projectId, buffer, mimetype, ); + this.logger.debug('project1', project1.photoUrl); + return project1; } @Mutation(() => Project) @@ -152,8 +154,10 @@ export class ProjectsResolver { */ @Query(() => [Project]) async fetchPublicProjects( + @GetUserIdFromToken() userId: string, @Args('input') input: FetchPublicProjectsInputs, ): Promise { + input.currentUserId = userId; return this.projectService.fetchPublicProjects(input); } diff --git a/backend/src/project/project.service.ts b/backend/src/project/project.service.ts index a2897c36..ad17897f 100644 --- a/backend/src/project/project.service.ts +++ b/backend/src/project/project.service.ts @@ -644,9 +644,8 @@ export class ProjectService { const limit = input.size > 50 ? 50 : input.size; const whereCondition = { - isPublic: true, isDeleted: false, - photoUrl: Not(IsNull()), + userId: Not(input.currentUserId), // Exclude current user's projects }; if (input.strategy === 'latest') { @@ -808,7 +807,7 @@ export class ProjectService { this.logger.log( 'check if the github project exist: ' + project.isSyncedWithGitHub, ); - // 2) Check user’s GitHub installation + // 2) Check user's GitHub installation if (!user.githubInstallationId) { throw new Error('GitHub App not installed for this user'); } @@ -819,7 +818,7 @@ export class ProjectService { ); const userOAuthToken = user.githubAccessToken; - // 4) Create the repo if the project doesn’t have it yet + // 4) Create the repo if the project doesn't have it yet if (!project.githubRepoName || !project.githubOwner) { // Use project.projectName or generate a safe name diff --git a/frontend/src/app/api/runProject/route.ts b/frontend/src/app/api/runProject/route.ts index 79b286ff..37704a66 100644 --- a/frontend/src/app/api/runProject/route.ts +++ b/frontend/src/app/api/runProject/route.ts @@ -517,10 +517,12 @@ export async function GET(req: Request) { existingContainer.containerId ); if (isRunning) { + logger.info(`Using existing container with port: ${existingContainer.port}`); return NextResponse.json({ message: 'Docker container already running', domain: existingContainer.domain, containerId: existingContainer.containerId, + port: existingContainer.port, }); } else { // Remove non-running container from state @@ -547,12 +549,14 @@ export async function GET(req: Request) { processingRequests.add(projectPath); try { - const { domain, containerId } = await runDockerContainer(projectPath); + const { domain, containerId, port } = await runDockerContainer(projectPath); + logger.info(`Successfully started container on port: ${port}`); return NextResponse.json({ message: 'Docker container started', domain, containerId, + port, }); } catch (error: any) { logger.error(`Failed to start Docker container:`, error); diff --git a/frontend/src/app/api/screenshot/route.ts b/frontend/src/app/api/screenshot/route.ts index a77785dd..922ef833 100644 --- a/frontend/src/app/api/screenshot/route.ts +++ b/frontend/src/app/api/screenshot/route.ts @@ -22,6 +22,7 @@ export async function GET(req: Request) { const { searchParams } = new URL(req.url); const url = searchParams.get('url'); let page = null; + const MAX_RETRIES = 3; if (!url) { return NextResponse.json( @@ -30,12 +31,15 @@ export async function GET(req: Request) { ); } - try { - // Get browser instance - const browser = await getBrowser(); + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + logger.info(`Screenshot attempt ${attempt + 1} for ${url}`); + + // Get browser instance + const browser = await getBrowser(); - // Create a new page - page = await browser.newPage(); + // Create a new page + page = await browser.newPage(); // Set viewport to a reasonable size await page.setViewport({ @@ -43,28 +47,29 @@ export async function GET(req: Request) { height: 900, }); - // Navigate to URL with increased timeout and more reliable wait condition - await page.goto(url, { - waitUntil: 'domcontentloaded', // Less strict than networkidle0 - timeout: 60000, // Increased timeout to 60 seconds - }); + // Navigate to URL with increased timeout and more reliable wait condition + await page.goto(url, { + waitUntil: 'networkidle2', // 更改为等待网络空闲状态,确保页面完全加载 + timeout: 90000, // 增加超时时间到90秒 + }); - // 等待额外的时间让页面完全渲染 - await page.waitForTimeout(3000); + // 等待额外的时间让页面完全渲染 + await page.waitForTimeout(8000); // 增加等待时间到8秒 - // 尝试等待页面上的内容加载,如果失败也继续处理 - try { - // 等待页面上可能存在的主要内容元素 - await Promise.race([ - page.waitForSelector('main', { timeout: 2000 }), - page.waitForSelector('#root', { timeout: 2000 }), - page.waitForSelector('.app', { timeout: 2000 }), - page.waitForSelector('h1', { timeout: 2000 }), - ]); - } catch (waitError) { - // 忽略等待选择器的错误,继续截图 - logger.info('Unable to find common page elements, continuing with screenshot'); - } + // 尝试等待页面上的内容加载,如果失败也继续处理 + try { + // 等待页面上可能存在的主要内容元素 + await Promise.race([ + page.waitForSelector('main', { timeout: 5000 }), + page.waitForSelector('#root', { timeout: 5000 }), + page.waitForSelector('.app', { timeout: 5000 }), + page.waitForSelector('h1', { timeout: 5000 }), + page.waitForSelector('div', { timeout: 5000 }), // 添加更通用的选择器 + ]); + } catch (waitError) { + // 忽略等待选择器的错误,继续截图 + logger.info('Unable to find common page elements, continuing with screenshot'); + } // Take screenshot const screenshot = await page.screenshot({ @@ -72,51 +77,66 @@ export async function GET(req: Request) { fullPage: true, }); - // Always close the page when done - if (page) { - await page.close(); - } - - // Return the screenshot as a PNG image - return new Response(screenshot, { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 's-maxage=3600', - }, - }); - } catch (error: any) { - logger.error('Screenshot error:', error); - - // Ensure page is closed even if an error occurs - if (page) { - try { + // Always close the page when done + if (page) { await page.close(); - } catch (closeError) { - logger.error('Error closing page:', closeError); } - } - // If browser seems to be in a bad state, recreate it - if ( - error.message.includes('Target closed') || - error.message.includes('Protocol error') || - error.message.includes('Target.createTarget') - ) { - try { - if (browserInstance) { - await browserInstance.close(); - browserInstance = null; + // Return the screenshot as a PNG image + return new Response(screenshot, { + headers: { + 'Content-Type': 'image/png', + 'Cache-Control': 's-maxage=3600', + }, + }); + } catch (error: any) { + logger.error(`Screenshot error on attempt ${attempt + 1}:`, error); + + // Ensure page is closed even if an error occurs + if (page) { + try { + await page.close(); + } catch (closeError) { + logger.error('Error closing page:', closeError); } - } catch (closeBrowserError) { - logger.error('Error closing browser:', closeBrowserError); } - } - return NextResponse.json( - { error: error.message || 'Failed to capture screenshot' }, - { status: 500 } - ); + // If browser seems to be in a bad state, recreate it + if ( + error.message.includes('Target closed') || + error.message.includes('Protocol error') || + error.message.includes('Target.createTarget') + ) { + try { + if (browserInstance) { + await browserInstance.close(); + browserInstance = null; + } + } catch (closeBrowserError) { + logger.error('Error closing browser:', closeBrowserError); + } + } + + // 如果这不是最后一次尝试,则继续 + if (attempt < MAX_RETRIES - 1) { + // 等待一会儿再重试 + await new Promise(resolve => setTimeout(resolve, 3000)); + continue; + } + + // 最后一次尝试失败 + return NextResponse.json( + { error: error.message || 'Failed to capture screenshot after multiple attempts' }, + { status: 500 } + ); + } } + + // 如果重试都失败 + return NextResponse.json( + { error: 'Failed to capture screenshot after exhausting all retries' }, + { status: 500 } + ); } // Handle process termination to close browser diff --git a/frontend/src/components/global-project-poller.tsx b/frontend/src/components/global-project-poller.tsx index 85058bb2..b9ad6a9c 100644 --- a/frontend/src/components/global-project-poller.tsx +++ b/frontend/src/components/global-project-poller.tsx @@ -65,8 +65,17 @@ const GlobalToastListener = () => { if (project.id && project.projectPath) { logger.info(`Taking screenshot for project ${project.id}`); // 获取项目URL并进行截图 - const { domain } = await getWebUrl(project.projectPath); - const baseUrl = `${URL_PROTOCOL_PREFIX}://${domain}`; + const { domain, port } = await getWebUrl(project.projectPath); + + // 使用端口直接访问 + let baseUrl; + if (port) { + baseUrl = `${URL_PROTOCOL_PREFIX}://localhost:${port}`; + } else { + baseUrl = `${URL_PROTOCOL_PREFIX}://${domain}`; + } + + logger.info(`Using URL for screenshot: ${baseUrl}`); // 等待5秒钟让服务完全启动 logger.info(`Waiting for service to fully start before taking screenshot for project ${project.id}`); diff --git a/frontend/src/components/root/expand-card.tsx b/frontend/src/components/root/expand-card.tsx index e3939358..613a4833 100644 --- a/frontend/src/components/root/expand-card.tsx +++ b/frontend/src/components/root/expand-card.tsx @@ -33,26 +33,10 @@ export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { }, [active]); const handleCardClick = async (project) => { - if (isGenerating && onOpenChat) { + if (onOpenChat) { onOpenChat(); return; } - - setActive(project); - setIframeUrl(''); - if (cachedUrls.current.has(project.id)) { - setIframeUrl(cachedUrls.current.get(project.id)); - return; - } - - try { - const data = await getWebUrl(project.path); - const url = `${URL_PROTOCOL_PREFIX}://${data.domain}`; - cachedUrls.current.set(project.id, url); - setIframeUrl(url); - } catch (error) { - logger.error('Error fetching project URL:', error); - } }; return ( @@ -148,7 +132,7 @@ export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { className="absolute inset-0 bg-black/40 flex items-center justify-center" > - {isGenerating ? 'Open Chat' : 'View Project'} + {isGenerating ? 'Open Chat' : 'Open Project'} diff --git a/frontend/src/components/root/projects-section.tsx b/frontend/src/components/root/projects-section.tsx index f2f11f4a..cefbe562 100644 --- a/frontend/src/components/root/projects-section.tsx +++ b/frontend/src/components/root/projects-section.tsx @@ -34,7 +34,7 @@ export function ProjectsSection() { error: publicError, refetch: refetchPublic, } = useQuery(FETCH_PUBLIC_PROJECTS, { - variables: { input: { size: 100, strategy: 'latest' } }, + variables: { input: { size: 100, strategy: 'latest', currentUserId: user?.id || '' } }, fetchPolicy: 'network-only', }); @@ -114,7 +114,7 @@ export function ProjectsSection() { const displayProjects = view === 'my' ? mergedMyProjects : publicProjects; - const transformedProjects = displayProjects + const transformedProjects = [...displayProjects] .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .map((project) => ({ id: project.id, @@ -126,9 +126,7 @@ export function ProjectsSection() { : 'N/A', author: project.user?.username || user?.username || 'Unknown', forkNum: project.subNumber || 0, - image: project.photoUrl || (project.projectPath - ? `https://picsum.photos/500/250?random=${project.id}` - : null), + image: project.photoUrl || null, })); // 添加临时生成中的项目 diff --git a/frontend/src/graphql/schema.gql b/frontend/src/graphql/schema.gql index 8bbdde17..78bbeb72 100644 --- a/frontend/src/graphql/schema.gql +++ b/frontend/src/graphql/schema.gql @@ -69,6 +69,7 @@ type EmailConfirmationResponse { } input FetchPublicProjectsInputs { + currentUserId: String! size: Float! strategy: String! } From b0dec4ae4adc5012896beecce0c5281cc243982e Mon Sep 17 00:00:00 2001 From: pengyu Date: Fri, 11 Apr 2025 18:02:24 -0400 Subject: [PATCH 13/19] revert route --- frontend/src/app/api/runProject/route.ts | 8 +- frontend/src/app/api/screenshot/route.ts | 107 +++++++----------- .../chat/code-engine/project-context.tsx | 27 +++-- 3 files changed, 62 insertions(+), 80 deletions(-) diff --git a/frontend/src/app/api/runProject/route.ts b/frontend/src/app/api/runProject/route.ts index 37704a66..a9fb8751 100644 --- a/frontend/src/app/api/runProject/route.ts +++ b/frontend/src/app/api/runProject/route.ts @@ -517,12 +517,10 @@ export async function GET(req: Request) { existingContainer.containerId ); if (isRunning) { - logger.info(`Using existing container with port: ${existingContainer.port}`); return NextResponse.json({ message: 'Docker container already running', domain: existingContainer.domain, containerId: existingContainer.containerId, - port: existingContainer.port, }); } else { // Remove non-running container from state @@ -549,14 +547,12 @@ export async function GET(req: Request) { processingRequests.add(projectPath); try { - const { domain, containerId, port } = await runDockerContainer(projectPath); - logger.info(`Successfully started container on port: ${port}`); + const { domain, containerId } = await runDockerContainer(projectPath); return NextResponse.json({ message: 'Docker container started', domain, containerId, - port, }); } catch (error: any) { logger.error(`Failed to start Docker container:`, error); @@ -567,4 +563,4 @@ export async function GET(req: Request) { } finally { processingRequests.delete(projectPath); } -} +} \ No newline at end of file diff --git a/frontend/src/app/api/screenshot/route.ts b/frontend/src/app/api/screenshot/route.ts index 922ef833..25cc91fe 100644 --- a/frontend/src/app/api/screenshot/route.ts +++ b/frontend/src/app/api/screenshot/route.ts @@ -22,7 +22,6 @@ export async function GET(req: Request) { const { searchParams } = new URL(req.url); const url = searchParams.get('url'); let page = null; - const MAX_RETRIES = 3; if (!url) { return NextResponse.json( @@ -31,15 +30,12 @@ export async function GET(req: Request) { ); } - for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { - try { - logger.info(`Screenshot attempt ${attempt + 1} for ${url}`); - - // Get browser instance - const browser = await getBrowser(); + try { + // Get browser instance + const browser = await getBrowser(); - // Create a new page - page = await browser.newPage(); + // Create a new page + page = await browser.newPage(); // Set viewport to a reasonable size await page.setViewport({ @@ -77,66 +73,51 @@ export async function GET(req: Request) { fullPage: true, }); - // Always close the page when done - if (page) { - await page.close(); - } + // Always close the page when done + if (page) { + await page.close(); + } - // Return the screenshot as a PNG image - return new Response(screenshot, { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 's-maxage=3600', - }, - }); - } catch (error: any) { - logger.error(`Screenshot error on attempt ${attempt + 1}:`, error); - - // Ensure page is closed even if an error occurs - if (page) { - try { - await page.close(); - } catch (closeError) { - logger.error('Error closing page:', closeError); - } - } + // Return the screenshot as a PNG image + return new Response(screenshot, { + headers: { + 'Content-Type': 'image/png', + 'Cache-Control': 's-maxage=3600', + }, + }); + } catch (error: any) { + logger.error('Screenshot error:', error); - // If browser seems to be in a bad state, recreate it - if ( - error.message.includes('Target closed') || - error.message.includes('Protocol error') || - error.message.includes('Target.createTarget') - ) { - try { - if (browserInstance) { - await browserInstance.close(); - browserInstance = null; - } - } catch (closeBrowserError) { - logger.error('Error closing browser:', closeBrowserError); - } + // Ensure page is closed even if an error occurs + if (page) { + try { + await page.close(); + } catch (closeError) { + logger.error('Error closing page:', closeError); } + } - // 如果这不是最后一次尝试,则继续 - if (attempt < MAX_RETRIES - 1) { - // 等待一会儿再重试 - await new Promise(resolve => setTimeout(resolve, 3000)); - continue; + // If browser seems to be in a bad state, recreate it + if ( + error.message.includes('Target closed') || + error.message.includes('Protocol error') || + error.message.includes('Target.createTarget') + ) { + try { + if (browserInstance) { + await browserInstance.close(); + browserInstance = null; + } + } catch (closeBrowserError) { + logger.error('Error closing browser:', closeBrowserError); } - - // 最后一次尝试失败 - return NextResponse.json( - { error: error.message || 'Failed to capture screenshot after multiple attempts' }, - { status: 500 } - ); } - } - // 如果重试都失败 - return NextResponse.json( - { error: 'Failed to capture screenshot after exhausting all retries' }, - { status: 500 } - ); + return NextResponse.json( + { error: error.message || 'Failed to capture screenshot' }, + { status: 500 } + ); + } } // Handle process termination to close browser @@ -147,4 +128,4 @@ process.on('SIGINT', async () => { browserInstance = null; } process.exit(0); -}); +}); \ No newline at end of file diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index 28dd58bf..82ae6409 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -603,33 +603,36 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const takeProjectScreenshot = useCallback( async (projectId: string, url: string): Promise => { - // Check if this screenshot operation is already in progress const operationKey = `screenshot_${projectId}`; if (pendingOperations.current.get(operationKey)) { + logger.debug(`[screenshot] Project ${projectId} is already being processed`); return; } - + pendingOperations.current.set(operationKey, true); - + logger.debug(`[screenshot] Start for Project ${projectId}, URL: ${url}`); + try { - // Check if the URL is accessible + logger.debug(`[screenshot] Checking accessibility for ${url}`); const isUrlAccessible = await checkUrlStatus(url); if (!isUrlAccessible) { - logger.warn(`URL ${url} is not accessible after multiple retries`); + logger.warn(`[screenshot] URL ${url} is not accessible after retries`); return; } - - // Add a cache buster to avoid previous screenshot caching + const screenshotUrl = `/api/screenshot?url=${encodeURIComponent(url)}&t=${Date.now()}`; + logger.debug(`[screenshot] Sending request to ${screenshotUrl}`); const screenshotResponse = await fetch(screenshotUrl); - + if (!screenshotResponse.ok) { throw new Error( - `Failed to capture screenshot: ${screenshotResponse.status} ${screenshotResponse.statusText}` + `[screenshot] Failed to capture: ${screenshotResponse.status} ${screenshotResponse.statusText}` ); } - + const arrayBuffer = await screenshotResponse.arrayBuffer(); + logger.debug(`[screenshot] Screenshot captured for Project ${projectId}, uploading...`); + const blob = new Blob([arrayBuffer], { type: 'image/png' }); const file = new File([blob], 'screenshot.png', { type: 'image/png' }); @@ -642,13 +645,15 @@ export function ProjectProvider({ children }: { children: ReactNode }) { }, }); } catch (error) { - logger.error('Error taking screenshot:', error); + logger.error(`[screenshot] Error for Project ${projectId}:`, error); } finally { + logger.debug(`[screenshot] Finished process for Project ${projectId}`); pendingOperations.current.delete(operationKey); } }, [updateProjectPhotoMutation] ); + const getWebUrl = useCallback( async ( From 1e27c8de6f1502903f0a928a14c365b755f9b412 Mon Sep 17 00:00:00 2001 From: pengyu Date: Sat, 12 Apr 2025 16:56:29 -0400 Subject: [PATCH 14/19] fix the problem doesn't allow for screenshot --- docker/project-base-image/Dockerfile | 12 ++++--- frontend/src/app/api/screenshot/route.ts | 40 +++++++++++++----------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/docker/project-base-image/Dockerfile b/docker/project-base-image/Dockerfile index 9d8300e0..59f9dac4 100644 --- a/docker/project-base-image/Dockerfile +++ b/docker/project-base-image/Dockerfile @@ -5,12 +5,14 @@ WORKDIR /app # Pre-install common frontend dependencies to speed up project startup RUN npm install -g npm@latest vite@latest -# Create a non-root user to run the app -RUN groupadd -r appuser && useradd -r -g appuser -m appuser -RUN chown -R appuser:appuser /app +#TODO: Uncomment this when we have a non-root usr (Allen) +# #Create a non-root user to run the app +# RUN groupadd -r appuser && useradd -r -g appuser -m appuser +# RUN chown -R appuser:appuser /app +# RUN chmod -R u+w /app -# Switch to non-root user for security -USER appuser +# # Switch to non-root user for security +# USER appuser EXPOSE 5173 diff --git a/frontend/src/app/api/screenshot/route.ts b/frontend/src/app/api/screenshot/route.ts index 25cc91fe..60c0977d 100644 --- a/frontend/src/app/api/screenshot/route.ts +++ b/frontend/src/app/api/screenshot/route.ts @@ -14,6 +14,8 @@ async function getBrowser(): Promise { protocolTimeout: 240000, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); + } else { + logger.info('Reusing existing browser instance...'); } return browserInstance; } @@ -24,24 +26,27 @@ export async function GET(req: Request) { let page = null; if (!url) { + logger.warn('No URL provided in query parameters'); return NextResponse.json( { error: 'URL parameter is required' }, { status: 400 } ); } + logger.info(`Starting screenshot for URL: ${url}`); + try { // Get browser instance const browser = await getBrowser(); + logger.info('Browser instance acquired'); // Create a new page page = await browser.newPage(); + logger.info('New page created'); - // Set viewport to a reasonable size - await page.setViewport({ - width: 1600, - height: 900, - }); + // Set viewport + await page.setViewport({ width: 1600, height: 900 }); + logger.info('Viewport set to 1600x900'); // Navigate to URL with increased timeout and more reliable wait condition await page.goto(url, { @@ -72,13 +77,12 @@ export async function GET(req: Request) { type: 'png', fullPage: true, }); + logger.info('Screenshot captured'); - // Always close the page when done - if (page) { - await page.close(); - } + // Clean up + if (page) await page.close(); + logger.info('Page closed'); - // Return the screenshot as a PNG image return new Response(screenshot, { headers: { 'Content-Type': 'image/png', @@ -88,24 +92,24 @@ export async function GET(req: Request) { } catch (error: any) { logger.error('Screenshot error:', error); - // Ensure page is closed even if an error occurs if (page) { try { await page.close(); + logger.info('Closed page after error'); } catch (closeError) { logger.error('Error closing page:', closeError); } } - // If browser seems to be in a bad state, recreate it if ( - error.message.includes('Target closed') || - error.message.includes('Protocol error') || - error.message.includes('Target.createTarget') + error.message?.includes('Target closed') || + error.message?.includes('Protocol error') || + error.message?.includes('Target.createTarget') ) { try { if (browserInstance) { await browserInstance.close(); + logger.warn('Browser instance was closed due to protocol error'); browserInstance = null; } } catch (closeBrowserError) { @@ -120,12 +124,12 @@ export async function GET(req: Request) { } } -// Handle process termination to close browser +// Gracefully close the browser when the process exits process.on('SIGINT', async () => { if (browserInstance) { - logger.info('Closing browser instance...'); + logger.info('SIGINT received. Closing browser instance...'); await browserInstance.close(); browserInstance = null; } process.exit(0); -}); \ No newline at end of file +}); From a3376d84d10ef8d5e0bdce1ab95919c28d137314 Mon Sep 17 00:00:00 2001 From: pengyu Date: Sun, 13 Apr 2025 10:52:12 -0400 Subject: [PATCH 15/19] worked screenshot --- frontend/src/app/api/media/[...path]/route.ts | 22 ++++-- frontend/src/app/media/[...path]/route.ts | 72 +++++++++++++++++++ .../chat/code-engine/project-context.tsx | 4 ++ 3 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/media/[...path]/route.ts diff --git a/frontend/src/app/api/media/[...path]/route.ts b/frontend/src/app/api/media/[...path]/route.ts index 54619efd..5e1b059b 100644 --- a/frontend/src/app/api/media/[...path]/route.ts +++ b/frontend/src/app/api/media/[...path]/route.ts @@ -4,21 +4,26 @@ import path from 'path'; import { getMediaDir } from 'codefox-common'; import { logger } from '@/app/log/logger'; + export async function GET( request: NextRequest, { params }: { params: { path: string[] } } ) { try { const mediaDir = getMediaDir(); + logger.info(`📁 getMediaDir = ${mediaDir}`); const filePath = path.join(mediaDir, ...params.path); const normalizedPath = path.normalize(filePath); + logger.info(`📁 getMediaDir = ${mediaDir}`); +logger.info(`📂 full filePath = ${filePath}`); + logger.debug(`Requested path: ${params.path.join('/')}`); + logger.debug(`Full resolved path: ${filePath}`); if (!normalizedPath.startsWith(mediaDir)) { - logger.error('Possible directory traversal attempt:', filePath); + logger.warn('⛔ Directory traversal attempt blocked:', filePath); return new Response('Access denied', { status: 403 }); } - // File extension allowlist const contentTypeMap: Record = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', @@ -27,25 +32,28 @@ export async function GET( }; const ext = path.extname(filePath).toLowerCase(); + logger.debug(`File extension: ${ext}`); if (!contentTypeMap[ext]) { + logger.warn(`⛔ Forbidden file type: ${ext}`); return new Response('Forbidden file type', { status: 403 }); } - // File existence and size check let fileStat; try { fileStat = await fs.stat(filePath); } catch (err) { + logger.warn(`❌ File not found at path: ${filePath}`); return new Response('File not found', { status: 404 }); } if (fileStat.size > 10 * 1024 * 1024) { - // 10MB limit + logger.warn(`📦 File too large (${fileStat.size} bytes): ${filePath}`); return new Response('File too large', { status: 413 }); } - // Read and return the file const fileBuffer = await fs.readFile(filePath); + logger.info(`✅ Serving file: ${filePath}`); + return new Response(fileBuffer, { headers: { 'Content-Type': contentTypeMap[ext], @@ -53,8 +61,8 @@ export async function GET( 'Cache-Control': 'public, max-age=31536000', }, }); - } catch (error) { - logger.error('Error serving media file:', error); + } catch (error: any) { + logger.error('🔥 Error serving media file:', error); const errorMessage = process.env.NODE_ENV === 'development' ? `Error serving file: ${error.message}` diff --git a/frontend/src/app/media/[...path]/route.ts b/frontend/src/app/media/[...path]/route.ts new file mode 100644 index 00000000..857aabdb --- /dev/null +++ b/frontend/src/app/media/[...path]/route.ts @@ -0,0 +1,72 @@ +import { NextRequest } from 'next/server'; +import fs from 'fs/promises'; // Use promises API +import path from 'path'; +import { getMediaDir } from 'codefox-common'; +import { logger } from '@/app/log/logger'; + +export async function GET( + request: NextRequest, + { params }: { params: { path: string[] } } +) { + try { + const mediaDir = getMediaDir(); + logger.info(`📁 getMediaDir = ${mediaDir}`); + const filePath = path.join(mediaDir, ...params.path); + const normalizedPath = path.normalize(filePath); + logger.info(`📁 getMediaDir = ${mediaDir}`); + logger.info(`📂 full filePath = ${filePath}`); + logger.debug(`Requested path: ${params.path.join('/')}`); + logger.debug(`Full resolved path: ${filePath}`); + + if (!normalizedPath.startsWith(mediaDir)) { + logger.warn('⛔ Directory traversal attempt blocked:', filePath); + return new Response('Access denied', { status: 403 }); + } + + const contentTypeMap: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.webp': 'image/webp', + }; + + const ext = path.extname(filePath).toLowerCase(); + logger.debug(`File extension: ${ext}`); + if (!contentTypeMap[ext]) { + logger.warn(`⛔ Forbidden file type: ${ext}`); + return new Response('Forbidden file type', { status: 403 }); + } + + let fileStat; + try { + fileStat = await fs.stat(filePath); + } catch (err) { + logger.warn(`❌ File not found at path: ${filePath}`); + return new Response('File not found', { status: 404 }); + } + + if (fileStat.size > 10 * 1024 * 1024) { + logger.warn(`📦 File too large (${fileStat.size} bytes): ${filePath}`); + return new Response('File too large', { status: 413 }); + } + + const fileBuffer = await fs.readFile(filePath); + logger.info(`✅ Serving file: ${filePath}`); + + return new Response(fileBuffer, { + headers: { + 'Content-Type': contentTypeMap[ext], + 'X-Content-Type-Options': 'nosniff', + 'Cache-Control': 'public, max-age=31536000', + }, + }); + } catch (error: any) { + logger.error('🔥 Error serving media file:', error); + const errorMessage = + process.env.NODE_ENV === 'development' + ? `Error serving file: ${error.message}` + : 'An error occurred while serving the file'; + + return new Response(errorMessage, { status: 500 }); + } +} diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index 82ae6409..163c401d 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -624,6 +624,10 @@ export function ProjectProvider({ children }: { children: ReactNode }) { logger.debug(`[screenshot] Sending request to ${screenshotUrl}`); const screenshotResponse = await fetch(screenshotUrl); + // 添加响应头调试 + logger.debug(`[screenshot] Response status: ${screenshotResponse.status}`); + logger.debug(`[screenshot] Response content-type: ${screenshotResponse.headers.get('content-type')}`); + if (!screenshotResponse.ok) { throw new Error( `[screenshot] Failed to capture: ${screenshotResponse.status} ${screenshotResponse.statusText}` From ee918f27ddcddf8ba0657efe6b344ab4eda70dce Mon Sep 17 00:00:00 2001 From: pengyu Date: Mon, 14 Apr 2025 00:08:57 -0400 Subject: [PATCH 16/19] fix the avatar --- frontend/src/components/avatar-uploader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/avatar-uploader.tsx b/frontend/src/components/avatar-uploader.tsx index 8dae464c..0baeefeb 100644 --- a/frontend/src/components/avatar-uploader.tsx +++ b/frontend/src/components/avatar-uploader.tsx @@ -29,7 +29,7 @@ export function normalizeAvatarUrl( // Handle paths that might not have the media/ prefix if (avatarUrl.includes('avatars/')) { const parts = avatarUrl.split('avatars/'); - return `/api/media/avatars/${parts[parts.length - 1]}`; + return `/media/avatars/${parts[parts.length - 1]}`; } // Return as is for other cases From 3961c14ed9e38420b9e248e3515c17ec271de72b Mon Sep 17 00:00:00 2001 From: pengyu Date: Mon, 14 Apr 2025 01:31:46 -0400 Subject: [PATCH 17/19] modify the preview --- frontend/src/app/api/media/[...path]/route.ts | 73 ------------------- frontend/src/app/media/[...path]/route.ts | 3 +- frontend/src/components/root/expand-card.tsx | 8 +- .../src/components/root/projects-section.tsx | 1 + 4 files changed, 7 insertions(+), 78 deletions(-) delete mode 100644 frontend/src/app/api/media/[...path]/route.ts diff --git a/frontend/src/app/api/media/[...path]/route.ts b/frontend/src/app/api/media/[...path]/route.ts deleted file mode 100644 index 5e1b059b..00000000 --- a/frontend/src/app/api/media/[...path]/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextRequest } from 'next/server'; -import fs from 'fs/promises'; // Use promises API -import path from 'path'; -import { getMediaDir } from 'codefox-common'; -import { logger } from '@/app/log/logger'; - - -export async function GET( - request: NextRequest, - { params }: { params: { path: string[] } } -) { - try { - const mediaDir = getMediaDir(); - logger.info(`📁 getMediaDir = ${mediaDir}`); - const filePath = path.join(mediaDir, ...params.path); - const normalizedPath = path.normalize(filePath); - logger.info(`📁 getMediaDir = ${mediaDir}`); -logger.info(`📂 full filePath = ${filePath}`); - logger.debug(`Requested path: ${params.path.join('/')}`); - logger.debug(`Full resolved path: ${filePath}`); - - if (!normalizedPath.startsWith(mediaDir)) { - logger.warn('⛔ Directory traversal attempt blocked:', filePath); - return new Response('Access denied', { status: 403 }); - } - - const contentTypeMap: Record = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.webp': 'image/webp', - }; - - const ext = path.extname(filePath).toLowerCase(); - logger.debug(`File extension: ${ext}`); - if (!contentTypeMap[ext]) { - logger.warn(`⛔ Forbidden file type: ${ext}`); - return new Response('Forbidden file type', { status: 403 }); - } - - let fileStat; - try { - fileStat = await fs.stat(filePath); - } catch (err) { - logger.warn(`❌ File not found at path: ${filePath}`); - return new Response('File not found', { status: 404 }); - } - - if (fileStat.size > 10 * 1024 * 1024) { - logger.warn(`📦 File too large (${fileStat.size} bytes): ${filePath}`); - return new Response('File too large', { status: 413 }); - } - - const fileBuffer = await fs.readFile(filePath); - logger.info(`✅ Serving file: ${filePath}`); - - return new Response(fileBuffer, { - headers: { - 'Content-Type': contentTypeMap[ext], - 'X-Content-Type-Options': 'nosniff', - 'Cache-Control': 'public, max-age=31536000', - }, - }); - } catch (error: any) { - logger.error('🔥 Error serving media file:', error); - const errorMessage = - process.env.NODE_ENV === 'development' - ? `Error serving file: ${error.message}` - : 'An error occurred while serving the file'; - - return new Response(errorMessage, { status: 500 }); - } -} diff --git a/frontend/src/app/media/[...path]/route.ts b/frontend/src/app/media/[...path]/route.ts index 857aabdb..5e1b059b 100644 --- a/frontend/src/app/media/[...path]/route.ts +++ b/frontend/src/app/media/[...path]/route.ts @@ -4,6 +4,7 @@ import path from 'path'; import { getMediaDir } from 'codefox-common'; import { logger } from '@/app/log/logger'; + export async function GET( request: NextRequest, { params }: { params: { path: string[] } } @@ -14,7 +15,7 @@ export async function GET( const filePath = path.join(mediaDir, ...params.path); const normalizedPath = path.normalize(filePath); logger.info(`📁 getMediaDir = ${mediaDir}`); - logger.info(`📂 full filePath = ${filePath}`); +logger.info(`📂 full filePath = ${filePath}`); logger.debug(`Requested path: ${params.path.join('/')}`); logger.debug(`Full resolved path: ${filePath}`); diff --git a/frontend/src/components/root/expand-card.tsx b/frontend/src/components/root/expand-card.tsx index 613a4833..18fe575e 100644 --- a/frontend/src/components/root/expand-card.tsx +++ b/frontend/src/components/root/expand-card.tsx @@ -9,7 +9,7 @@ import { URL_PROTOCOL_PREFIX } from '@/utils/const'; import { logger } from '@/app/log/logger'; import { Button } from '@/components/ui/button'; -export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { +export function ExpandableCard({ projects, isGenerating = false, onOpenChat, isCommunityProject = false }) { const [active, setActive] = useState(null); const [iframeUrl, setIframeUrl] = useState(''); const ref = useRef(null); @@ -33,9 +33,10 @@ export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { }, [active]); const handleCardClick = async (project) => { - if (onOpenChat) { + if (isCommunityProject) { + setActive(project); + } else if (onOpenChat) { onOpenChat(); - return; } }; @@ -143,7 +144,6 @@ export function ExpandableCard({ projects, isGenerating = false, onOpenChat }) { className="font-medium text-gray-900 dark:text-gray-100 flex items-center text-sm truncate" > {project.name} - handleOpenChat(project.id)} + isCommunityProject={view === 'community'} /> ))}
From 59f86b0d7e32904a893a429542fdfa64c456da87 Mon Sep 17 00:00:00 2001 From: pengyu Date: Fri, 18 Apr 2025 12:37:17 -0400 Subject: [PATCH 18/19] clear chinese comment and update model --- .../test.frontend-code-generate.spec.ts | 2 +- backend/src/build-system/context.ts | 2 +- .../frontend-code-generate/CodeReview.ts | 4 +- .../handlers/frontend-code-generate/index.ts | 4 +- .../handlers/ux/uiux-layout/index.ts | 2 +- backend/src/user/user.model.ts | 2 +- backend/src/user/user.service.ts | 66 ++++++++++++++++ frontend/src/app/api/screenshot/route.ts | 39 ++++++---- frontend/src/app/globals.css | 2 +- frontend/src/app/media/[...path]/route.ts | 3 +- .../chat/code-engine/code-engine.tsx | 2 +- .../chat/code-engine/file-structure.tsx | 14 ++-- .../chat/code-engine/project-context.tsx | 75 ++++++++++++------- .../components/chat/code-engine/web-view.tsx | 4 +- .../src/components/global-project-poller.tsx | 62 ++++++++++----- frontend/src/components/modal.tsx | 22 +++--- .../src/components/root/projects-section.tsx | 39 +++++++--- frontend/src/components/sidebar-item.tsx | 2 +- frontend/src/graphql/request.ts | 1 - frontend/src/hooks/useChatList.ts | 4 +- frontend/src/providers/AuthProvider.tsx | 12 +-- 21 files changed, 250 insertions(+), 113 deletions(-) diff --git a/backend/src/build-system/__tests__/test.frontend-code-generate.spec.ts b/backend/src/build-system/__tests__/test.frontend-code-generate.spec.ts index 58ab32e2..bc425088 100644 --- a/backend/src/build-system/__tests__/test.frontend-code-generate.spec.ts +++ b/backend/src/build-system/__tests__/test.frontend-code-generate.spec.ts @@ -12,7 +12,7 @@ describe('FrontendCodeHandler', () => { name: 'Spotify-like Music Web', description: 'Users can play music', databaseType: 'SQLite', - model: 'o3-mini-high', + model: 'o4-mini', nodes: [ { handler: FrontendCodeHandler, diff --git a/backend/src/build-system/context.ts b/backend/src/build-system/context.ts index 95ae370d..e4006fb7 100644 --- a/backend/src/build-system/context.ts +++ b/backend/src/build-system/context.ts @@ -115,7 +115,7 @@ export class BuilderContext { this.globalContext.set('projectSize', 'small'); break; case 'gpt-4o': - case 'o3-mini-high': + case 'o4-mini': this.globalContext.set('projectSize', 'medium'); break; default: diff --git a/backend/src/build-system/handlers/frontend-code-generate/CodeReview.ts b/backend/src/build-system/handlers/frontend-code-generate/CodeReview.ts index 8f6f1396..4a01ea81 100644 --- a/backend/src/build-system/handlers/frontend-code-generate/CodeReview.ts +++ b/backend/src/build-system/handlers/frontend-code-generate/CodeReview.ts @@ -198,7 +198,7 @@ export class FrontendQueueProcessor { let fixResponse = await chatSyncWithClocker( this.context, { - model: 'o3-mini-high', + model: 'o4-mini', messages: [ { role: 'system', content: fixPrompt }, { @@ -270,7 +270,7 @@ export class FrontendQueueProcessor { fixResponse = await chatSyncWithClocker( this.context, { - model: 'o3-mini-high', + model: 'o4-mini', messages: [ { role: 'system', content: fixPrompt }, { diff --git a/backend/src/build-system/handlers/frontend-code-generate/index.ts b/backend/src/build-system/handlers/frontend-code-generate/index.ts index 502c568f..8f3b943b 100644 --- a/backend/src/build-system/handlers/frontend-code-generate/index.ts +++ b/backend/src/build-system/handlers/frontend-code-generate/index.ts @@ -366,8 +366,8 @@ export class FrontendCodeHandler implements BuildHandler { context, { model: isSPAFlag - ? 'claude-3.7-sonnet' // Use Claude for SPAs - : 'o3-mini-high', // Use default or fallback for non-SPAs + ? 'gpt-4o-mini' // Use Claude for SPAs + : 'o4-mini', // Use default or fallback for non-SPAs messages, }, 'generate frontend code', diff --git a/backend/src/build-system/handlers/ux/uiux-layout/index.ts b/backend/src/build-system/handlers/ux/uiux-layout/index.ts index acbe275d..f694734c 100644 --- a/backend/src/build-system/handlers/ux/uiux-layout/index.ts +++ b/backend/src/build-system/handlers/ux/uiux-layout/index.ts @@ -75,7 +75,7 @@ export class UIUXLayoutHandler implements BuildHandler { context, { // model: context.defaultModel || 'gpt-4o-mini', - model: 'claude-3.7-sonnet', + model: 'gpt-4o-mini', messages, }, 'generateUIUXLayout', diff --git a/backend/src/user/user.model.ts b/backend/src/user/user.model.ts index 8267f03e..ebb92550 100644 --- a/backend/src/user/user.model.ts +++ b/backend/src/user/user.model.ts @@ -25,7 +25,7 @@ export class User extends SystemBaseModel { googleId: string; @Field() - @Column() + @Column({ unique: true }) username: string; @Column({ nullable: true }) // Made nullable for OAuth users diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 6257e652..0ea9293e 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ConflictException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -107,4 +108,69 @@ export class UserService { return true; } + + /** + * Checks if a username already exists in the database + * @param username Username to check + * @param excludeUserId Optional user ID to exclude from the check (for updates) + * @returns Boolean indicating if the username exists + */ + async isUsernameExists( + username: string, + excludeUserId?: string, + ): Promise { + const query = this.userRepository + .createQueryBuilder('user') + .where('LOWER(user.username) = LOWER(:username)', { + username: username.toLowerCase(), + }); + + if (excludeUserId) { + query.andWhere('user.id != :userId', { userId: excludeUserId }); + } + + const count = await query.getCount(); + return count > 0; + } + + /** + * Updates a user's username with uniqueness validation + * @param userId User ID + * @param newUsername New username to set + * @returns Updated user object + */ + async updateUsername(userId: string, newUsername: string): Promise { + if (!newUsername || newUsername.trim().length < 3) { + throw new BadRequestException( + 'Username must be at least 3 characters long', + ); + } + + // Check if the username is already taken + const exists = await this.isUsernameExists(newUsername, userId); + if (exists) { + throw new ConflictException( + `Username '${newUsername}' is already taken. Please choose another one.`, + ); + } + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new NotFoundException('User not found'); + } + + user.username = newUsername; + + try { + return await this.userRepository.save(user); + } catch (error) { + // Check for unique constraint error (just in case of race condition) + if (error.code === '23505' || error.message.includes('duplicate')) { + throw new ConflictException( + `Username '${newUsername}' is already taken. Please choose another one.`, + ); + } + throw error; + } + } } diff --git a/frontend/src/app/api/screenshot/route.ts b/frontend/src/app/api/screenshot/route.ts index 60c0977d..bf7fc15b 100644 --- a/frontend/src/app/api/screenshot/route.ts +++ b/frontend/src/app/api/screenshot/route.ts @@ -33,20 +33,23 @@ export async function GET(req: Request) { ); } - logger.info(`Starting screenshot for URL: ${url}`); + logger.info(`[SCREENSHOT] Starting screenshot for URL: ${url}`); try { // Get browser instance + logger.info(`[SCREENSHOT] Attempting to get browser instance`); const browser = await getBrowser(); - logger.info('Browser instance acquired'); + logger.info(`[SCREENSHOT] Browser instance acquired successfully`); // Create a new page + logger.info(`[SCREENSHOT] Creating new page`); page = await browser.newPage(); - logger.info('New page created'); + logger.info(`[SCREENSHOT] New page created successfully`); // Set viewport + logger.info(`[SCREENSHOT] Setting viewport`); await page.setViewport({ width: 1600, height: 900 }); - logger.info('Viewport set to 1600x900'); + logger.info(`[SCREENSHOT] Viewport set successfully`); // Navigate to URL with increased timeout and more reliable wait condition await page.goto(url, { @@ -73,31 +76,40 @@ export async function GET(req: Request) { } // Take screenshot + logger.info(`[SCREENSHOT] Taking screenshot`); const screenshot = await page.screenshot({ type: 'png', fullPage: true, }); - logger.info('Screenshot captured'); + logger.info(`[SCREENSHOT] Screenshot captured successfully, size: ${screenshot.length} bytes`); // Clean up - if (page) await page.close(); - logger.info('Page closed'); + if (page) { + logger.info(`[SCREENSHOT] Closing page`); + await page.close(); + logger.info(`[SCREENSHOT] Page closed successfully`); + } + logger.info(`[SCREENSHOT] Returning screenshot response`); return new Response(screenshot, { headers: { 'Content-Type': 'image/png', - 'Cache-Control': 's-maxage=3600', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' }, }); } catch (error: any) { - logger.error('Screenshot error:', error); + logger.error(`[SCREENSHOT] Error capturing screenshot: ${error.message}`, error); + logger.error(`[SCREENSHOT] Error stack: ${error.stack}`); if (page) { try { + logger.info(`[SCREENSHOT] Attempting to close page after error`); await page.close(); - logger.info('Closed page after error'); + logger.info(`[SCREENSHOT] Successfully closed page after error`); } catch (closeError) { - logger.error('Error closing page:', closeError); + logger.error(`[SCREENSHOT] Error closing page: ${closeError.message}`); } } @@ -108,12 +120,13 @@ export async function GET(req: Request) { ) { try { if (browserInstance) { + logger.warn(`[SCREENSHOT] Resetting browser instance due to protocol error`); await browserInstance.close(); - logger.warn('Browser instance was closed due to protocol error'); browserInstance = null; + logger.warn(`[SCREENSHOT] Browser instance reset successfully`); } } catch (closeBrowserError) { - logger.error('Error closing browser:', closeBrowserError); + logger.error(`[SCREENSHOT] Error closing browser: ${closeBrowserError.message}`); } } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 0b10650f..4ee744a5 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -82,7 +82,7 @@ height: 1px; } -/* 修改滚动条颜色为主题色调 */ +/* Modify scrollbar color to match theme */ :root { --scrollbar-thumb-color: rgba( 99, diff --git a/frontend/src/app/media/[...path]/route.ts b/frontend/src/app/media/[...path]/route.ts index 5e1b059b..857aabdb 100644 --- a/frontend/src/app/media/[...path]/route.ts +++ b/frontend/src/app/media/[...path]/route.ts @@ -4,7 +4,6 @@ import path from 'path'; import { getMediaDir } from 'codefox-common'; import { logger } from '@/app/log/logger'; - export async function GET( request: NextRequest, { params }: { params: { path: string[] } } @@ -15,7 +14,7 @@ export async function GET( const filePath = path.join(mediaDir, ...params.path); const normalizedPath = path.normalize(filePath); logger.info(`📁 getMediaDir = ${mediaDir}`); -logger.info(`📂 full filePath = ${filePath}`); + logger.info(`📂 full filePath = ${filePath}`); logger.debug(`Requested path: ${params.path.join('/')}`); logger.debug(`Full resolved path: ${filePath}`); diff --git a/frontend/src/components/chat/code-engine/code-engine.tsx b/frontend/src/components/chat/code-engine/code-engine.tsx index 634236f0..7c555126 100644 --- a/frontend/src/components/chat/code-engine/code-engine.tsx +++ b/frontend/src/components/chat/code-engine/code-engine.tsx @@ -371,7 +371,7 @@ export function CodeEngine({ return () => interval && clearInterval(interval); }, [timerActive]); - // 获取带用户ID的localStorage键 + // Get localStorage key with user ID const getUserStorageKey = (key: string) => { return user?.id ? `${key}_${user.id}` : key; }; diff --git a/frontend/src/components/chat/code-engine/file-structure.tsx b/frontend/src/components/chat/code-engine/file-structure.tsx index dcddb982..1232a427 100644 --- a/frontend/src/components/chat/code-engine/file-structure.tsx +++ b/frontend/src/components/chat/code-engine/file-structure.tsx @@ -36,11 +36,11 @@ export default function FileStructure({ })) ); - // 判断是否显示加载状态 + // Check if loading state should be displayed const isEmpty = Object.keys(data).length === 0; const showLoading = isLoading || isEmpty; - // 当数据变化时更新数据提供者 + // Update data provider when data changes useEffect(() => { if (!isEmpty) { setDataProvider( @@ -52,27 +52,27 @@ export default function FileStructure({ } }, [data, isEmpty]); - // 处理选择文件事件 + // Handle file selection event const handleSelectItems = (items) => { if (items.length > 0) { const newPath = items[0].toString().replace(/^root\//, ''); const selectedItem = data[items[0]]; - // 只有当选择的是文件时才设置文件路径 + // Only set file path when a file (not folder) is selected if (selectedItem && !selectedItem.isFolder) { onFileSelect?.(newPath); } } }; - // 根据文件路径获取要展开的文件夹 + // Get expanded folders based on current file path const getExpandedFolders = () => { if (!filePath) return ['root']; const parts = filePath.split('/'); const expandedFolders = ['root']; - // 逐级构建路径 + // Build path incrementally for (let i = 0; i < parts.length - 1; i++) { const folderPath = parts.slice(0, i + 1).join('/'); expandedFolders.push(`root/${folderPath}`); @@ -100,7 +100,7 @@ export default function FileStructure({ dataProvider={dataProvider} getItemTitle={(item) => item.data} viewState={{ - // 展开包含当前文件的目录 + // Expand directories containing the current file ['fileTree']: { expandedItems: getExpandedFolders(), }, diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index 163c401d..915291f5 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -46,7 +46,7 @@ export interface ProjectContextType { isLoading: boolean; getWebUrl: ( projectPath: string - ) => Promise<{ domain: string; containerId: string }>; + ) => Promise<{ domain: string; containerId: string; port?: string }>; takeProjectScreenshot: (projectId: string, url: string) => Promise; refreshProjects: () => Promise; editorRef?: React.MutableRefObject; @@ -117,12 +117,12 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [filePath, setFilePath] = useState(null); const [isLoading, setIsLoading] = useState(false); const editorRef = useRef(null); - - // 获取带用户ID的localStorage键 + + // Get localStorage key with user ID const getUserStorageKey = (key: string) => { return user?.id ? `${key}_${user.id}` : key; }; - + const [pendingProjects, setPendingProjects] = useState(() => { if (typeof window !== 'undefined' && user?.id) { try { @@ -136,7 +136,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { } return []; }); - + const setRecentlyCompletedProjectId = (id: string | null) => { if (typeof window !== 'undefined' && user?.id) { if (id) { @@ -154,17 +154,20 @@ export function ProjectProvider({ children }: { children: ReactNode }) { ? localStorage.getItem(getUserStorageKey('pendingChatId')) : null ); - + useEffect(() => { if (typeof window === 'undefined' || !user?.id) return; try { - localStorage.setItem(getUserStorageKey('pendingProjects'), JSON.stringify(pendingProjects)); + localStorage.setItem( + getUserStorageKey('pendingProjects'), + JSON.stringify(pendingProjects) + ); } catch (e) { logger.warn('Failed to store pendingProjects in localStorage'); } }, [pendingProjects, user?.id]); - - // setter:更新 state + localStorage + + // Setter: Update state + localStorage const setTempLoadingProjectId = (id: string | null) => { if (typeof window !== 'undefined' && user?.id) { if (id) { @@ -175,7 +178,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { } setTempLoadingProjectIdRaw(id); }; - + const [chatId, setChatId] = useState(null); const [pollTime, setPollTime] = useState(Date.now()); const [isCreateButtonClicked, setIsCreateButtonClicked] = useState(false); @@ -605,38 +608,48 @@ export function ProjectProvider({ children }: { children: ReactNode }) { async (projectId: string, url: string): Promise => { const operationKey = `screenshot_${projectId}`; if (pendingOperations.current.get(operationKey)) { - logger.debug(`[screenshot] Project ${projectId} is already being processed`); + logger.debug( + `[screenshot] Project ${projectId} is already being processed` + ); return; } - + pendingOperations.current.set(operationKey, true); logger.debug(`[screenshot] Start for Project ${projectId}, URL: ${url}`); - + try { logger.debug(`[screenshot] Checking accessibility for ${url}`); const isUrlAccessible = await checkUrlStatus(url); if (!isUrlAccessible) { - logger.warn(`[screenshot] URL ${url} is not accessible after retries`); + logger.warn( + `[screenshot] URL ${url} is not accessible after retries` + ); return; } - + const screenshotUrl = `/api/screenshot?url=${encodeURIComponent(url)}&t=${Date.now()}`; logger.debug(`[screenshot] Sending request to ${screenshotUrl}`); const screenshotResponse = await fetch(screenshotUrl); - + // 添加响应头调试 - logger.debug(`[screenshot] Response status: ${screenshotResponse.status}`); - logger.debug(`[screenshot] Response content-type: ${screenshotResponse.headers.get('content-type')}`); - + logger.debug( + `[screenshot] Response status: ${screenshotResponse.status}` + ); + logger.debug( + `[screenshot] Response content-type: ${screenshotResponse.headers.get('content-type')}` + ); + if (!screenshotResponse.ok) { throw new Error( `[screenshot] Failed to capture: ${screenshotResponse.status} ${screenshotResponse.statusText}` ); } - + const arrayBuffer = await screenshotResponse.arrayBuffer(); - logger.debug(`[screenshot] Screenshot captured for Project ${projectId}, uploading...`); - + logger.debug( + `[screenshot] Screenshot captured for Project ${projectId}, uploading...` + ); + const blob = new Blob([arrayBuffer], { type: 'image/png' }); const file = new File([blob], 'screenshot.png', { type: 'image/png' }); @@ -657,12 +670,11 @@ export function ProjectProvider({ children }: { children: ReactNode }) { }, [updateProjectPhotoMutation] ); - const getWebUrl = useCallback( async ( projectPath: string - ): Promise<{ domain: string; containerId: string }> => { + ): Promise<{ domain: string; containerId: string; port?: string }> => { // Check if this operation is already in progress const operationKey = `getWebUrl_${projectPath}`; if (pendingOperations.current.get(operationKey)) { @@ -704,6 +716,13 @@ export function ProjectProvider({ children }: { children: ReactNode }) { ); } + // Extract port from domain if it's in the format domain:port + let port: string | undefined = undefined; + const domainParts = data.domain.split(':'); + if (domainParts.length > 1) { + port = domainParts[domainParts.length - 1]; + } + const baseUrl = `${URL_PROTOCOL_PREFIX}://${data.domain}`; // Find project and take screenshot if needed @@ -718,6 +737,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { return { domain: data.domain, containerId: data.containerId, + port: port, // Include port in the returned object }; } catch (error) { logger.error('Error getting web URL:', error); @@ -812,7 +832,10 @@ export function ProjectProvider({ children }: { children: ReactNode }) { if (createdChat?.id) { setChatId(createdChat.id); setIsCreateButtonClicked(true); - localStorage.setItem(getUserStorageKey('pendingChatId'), createdChat.id); + localStorage.setItem( + getUserStorageKey('pendingChatId'), + createdChat.id + ); setTempLoadingProjectId(createdChat.id); return createdChat.id; } else { @@ -1027,7 +1050,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { useEffect(() => { const interval = setInterval(() => { - setPollTime(Date.now()); // 每6秒更新时间,触发下面的 useEffect + setPollTime(Date.now()); // Update time every 6 seconds to trigger the useEffect below }, 6000); return () => clearInterval(interval); diff --git a/frontend/src/components/chat/code-engine/web-view.tsx b/frontend/src/components/chat/code-engine/web-view.tsx index 81700d7f..c060c91b 100644 --- a/frontend/src/components/chat/code-engine/web-view.tsx +++ b/frontend/src/components/chat/code-engine/web-view.tsx @@ -274,11 +274,11 @@ function PreviewContent({ }; const zoomIn = () => { - setScale((prevScale) => Math.min(prevScale + 0.1, 2)); // 最大缩放比例为 2 + setScale((prevScale) => Math.min(prevScale + 0.1, 2)); // Maximum zoom scale is 2 }; const zoomOut = () => { - setScale((prevScale) => Math.max(prevScale - 0.1, 0.5)); // 最小缩放比例为 0.5 + setScale((prevScale) => Math.max(prevScale - 0.1, 0.5)); // Minimum zoom scale is 0.5 }; return ( diff --git a/frontend/src/components/global-project-poller.tsx b/frontend/src/components/global-project-poller.tsx index b9ad6a9c..0528ea69 100644 --- a/frontend/src/components/global-project-poller.tsx +++ b/frontend/src/components/global-project-poller.tsx @@ -40,7 +40,7 @@ const GlobalToastListener = () => { refetchPublicProjects, setTempLoadingProjectId, getWebUrl, - takeProjectScreenshot + takeProjectScreenshot, } = useContext(ProjectContext); const router = useRouter(); const intervalRef = useRef(null); @@ -59,35 +59,48 @@ const GlobalToastListener = () => { await refreshProjects(); await refetchPublicProjects(); setTempLoadingProjectId(null); - - // 确保为项目截图 + + // Make sure it's for project screenshot try { if (project.id && project.projectPath) { - logger.info(`Taking screenshot for project ${project.id}`); - // 获取项目URL并进行截图 + logger.info( + `[PROJECT_POLLER] Taking screenshot for project ${project.id}` + ); + // Get project URL and take screenshot const { domain, port } = await getWebUrl(project.projectPath); - - // 使用端口直接访问 + + // Access directly using port let baseUrl; if (port) { baseUrl = `${URL_PROTOCOL_PREFIX}://localhost:${port}`; + logger.info( + `[PROJECT_POLLER] Using localhost URL with port: ${baseUrl}` + ); } else { baseUrl = `${URL_PROTOCOL_PREFIX}://${domain}`; + logger.info(`[PROJECT_POLLER] Using domain URL: ${baseUrl}`); } - - logger.info(`Using URL for screenshot: ${baseUrl}`); - - // 等待5秒钟让服务完全启动 - logger.info(`Waiting for service to fully start before taking screenshot for project ${project.id}`); - await new Promise(resolve => setTimeout(resolve, 5000)); - - await takeProjectScreenshot(project.id, baseUrl); - logger.info(`Screenshot taken for project ${project.id}`); + + logger.info( + `[PROJECT_POLLER] Waiting for service to fully start before taking screenshot` + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); // Increase wait time to 10 seconds + logger.info( + `[PROJECT_POLLER] Wait completed, proceeding with screenshot` + ); + + const result = await takeProjectScreenshot(project.id, baseUrl); + logger.info( + `[PROJECT_POLLER] Screenshot taken for project ${project.id}, result: ${JSON.stringify(result)}` + ); } } catch (screenshotError) { - logger.error('Error taking project screenshot:', screenshotError); + logger.error( + `[PROJECT_POLLER] Error taking project screenshot: ${screenshotError.message}`, + screenshotError + ); } - + toast.custom( (t) => ( { return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; - }, [recentlyCompletedProjectId, pollChatProject, refreshProjects, refetchPublicProjects, - setTempLoadingProjectId, getWebUrl, takeProjectScreenshot, router, setChatId]); + }, [ + recentlyCompletedProjectId, + pollChatProject, + refreshProjects, + refetchPublicProjects, + setTempLoadingProjectId, + getWebUrl, + takeProjectScreenshot, + router, + setChatId, + ]); return null; }; diff --git a/frontend/src/components/modal.tsx b/frontend/src/components/modal.tsx index 4ba66712..04509416 100644 --- a/frontend/src/components/modal.tsx +++ b/frontend/src/components/modal.tsx @@ -19,17 +19,17 @@ export function StableModal({ children, className = '', }: ModalProps) { - // 使用ref跟踪模态框是否已挂载 + // Use ref to track if modal is mounted const modalRoot = useRef(null); - // 处理点击外部区域关闭 + // Handle clicks outside to close const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; - // 处理ESC键关闭 + // Handle ESC key to close useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (isOpen && e.key === 'Escape') { @@ -41,7 +41,7 @@ export function StableModal({ return () => window.removeEventListener('keydown', handleEsc); }, [isOpen, onClose]); - // 使用useEffect获取或创建portal容器 + // Use useEffect to get or create portal container useEffect(() => { if (typeof window !== 'undefined') { let element = document.getElementById('modal-root'); @@ -54,7 +54,7 @@ export function StableModal({ modalRoot.current = element; - // 在组件卸载时清理 + // Clean up when component unmounts return () => { if (element && element.childNodes.length === 0) { document.body.removeChild(element); @@ -63,17 +63,17 @@ export function StableModal({ } }, []); - // 在服务器端渲染时,不渲染任何内容 + // Don't render anything on server side if (typeof window === 'undefined' || !modalRoot.current) { return null; } - // 使用createPortal将模态框内容渲染到body末尾 + // Use createPortal to render modal content at the end of body return createPortal( {isOpen && (
- {/* 背景遮罩 */} + {/* Background overlay */} - {/* 模态框内容 */} + {/* Modal content */} - {/* 标题栏(如果提供) */} + {/* Title bar (if provided) */} {title && (

{title}

@@ -116,7 +116,7 @@ export function StableModal({
)} - {/* 内容区域 */} + {/* Content area */}
{children}
diff --git a/frontend/src/components/root/projects-section.tsx b/frontend/src/components/root/projects-section.tsx index 77887157..f8c0a709 100644 --- a/frontend/src/components/root/projects-section.tsx +++ b/frontend/src/components/root/projects-section.tsx @@ -34,7 +34,9 @@ export function ProjectsSection() { error: publicError, refetch: refetchPublic, } = useQuery(FETCH_PUBLIC_PROJECTS, { - variables: { input: { size: 100, strategy: 'latest', currentUserId: user?.id || '' } }, + variables: { + input: { size: 100, strategy: 'latest', currentUserId: user?.id || '' }, + }, fetchPolicy: 'network-only', }); @@ -56,7 +58,7 @@ export function ProjectsSection() { refetchUser(); refetchPublic(); // Clean up any deleted projects from pendingProjects - setPendingProjects((prev) => + setPendingProjects((prev) => prev.filter((p) => userProjects.some((up) => up.id === p.id)) ); }; @@ -97,7 +99,7 @@ export function ProjectsSection() { // Only add pending projects that are not in userProjects (not yet completed) pendingProjects - .filter(p => !userProjects.some(up => up.id === p.id)) + .filter((p) => !userProjects.some((up) => up.id === p.id)) .forEach((p) => map.set(p.id, { ...p, @@ -105,7 +107,7 @@ export function ProjectsSection() { createdAt: p.createdAt || new Date().toISOString(), }) ); - + // Add all user projects userProjects.forEach((p) => map.set(p.id, p)); @@ -115,7 +117,10 @@ export function ProjectsSection() { const displayProjects = view === 'my' ? mergedMyProjects : publicProjects; const transformedProjects = [...displayProjects] - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) .map((project) => ({ id: project.id, name: project.projectName || 'Untitled Project', @@ -129,11 +134,15 @@ export function ProjectsSection() { image: project.photoUrl || null, })); - // 添加临时生成中的项目 + // Add temporary generating projects const allProjects = [...transformedProjects]; - // 添加当前正在加载的项目(如果有且不在已有列表中) - if (view === 'my' && tempLoadingProjectId && !allProjects.some(p => p.id === tempLoadingProjectId)) { + // Add currently loading project (if exists and not already in the list) + if ( + view === 'my' && + tempLoadingProjectId && + !allProjects.some((p) => p.id === tempLoadingProjectId) + ) { allProjects.unshift({ id: tempLoadingProjectId, name: 'Generating Project...', @@ -146,17 +155,23 @@ export function ProjectsSection() { }); } - // 添加其他待处理项目 + // Add other pending projects if (view === 'my') { pendingProjects - .filter(p => !p.projectPath && p.id !== tempLoadingProjectId && !allProjects.some(proj => proj.id === p.id)) - .forEach(project => { + .filter( + (p) => + !p.projectPath && + p.id !== tempLoadingProjectId && + !allProjects.some((proj) => proj.id === p.id) + ) + .forEach((project) => { allProjects.unshift({ id: project.id, name: project.projectName || 'Generating Project...', path: '', isReady: false, - createDate: project.createdAt || new Date().toISOString().split('T')[0], + createDate: + project.createdAt || new Date().toISOString().split('T')[0], author: user?.username || 'Unknown', forkNum: 0, image: null, diff --git a/frontend/src/components/sidebar-item.tsx b/frontend/src/components/sidebar-item.tsx index c637f381..8ebe5b2c 100644 --- a/frontend/src/components/sidebar-item.tsx +++ b/frontend/src/components/sidebar-item.tsx @@ -105,7 +105,7 @@ function SideBarItemComponent({ await deleteProject({ variables: { projectId }, update: (cache) => { - // 清除项目缓存 + // Clear project cache cache.evict({ id: `Project:${projectId}` }); cache.gc(); } diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index 89d7d012..11a593f8 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -248,7 +248,6 @@ export const UPDATE_PROJECT_PUBLIC_STATUS = gql` updateProjectPublicStatus(projectId: $projectId, isPublic: $isPublic) { id projectName - path projectPackages { id content diff --git a/frontend/src/hooks/useChatList.ts b/frontend/src/hooks/useChatList.ts index 354228cf..f2a28517 100644 --- a/frontend/src/hooks/useChatList.ts +++ b/frontend/src/hooks/useChatList.ts @@ -26,7 +26,7 @@ export function useChatList() { setChatListUpdated(value); }, []); - // 监听用户变化和新聊天事件 + // Listen for user changes and new chat events useEffect(() => { const handleNewChat = () => { handleRefetch(); @@ -38,7 +38,7 @@ export function useChatList() { }; }, [handleRefetch]); - // 当用户ID变化时,强制刷新聊天列表 + // When the user ID changes, force refresh the chat list useEffect(() => { if (user?.id) { handleRefetch(); diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index 0552bcb6..8faba93c 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -139,16 +139,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { ); const logout = useCallback(() => { - // 清除当前用户的数据 + // Clear current user data if (user?.id) { - // 清除所有与当前用户相关的 localStorage 数据 + // Clear all localStorage data related to the current user localStorage.removeItem(`completedChatIds_${user.id}`); localStorage.removeItem(`pendingChatId_${user.id}`); localStorage.removeItem(`pendingProjects_${user.id}`); localStorage.removeItem(`tempLoadingProjectId_${user.id}`); localStorage.removeItem(`lastProjectId_${user.id}`); - - // 清除当前用户的所有项目完成状态 + + // Clear all project completion status for the current user for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(`project-completed-${user.id}-`)) { @@ -156,8 +156,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } } } - - // 清除认证相关的 localStorage + + // Clear authentication related localStorage setToken(null); setIsAuthorized(false); setUser(null); From 8f2af7c086c578ba07efdc3b055730a50ddb0fb3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:51:41 +0000 Subject: [PATCH 19/19] [autofix.ci] apply automated fixes --- backend/src/project/project.resolver.ts | 2 +- backend/src/project/project.service.ts | 4 +- frontend/src/app/api/runProject/route.ts | 2 +- frontend/src/app/api/screenshot/route.ts | 67 +++++++++++-------- .../chat/code-engine/code-engine.tsx | 10 ++- frontend/src/components/root/expand-card.tsx | 7 +- frontend/src/components/sidebar-item.tsx | 23 ++++--- frontend/src/graphql/request.ts | 2 +- 8 files changed, 72 insertions(+), 45 deletions(-) diff --git a/backend/src/project/project.resolver.ts b/backend/src/project/project.resolver.ts index d7fdf6d6..24b0dbed 100644 --- a/backend/src/project/project.resolver.ts +++ b/backend/src/project/project.resolver.ts @@ -103,7 +103,7 @@ export class ProjectsResolver { const { buffer, mimetype } = await validateAndBufferFile(file); // Call the service with the extracted buffer and mimetype - const project1= await this.projectService.updateProjectPhotoUrl( + const project1 = await this.projectService.updateProjectPhotoUrl( userId, projectId, buffer, diff --git a/backend/src/project/project.service.ts b/backend/src/project/project.service.ts index ad17897f..682537bd 100644 --- a/backend/src/project/project.service.ts +++ b/backend/src/project/project.service.ts @@ -7,7 +7,7 @@ import { ForbiddenException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Between, In, IsNull, Not, Repository } from 'typeorm'; +import { Between, In, Not, Repository } from 'typeorm'; import { Project } from './project.model'; import { ProjectPackages } from './project-packages.model'; import { @@ -857,4 +857,4 @@ export class ProjectService { project.isSyncedWithGitHub = true; return this.projectsRepository.save(project); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/runProject/route.ts b/frontend/src/app/api/runProject/route.ts index a9fb8751..79b286ff 100644 --- a/frontend/src/app/api/runProject/route.ts +++ b/frontend/src/app/api/runProject/route.ts @@ -563,4 +563,4 @@ export async function GET(req: Request) { } finally { processingRequests.delete(projectPath); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/screenshot/route.ts b/frontend/src/app/api/screenshot/route.ts index bf7fc15b..50237cd3 100644 --- a/frontend/src/app/api/screenshot/route.ts +++ b/frontend/src/app/api/screenshot/route.ts @@ -51,29 +51,31 @@ export async function GET(req: Request) { await page.setViewport({ width: 1600, height: 900 }); logger.info(`[SCREENSHOT] Viewport set successfully`); - // Navigate to URL with increased timeout and more reliable wait condition - await page.goto(url, { - waitUntil: 'networkidle2', // 更改为等待网络空闲状态,确保页面完全加载 - timeout: 90000, // 增加超时时间到90秒 - }); - - // 等待额外的时间让页面完全渲染 - await page.waitForTimeout(8000); // 增加等待时间到8秒 + // Navigate to URL with increased timeout and more reliable wait condition + await page.goto(url, { + waitUntil: 'networkidle2', // 更改为等待网络空闲状态,确保页面完全加载 + timeout: 90000, // 增加超时时间到90秒 + }); - // 尝试等待页面上的内容加载,如果失败也继续处理 - try { - // 等待页面上可能存在的主要内容元素 - await Promise.race([ - page.waitForSelector('main', { timeout: 5000 }), - page.waitForSelector('#root', { timeout: 5000 }), - page.waitForSelector('.app', { timeout: 5000 }), - page.waitForSelector('h1', { timeout: 5000 }), - page.waitForSelector('div', { timeout: 5000 }), // 添加更通用的选择器 - ]); - } catch (waitError) { - // 忽略等待选择器的错误,继续截图 - logger.info('Unable to find common page elements, continuing with screenshot'); - } + // 等待额外的时间让页面完全渲染 + await page.waitForTimeout(8000); // 增加等待时间到8秒 + + // 尝试等待页面上的内容加载,如果失败也继续处理 + try { + // 等待页面上可能存在的主要内容元素 + await Promise.race([ + page.waitForSelector('main', { timeout: 5000 }), + page.waitForSelector('#root', { timeout: 5000 }), + page.waitForSelector('.app', { timeout: 5000 }), + page.waitForSelector('h1', { timeout: 5000 }), + page.waitForSelector('div', { timeout: 5000 }), // 添加更通用的选择器 + ]); + } catch (waitError) { + // 忽略等待选择器的错误,继续截图 + logger.info( + 'Unable to find common page elements, continuing with screenshot' + ); + } // Take screenshot logger.info(`[SCREENSHOT] Taking screenshot`); @@ -81,7 +83,9 @@ export async function GET(req: Request) { type: 'png', fullPage: true, }); - logger.info(`[SCREENSHOT] Screenshot captured successfully, size: ${screenshot.length} bytes`); + logger.info( + `[SCREENSHOT] Screenshot captured successfully, size: ${screenshot.length} bytes` + ); // Clean up if (page) { @@ -95,12 +99,15 @@ export async function GET(req: Request) { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0' + Pragma: 'no-cache', + Expires: '0', }, }); } catch (error: any) { - logger.error(`[SCREENSHOT] Error capturing screenshot: ${error.message}`, error); + logger.error( + `[SCREENSHOT] Error capturing screenshot: ${error.message}`, + error + ); logger.error(`[SCREENSHOT] Error stack: ${error.stack}`); if (page) { @@ -120,13 +127,17 @@ export async function GET(req: Request) { ) { try { if (browserInstance) { - logger.warn(`[SCREENSHOT] Resetting browser instance due to protocol error`); + logger.warn( + `[SCREENSHOT] Resetting browser instance due to protocol error` + ); await browserInstance.close(); browserInstance = null; logger.warn(`[SCREENSHOT] Browser instance reset successfully`); } } catch (closeBrowserError) { - logger.error(`[SCREENSHOT] Error closing browser: ${closeBrowserError.message}`); + logger.error( + `[SCREENSHOT] Error closing browser: ${closeBrowserError.message}` + ); } } diff --git a/frontend/src/components/chat/code-engine/code-engine.tsx b/frontend/src/components/chat/code-engine/code-engine.tsx index 7c555126..e52e2ccb 100644 --- a/frontend/src/components/chat/code-engine/code-engine.tsx +++ b/frontend/src/components/chat/code-engine/code-engine.tsx @@ -332,7 +332,10 @@ export function CodeEngine({ setProjectCompleted(true); isProjectLoadedRef.current = true; try { - localStorage.setItem(getUserStorageKey(`project-completed-${chatId}`), 'true'); + localStorage.setItem( + getUserStorageKey(`project-completed-${chatId}`), + 'true' + ); } catch (e) { logger.error('Failed to save project completion status:', e); } @@ -390,7 +393,10 @@ export function CodeEngine({ isProjectLoadedRef.current = true; try { - localStorage.setItem(getUserStorageKey(`project-completed-${chatId}`), 'true'); + localStorage.setItem( + getUserStorageKey(`project-completed-${chatId}`), + 'true' + ); } catch (e) { logger.error('Failed to save project completion status:', e); } diff --git a/frontend/src/components/root/expand-card.tsx b/frontend/src/components/root/expand-card.tsx index 18fe575e..f2b040ab 100644 --- a/frontend/src/components/root/expand-card.tsx +++ b/frontend/src/components/root/expand-card.tsx @@ -9,7 +9,12 @@ import { URL_PROTOCOL_PREFIX } from '@/utils/const'; import { logger } from '@/app/log/logger'; import { Button } from '@/components/ui/button'; -export function ExpandableCard({ projects, isGenerating = false, onOpenChat, isCommunityProject = false }) { +export function ExpandableCard({ + projects, + isGenerating = false, + onOpenChat, + isCommunityProject = false, +}) { const [active, setActive] = useState(null); const [iframeUrl, setIframeUrl] = useState(''); const ref = useRef(null); diff --git a/frontend/src/components/sidebar-item.tsx b/frontend/src/components/sidebar-item.tsx index 8ebe5b2c..375b139a 100644 --- a/frontend/src/components/sidebar-item.tsx +++ b/frontend/src/components/sidebar-item.tsx @@ -14,7 +14,11 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { DELETE_CHAT, DELETE_PROJECT, GET_CHAT_DETAILS } from '@/graphql/request'; +import { + DELETE_CHAT, + DELETE_PROJECT, + GET_CHAT_DETAILS, +} from '@/graphql/request'; import { cn } from '@/lib/utils'; import { useMutation, useLazyQuery } from '@apollo/client'; import { MoreHorizontal, Trash2 } from 'lucide-react'; @@ -44,13 +48,14 @@ function SideBarItemComponent({ const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); const router = useRouter(); - const { recentlyCompletedProjectId, setPendingProjects } = useContext(ProjectContext); + const { recentlyCompletedProjectId, setPendingProjects } = + useContext(ProjectContext); const isGenerating = id === recentlyCompletedProjectId; const isSelected = currentChatId === id; const variant = isSelected ? 'secondary' : 'ghost'; const [getChatDetails] = useLazyQuery(GET_CHAT_DETAILS); - + const [deleteProject] = useMutation(DELETE_PROJECT, { onCompleted: () => { logger.info('Project deleted successfully'); @@ -84,11 +89,11 @@ function SideBarItemComponent({ const handleDeleteChat = async () => { try { const chatDetailsResult = await getChatDetails({ - variables: { chatId: id } + variables: { chatId: id }, }); - + const projectId = chatDetailsResult?.data?.getChatDetails?.project?.id; - + await deleteChat({ variables: { chatId: id, @@ -99,7 +104,7 @@ function SideBarItemComponent({ cache.gc(); }, }); - + if (projectId) { try { await deleteProject({ @@ -108,13 +113,13 @@ function SideBarItemComponent({ // Clear project cache cache.evict({ id: `Project:${projectId}` }); cache.gc(); - } + }, }); } catch (projectError) { logger.error('Error deleting associated project:', projectError); } } - + setIsDialogOpen(false); } catch (error) { logger.error('Error deleting chat:', error); diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index 11a593f8..0ca970bf 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -100,7 +100,7 @@ export const DELETE_CHAT = gql` export const GET_USER_INFO = gql` query me { me { - id, + id username email avatarUrl