import { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { IpcClient } from "@/ipc/ipc_client"; import { useMutation } from "@tanstack/react-query"; import { showError, showSuccess } from "@/lib/toast"; import { Folder, X, Loader2, Info } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Label } from "@radix-ui/react-label"; import { useNavigate } from "@tanstack/react-router"; import { useStreamChat } from "@/hooks/useStreamChat"; import type { GithubRepository } from "@/ipc/ipc_types"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { useSetAtom } from "jotai"; import { useLoadApps } from "@/hooks/useLoadApps"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "./ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useSettings } from "@/hooks/useSettings"; import { UnconnectedGitHubConnector } from "@/components/GitHubConnector"; interface ImportAppDialogProps { isOpen: boolean; onClose: () => void; } export const AI_RULES_PROMPT = "Generate an AI_RULES.md file for this app. Describe the tech stack in 5-10 bullet points and describe clear rules about what libraries to use for what."; export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { const [selectedPath, setSelectedPath] = useState(null); const [hasAiRules, setHasAiRules] = useState(null); const [customAppName, setCustomAppName] = useState(""); const [nameExists, setNameExists] = useState(false); const [isCheckingName, setIsCheckingName] = useState(false); const [installCommand, setInstallCommand] = useState(""); const [startCommand, setStartCommand] = useState(""); const navigate = useNavigate(); const { streamMessage } = useStreamChat({ hasChatId: false }); const { refreshApps } = useLoadApps(); const setSelectedAppId = useSetAtom(selectedAppIdAtom); // GitHub import state const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(false); const [url, setUrl] = useState(""); const [importing, setImporting] = useState(false); const { settings, refreshSettings } = useSettings(); const isAuthenticated = !!settings?.githubAccessToken; const [githubAppName, setGithubAppName] = useState(""); const [githubNameExists, setGithubNameExists] = useState(false); const [isCheckingGithubName, setIsCheckingGithubName] = useState(false); useEffect(() => { if (isOpen) { setGithubAppName(""); setGithubNameExists(false); // Fetch GitHub repos if authenticated if (isAuthenticated) { fetchRepos(); } } }, [isOpen, isAuthenticated]); const fetchRepos = async () => { setLoading(true); try { const fetchedRepos = await IpcClient.getInstance().listGithubRepos(); setRepos(fetchedRepos); } catch (err: unknown) { showError("Failed to fetch repositories.: " + (err as any).toString()); } finally { setLoading(false); } }; const handleUrlBlur = async () => { if (!url.trim()) return; const repoName = extractRepoNameFromUrl(url); if (repoName) { setGithubAppName(repoName); setIsCheckingGithubName(true); try { const result = await IpcClient.getInstance().checkAppName({ appName: repoName, }); setGithubNameExists(result.exists); } catch (error: unknown) { showError("Failed to check app name: " + (error as any).toString()); } finally { setIsCheckingGithubName(false); } } }; const extractRepoNameFromUrl = (url: string): string | null => { const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?\/?$/); return match ? match[2] : null; }; const handleImportFromUrl = async () => { setImporting(true); try { const match = extractRepoNameFromUrl(url); const repoName = match ? match[2] : ""; const appName = githubAppName.trim() || repoName; const result = await IpcClient.getInstance().cloneRepoFromUrl({ url, installCommand: installCommand.trim() || undefined, startCommand: startCommand.trim() || undefined, appName, }); if ("error" in result) { showError(result.error); setImporting(false); return; } setSelectedAppId(result.app.id); showSuccess(`Successfully imported ${result.app.name}`); const chatId = await IpcClient.getInstance().createChat(result.app.id); navigate({ to: "/chat", search: { id: chatId } }); if (!result.hasAiRules) { streamMessage({ prompt: AI_RULES_PROMPT, chatId, }); } onClose(); } catch (error: unknown) { showError("Failed to import repository: " + (error as any).toString()); } finally { setImporting(false); } }; const handleSelectRepo = async (repo: GithubRepository) => { setImporting(true); try { const appName = githubAppName.trim() || repo.name; const result = await IpcClient.getInstance().cloneRepoFromUrl({ url: `https://github.com/${repo.full_name}.git`, installCommand: installCommand.trim() || undefined, startCommand: startCommand.trim() || undefined, appName, }); if ("error" in result) { showError(result.error); setImporting(false); return; } setSelectedAppId(result.app.id); showSuccess(`Successfully imported ${result.app.name}`); const chatId = await IpcClient.getInstance().createChat(result.app.id); navigate({ to: "/chat", search: { id: chatId } }); if (!result.hasAiRules) { streamMessage({ prompt: AI_RULES_PROMPT, chatId, }); } onClose(); } catch (error: unknown) { showError("Failed to import repository: " + (error as any).toString()); } finally { setImporting(false); } }; const handleGithubAppNameChange = async ( e: React.ChangeEvent, ) => { const newName = e.target.value; setGithubAppName(newName); if (newName.trim()) { setIsCheckingGithubName(true); try { const result = await IpcClient.getInstance().checkAppName({ appName: newName, }); setGithubNameExists(result.exists); } catch (error: unknown) { showError("Failed to check app name: " + (error as any).toString()); } finally { setIsCheckingGithubName(false); } } }; const checkAppName = async (name: string): Promise => { setIsCheckingName(true); try { const result = await IpcClient.getInstance().checkAppName({ appName: name, }); setNameExists(result.exists); } catch (error: unknown) { showError("Failed to check app name: " + (error as any).toString()); } finally { setIsCheckingName(false); } }; const selectFolderMutation = useMutation({ mutationFn: async () => { const result = await IpcClient.getInstance().selectAppFolder(); if (!result.path || !result.name) { throw new Error("No folder selected"); } const aiRulesCheck = await IpcClient.getInstance().checkAiRules({ path: result.path, }); setHasAiRules(aiRulesCheck.exists); setSelectedPath(result.path); // Use the folder name from the IPC response setCustomAppName(result.name); // Check if the app name already exists await checkAppName(result.name); return result; }, onError: (error: Error) => { showError(error.message); }, }); const importAppMutation = useMutation({ mutationFn: async () => { if (!selectedPath) throw new Error("No folder selected"); return IpcClient.getInstance().importApp({ path: selectedPath, appName: customAppName, installCommand: installCommand || undefined, startCommand: startCommand || undefined, }); }, onSuccess: async (result) => { showSuccess( !hasAiRules ? "App imported successfully. Dyad will automatically generate an AI_RULES.md now." : "App imported successfully", ); onClose(); navigate({ to: "/chat", search: { id: result.chatId } }); if (!hasAiRules) { streamMessage({ prompt: AI_RULES_PROMPT, chatId: result.chatId, }); } setSelectedAppId(result.appId); await refreshApps(); }, onError: (error: Error) => { showError(error.message); }, }); const handleSelectFolder = () => { selectFolderMutation.mutate(); }; const handleImport = () => { importAppMutation.mutate(); }; const handleClear = () => { setSelectedPath(null); setHasAiRules(null); setCustomAppName(""); setNameExists(false); setInstallCommand(""); setStartCommand(""); }; const handleAppNameChange = async ( e: React.ChangeEvent, ) => { const newName = e.target.value; setCustomAppName(newName); if (newName.trim()) { await checkAppName(newName); } }; const hasInstallCommand = installCommand.trim().length > 0; const hasStartCommand = startCommand.trim().length > 0; const commandsValid = hasInstallCommand === hasStartCommand; // Add this component inside the ImportAppDialog.tsx file, before the main component return ( Import App Import existing app from local folder or clone from Github. App import is an experimental feature. If you encounter any issues, please report them using the Help button. Local Folder Your GitHub Repos GitHub URL
{!selectedPath ? ( ) : (

Selected folder:

{selectedPath}

{nameExists && (

An app with this name already exists. Please choose a different name:

)}
{isCheckingName && (
)}
Advanced options
setInstallCommand(e.target.value)} placeholder="pnpm install" disabled={importAppMutation.isPending} />
setStartCommand(e.target.value)} placeholder="pnpm dev" disabled={importAppMutation.isPending} />
{!commandsValid && (

Both commands are required when customizing.

)}
{hasAiRules === false && (

AI_RULES.md lets Dyad know which tech stack to use for editing the app

No AI_RULES.md found. Dyad will automatically generate one after importing.
)} {importAppMutation.isPending && (
Importing app...
)}
)}
{!isAuthenticated ? ( undefined} expanded={false} /> ) : ( <> {loading && (
)}
{isCheckingGithubName && (
)} {githubNameExists && (

An app with this name already exists. Please choose a different name.

)}
{!loading && repos.length === 0 && (

No repositories found

)} {repos.map((repo) => (

{repo.name}

{repo.full_name}

))}
{repos.length > 0 && ( <> Advanced options
setInstallCommand(e.target.value) } placeholder="pnpm install" disabled={importing} />
setStartCommand(e.target.value)} placeholder="pnpm dev" disabled={importing} />
{!commandsValid && (

Both commands are required when customizing.

)}
)} )}
setUrl(e.target.value)} disabled={importing} onBlur={handleUrlBlur} />
{isCheckingGithubName && (
)} {githubNameExists && (

An app with this name already exists. Please choose a different name.

)}
Advanced options
setInstallCommand(e.target.value)} placeholder="pnpm install" disabled={importing} />
setStartCommand(e.target.value)} placeholder="pnpm dev" disabled={importing} />
{!commandsValid && (

Both commands are required when customizing.

)}
); }