Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(frontend): fix cannot redirect into chat/project page after create project from home page #151

Merged
merged 5 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/app/api/file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function getFileType(filePath: string): string {
const extension = filePath.split('.').pop()?.toLowerCase() || '';

const typeMap: { [key: string]: string } = {
//TODO: Add more file types
tsx: 'typescriptreact',
txt: 'text',
md: 'markdown',
json: 'json',
Expand Down
95 changes: 83 additions & 12 deletions frontend/src/app/api/runProject/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ async function buildAndRunDocker(

return new Promise((resolve, reject) => {
// 2. Build the Docker image
console.log(
`Starting Docker build for image: ${imageName} in directory: ${directory}`
);
exec(
`docker build -t ${imageName} ${directory}`,
(buildErr, buildStdout, buildStderr) => {
Expand All @@ -141,19 +144,61 @@ async function buildAndRunDocker(

// 3. Run the Docker container
const runCommand = `docker run -d --name ${containerName} -l "traefik.enable=true" \
-l "traefik.http.routers.${subdomain}.rule=Host(\\"${domain}\\")" \
-l "traefik.http.services.${subdomain}.loadbalancer.server.port=5173" \
--network=codefox_traefik_network -p ${exposedPort}:5173 \
-v "${directory}:/app" \
${imageName}`;
-l "traefik.http.routers.${subdomain}.rule=Host(\\"${domain}\\")" \
-l "traefik.http.services.${subdomain}.loadbalancer.server.port=5173" \
--network=codefox_traefik_network -p ${exposedPort}:5173 \
-v "${directory}:/app" \
${imageName}`;

console.log(runCommand);
console.log(`Executing run command: ${runCommand}`);

exec(runCommand, (runErr, runStdout, runStderr) => {
if (runErr) {
// If the container name already exists
console.error(`Error during Docker run: ${runStderr}`);
if (runStderr.includes('Conflict. The container name')) {
resolve({ domain, containerId: containerName });
console.log(
`Container name conflict detected. Removing existing container ${containerName}.`
);
// Remove the existing container
exec(
`docker rm -f ${containerName}`,
(removeErr, removeStdout, removeStderr) => {
if (removeErr) {
console.error(
`Error removing existing container: ${removeStderr}`
);
return reject(removeErr);
}
console.log(
`Existing container ${containerName} removed. Retrying to run the container.`
);

// Retry running the Docker container
exec(
runCommand,
(retryRunErr, retryRunStdout, retryRunStderr) => {
if (retryRunErr) {
console.error(
`Error during Docker run: ${retryRunStderr}`
);
return reject(retryRunErr);
}

const containerActualId = retryRunStdout.trim();
runningContainers.set(projectPath, {
domain,
containerId: containerActualId,
});

console.log(
`Container ${containerName} is now running at http://${domain}`
);
resolve({ domain, containerId: containerActualId });
}
);
}
);
return;
}
console.error(`Error during Docker run: ${runStderr}`);
Expand All @@ -169,7 +214,6 @@ async function buildAndRunDocker(
console.log(
`Container ${containerName} is now running at http://${domain}`
);

resolve({ domain, containerId: containerActualId });
});
}
Expand Down Expand Up @@ -204,11 +248,38 @@ export async function GET(req: Request) {
// Check if a container is already running
const existingContainer = runningContainers.get(projectPath);
if (existingContainer) {
return NextResponse.json({
message: 'Docker container already running',
domain: existingContainer.domain,
containerId: existingContainer.containerId,
// Check if the container is running
const containerStatus = await new Promise<string>((resolve) => {
exec(
`docker inspect -f "{{.State.Running}}" ${existingContainer.containerId}`,
(err, stdout) => {
if (err) {
resolve('not found');
} else {
resolve(stdout.trim());
}
}
);
});

if (containerStatus === 'true') {
return NextResponse.json({
message: 'Docker container already running',
domain: existingContainer.domain,
containerId: existingContainer.containerId,
});
} else {
// Remove the existing container if it's not running
exec(`docker rm -f ${existingContainer.containerId}`, (removeErr) => {
if (removeErr) {
console.error(`Error removing existing container: ${removeErr}`);
} else {
console.log(
`Removed existing container: ${existingContainer.containerId}`
);
}
});
}
}

// Prevent duplicate builds
Expand Down
15 changes: 6 additions & 9 deletions frontend/src/components/chat/chat-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ProjectModal from '@/components/chat/project-modal';
import { useQuery } from '@apollo/client';
import { GET_USER_PROJECTS } from '@/graphql/request';
import { useAuthContext } from '@/providers/AuthProvider';
import { ProjectProvider } from './code-engine/project-context';

export default function ChatLayout({
children,
Expand All @@ -30,14 +29,12 @@ export default function ChatLayout({

return (
<main className="flex h-[calc(100dvh)] flex-col items-center">
<ProjectProvider>
<ProjectModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
refetchProjects={refetch}
/>
<div className="w-full h-full">{children}</div>
</ProjectProvider>
<ProjectModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
refetchProjects={refetch}
/>
<div className="w-full h-full">{children}</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Removed ProjectProvider wrapper - verify context dependencies.

You've removed the ProjectProvider wrapper from around ProjectModal and its children. This simplifies the component hierarchy, but ensure that ProjectModal and other child components don't depend on context data previously provided by ProjectProvider.


🏁 Script executed:

#!/bin/bash
# Check if ProjectModal uses any context from ProjectContext
ast-grep --pattern 'useContext(ProjectContext)' frontend/src/components/chat/project-modal.tsx || echo "ProjectContext not directly used in ProjectModal"

# Check for imports of ProjectContext in the modal
rg "import.*ProjectContext" frontend/src/components/chat/project-modal.tsx

Length of output: 363


Action Required: Address ProjectModal's Context Dependency

  • The ProjectModal component (at frontend/src/components/chat/project-modal.tsx) still uses useContext(ProjectContext) (line 24), and imports { ProjectContext, ProjectProvider }.
  • With the removal of the ProjectProvider wrapper in chat-layout.tsx, ProjectModal will no longer receive the expected context, which could lead to runtime errors or unexpected behavior.
  • Next Steps:
    • Either reintroduce the ProjectProvider wrapper around ProjectModal and its children, or refactor ProjectModal to remove its reliance on ProjectContext (or provide default values/props as needed).
    • Verify all components consuming ProjectContext are updated consistently throughout the codebase.

</main>
);
}
12 changes: 8 additions & 4 deletions frontend/src/components/chat/code-engine/code-engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,16 @@ export function CodeEngine({
// Effect: Fetch file structure when projectId changes
useEffect(() => {
async function fetchFiles() {
if (!curProject?.projectPath) return;
if (!curProject?.projectPath) {
console.log('no project path found');
return;
}

try {
const response = await fetch(
`/api/project?path=${curProject.projectPath}`
);
console.log('loading file structure');
const data = await response.json();
setFileStructureData(data.res || {});
} catch (error) {
Expand Down Expand Up @@ -270,12 +274,12 @@ export function CodeEngine({

// Render the CodeEngine layout
return (
<div className="rounded-lg border shadow-sm overflow-hidden h-full">
<div className="rounded-lg border shadow-sm overflow-scroll h-full">
{/* Header Bar */}
<ResponsiveToolbar isLoading={!isProjectReady} />

{/* Main Content Area with Loading */}
<div className="relative h-[calc(100vh-48px-2rem)]">
<div className="relative h-[calc(100vh-48px-4rem)]">
<AnimatePresence>
{!isProjectReady && (
<motion.div
Expand Down Expand Up @@ -311,7 +315,7 @@ export function CodeEngine({
<Editor
height="100%"
width="100%"
defaultLanguage="typescript"
defaultLanguage="typescriptreact"
value={newCode}
language={type}
loading={isLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) {
// Navigate to chat page after project creation
if (data?.createProject?.id) {
toast.success('Project created successfully!');
router.push(`/chat/${data.createProject.id}`);
router.push(`/chat?id=${data.createProject.id}`);
}
},
onError: (error) => {
Expand Down
84 changes: 79 additions & 5 deletions frontend/src/components/chat/code-engine/web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
ChevronRight,
Maximize,
ExternalLink,
RefreshCcw,
ZoomIn,
ZoomOut,
} from 'lucide-react';

export default function WebPreview() {
Expand All @@ -15,6 +18,7 @@
const [displayPath, setDisplayPath] = useState('/');
const [history, setHistory] = useState<string[]>(['/']);
const [currentIndex, setCurrentIndex] = useState(0);
const [scale, setScale] = useState(0.7);
const iframeRef = useRef(null);
const containerRef = useRef<{ projectPath: string; domain: string } | null>(
null
Expand Down Expand Up @@ -49,13 +53,34 @@
);
const json = await response.json();

await new Promise((resolve) => setTimeout(resolve, 200));
await new Promise((resolve) => setTimeout(resolve, 100));

containerRef.current = {
projectPath,
domain: json.domain,
};
setBaseUrl(`http://${json.domain}`);

const checkUrlStatus = async (url: string) => {
let status = 0;
while (status !== 200) {
try {
const res = await fetch(url, { method: 'HEAD' });
status = res.status;
if (status !== 200) {
console.log(`URL status: ${status}. Retrying...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
} catch (err) {
console.error('Error checking URL status:', err);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
};

const baseUrl = `http://${json.domain}`;
await checkUrlStatus(baseUrl);

setBaseUrl(baseUrl);
setDisplayPath('/');
} catch (error) {
console.error('fetching url error:', error);
Expand Down Expand Up @@ -109,6 +134,21 @@
setDisplayPath(history[currentIndex + 1]);
}
};
const reloadIframe = () => {
const iframe = document.getElementById('myIframe') as HTMLIFrameElement;
if (iframe) {
iframe.src = iframe.src;

Check failure on line 140 in frontend/src/components/chat/code-engine/web-view.tsx

View workflow job for this annotation

GitHub Actions / autofix

'iframe.src' is assigned to itself
setScale(0.7);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix self-assignment issue in reload function.

The current implementation assigns iframe.src to itself, which doesn't actually reload the iframe.

const reloadIframe = () => {
  const iframe = document.getElementById('myIframe') as HTMLIFrameElement;
  if (iframe) {
-    iframe.src = iframe.src;
+    const currentSrc = iframe.src;
+    iframe.src = '';
+    setTimeout(() => {
+      iframe.src = currentSrc;
+    }, 10);
    setScale(0.7);
  }
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const reloadIframe = () => {
const iframe = document.getElementById('myIframe') as HTMLIFrameElement;
if (iframe) {
iframe.src = iframe.src;
setScale(0.7);
}
};
const reloadIframe = () => {
const iframe = document.getElementById('myIframe') as HTMLIFrameElement;
if (iframe) {
const currentSrc = iframe.src;
iframe.src = '';
setTimeout(() => {
iframe.src = currentSrc;
}, 10);
setScale(0.7);
}
};
🧰 Tools
🪛 Biome (1.9.4)

[error] 140-140: src is assigned to itself.

This is where is assigned.

(lint/correctness/noSelfAssign)

🪛 GitHub Check: autofix

[failure] 140-140:
'iframe.src' is assigned to itself


const zoomIn = () => {
setScale((prevScale) => Math.min(prevScale + 0.1, 2)); // 最大缩放比例为 2
};

const zoomOut = () => {
setScale((prevScale) => Math.max(prevScale - 0.1, 0.5)); // 最小缩放比例为 0.5
};

return (
<div className="flex flex-col w-full h-full">
Expand All @@ -119,7 +159,7 @@
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="h-6 w-6"
onClick={goBack}
disabled={!baseUrl || currentIndex === 0}
>
Expand All @@ -128,12 +168,20 @@
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="h-6 w-6"
onClick={goForward}
disabled={!baseUrl || currentIndex >= history.length - 1}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={reloadIframe}
>
<RefreshCcw />
</Button>
</div>

{/* URL Input */}
Expand All @@ -150,6 +198,24 @@

{/* Actions */}
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={zoomOut}
className="h-8 w-8"
disabled={!baseUrl}
>
<ZoomOut className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={zoomIn}
className="h-8 w-8"
disabled={!baseUrl}
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
Expand All @@ -175,9 +241,17 @@
<div className="relative flex-1 w-full h-full">
{baseUrl ? (
<iframe
id="myIframe"
ref={iframeRef}
src={`${baseUrl}${displayPath}`}
className="absolute inset-0 w-full h-full border-none bg-background"
className="absolute inset-0 w-full h-80% border-none bg-background"
style={{
transform: `scale(${scale})`,
transformOrigin: 'top left',
width: `calc(100% / ${scale})`,
height: `calc(100% / ${scale})`,
border: 'none',
}}
/>
) : (
<div className="absolute inset-0 w-full h-full flex items-center justify-center bg-background">
Expand Down
Loading
Loading