27
src/components/ImportAppButton.tsx
Normal file
27
src/components/ImportAppButton.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Upload } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ImportAppDialog } from "./ImportAppDialog";
|
||||||
|
|
||||||
|
export function ImportAppButton() {
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="px-4 pb-1 flex justify-center">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="default"
|
||||||
|
onClick={() => setIsDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
Import App
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ImportAppDialog
|
||||||
|
isOpen={isDialogOpen}
|
||||||
|
onClose={() => setIsDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
275
src/components/ImportAppDialog.tsx
Normal file
275
src/components/ImportAppDialog.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import { useState } 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 {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
import { useLoadApps } from "@/hooks/useLoadApps";
|
||||||
|
|
||||||
|
interface ImportAppDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
||||||
|
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||||
|
const [hasAiRules, setHasAiRules] = useState<boolean | null>(null);
|
||||||
|
const [customAppName, setCustomAppName] = useState<string>("");
|
||||||
|
const [nameExists, setNameExists] = useState<boolean>(false);
|
||||||
|
const [isCheckingName, setIsCheckingName] = useState<boolean>(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { streamMessage } = useStreamChat({ hasChatId: false });
|
||||||
|
const { refreshApps } = useLoadApps();
|
||||||
|
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
||||||
|
|
||||||
|
const checkAppName = async (name: string): Promise<void> => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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 } });
|
||||||
|
streamMessage({
|
||||||
|
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.",
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAppNameChange = async (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const newName = e.target.value;
|
||||||
|
setCustomAppName(newName);
|
||||||
|
if (newName.trim()) {
|
||||||
|
await checkAppName(newName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Import App</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select an existing app folder to import into Dyad.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert className="border-blue-500/20 text-blue-500">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
App import is an experimental feature. If you encounter any issues,
|
||||||
|
please report them using the Help button.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="py-4">
|
||||||
|
{!selectedPath ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleSelectFolder}
|
||||||
|
disabled={selectFolderMutation.isPending}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{selectFolderMutation.isPending ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Folder className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{selectFolderMutation.isPending
|
||||||
|
? "Selecting folder..."
|
||||||
|
: "Select Folder"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-md border p-4">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium">Selected folder:</p>
|
||||||
|
<p className="text-sm text-muted-foreground break-all">
|
||||||
|
{selectedPath}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="h-8 w-8 p-0 flex-shrink-0"
|
||||||
|
disabled={importAppMutation.isPending}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Clear selection</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{nameExists && (
|
||||||
|
<p className="text-sm text-yellow-500">
|
||||||
|
An app with this name already exists. Please choose a
|
||||||
|
different name:
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="relative">
|
||||||
|
<Label className="text-sm ml-2 mb-2">App name</Label>
|
||||||
|
<Input
|
||||||
|
value={customAppName}
|
||||||
|
onChange={handleAppNameChange}
|
||||||
|
placeholder="Enter new app name"
|
||||||
|
className="w-full pr-8"
|
||||||
|
disabled={importAppMutation.isPending}
|
||||||
|
/>
|
||||||
|
{isCheckingName && (
|
||||||
|
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasAiRules === false && (
|
||||||
|
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="h-4 w-4 flex-shrink-0 mt-1" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
AI_RULES.md lets Dyad know which tech stack to use for
|
||||||
|
editing the app
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<AlertDescription>
|
||||||
|
No AI_RULES.md found. Dyad will automatically generate one
|
||||||
|
after importing.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{importAppMutation.isPending && (
|
||||||
|
<div className="flex items-center justify-center space-x-2 text-sm text-muted-foreground animate-pulse">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
<span>Importing app...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={importAppMutation.isPending}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={
|
||||||
|
!selectedPath || importAppMutation.isPending || nameExists
|
||||||
|
}
|
||||||
|
className="min-w-[80px]"
|
||||||
|
>
|
||||||
|
{importAppMutation.isPending ? <>Importing...</> : "Import"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -215,7 +215,7 @@ export function registerAppHandlers() {
|
|||||||
const commitHash = await git.commit({
|
const commitHash = await git.commit({
|
||||||
fs: fs,
|
fs: fs,
|
||||||
dir: fullAppPath,
|
dir: fullAppPath,
|
||||||
message: "Init from react vite template",
|
message: "Init Dyad app",
|
||||||
author: await getGitAuthor(),
|
author: await getGitAuthor(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
147
src/ipc/handlers/import_handlers.ts
Normal file
147
src/ipc/handlers/import_handlers.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { dialog } from "electron";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { createLoggedHandler } from "./safe_handle";
|
||||||
|
import log from "electron-log";
|
||||||
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
|
import { apps } from "@/db/schema";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { chats } from "@/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import git from "isomorphic-git";
|
||||||
|
import { getGitAuthor } from "../utils/git_author";
|
||||||
|
import { ImportAppParams, ImportAppResult } from "../ipc_types";
|
||||||
|
|
||||||
|
const logger = log.scope("import-handlers");
|
||||||
|
const handle = createLoggedHandler(logger);
|
||||||
|
|
||||||
|
export function registerImportHandlers() {
|
||||||
|
// Handler for selecting an app folder
|
||||||
|
handle("select-app-folder", async () => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ["openDirectory"],
|
||||||
|
title: "Select App Folder to Import",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled) {
|
||||||
|
return { path: null, name: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPath = result.filePaths[0];
|
||||||
|
const folderName = path.basename(selectedPath);
|
||||||
|
|
||||||
|
return { path: selectedPath, name: folderName };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler for checking if AI_RULES.md exists
|
||||||
|
handle("check-ai-rules", async (_, { path: appPath }: { path: string }) => {
|
||||||
|
try {
|
||||||
|
await fs.access(path.join(appPath, "AI_RULES.md"));
|
||||||
|
return { exists: true };
|
||||||
|
} catch {
|
||||||
|
return { exists: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler for checking if an app name is already taken
|
||||||
|
handle("check-app-name", async (_, { appName }: { appName: string }) => {
|
||||||
|
// Check filesystem
|
||||||
|
const appPath = getDyadAppPath(appName);
|
||||||
|
try {
|
||||||
|
await fs.access(appPath);
|
||||||
|
return { exists: true };
|
||||||
|
} catch {
|
||||||
|
// Path doesn't exist, continue checking database
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database
|
||||||
|
const existingApp = await db.query.apps.findFirst({
|
||||||
|
where: eq(apps.name, appName),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { exists: !!existingApp };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler for importing an app
|
||||||
|
handle(
|
||||||
|
"import-app",
|
||||||
|
async (
|
||||||
|
_,
|
||||||
|
{ path: sourcePath, appName }: ImportAppParams,
|
||||||
|
): Promise<ImportAppResult> => {
|
||||||
|
// Validate the source path exists
|
||||||
|
try {
|
||||||
|
await fs.access(sourcePath);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Source folder does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
const destPath = getDyadAppPath(appName);
|
||||||
|
|
||||||
|
// Check if the app already exists
|
||||||
|
const errorMessage = "An app with this name already exists";
|
||||||
|
try {
|
||||||
|
await fs.access(destPath);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === errorMessage) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Copy the app folder to the Dyad apps directory, excluding node_modules
|
||||||
|
await fs.cp(sourcePath, destPath, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (source) => !source.includes("node_modules"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isGitRepo = await fs
|
||||||
|
.access(path.join(destPath, ".git"))
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
if (!isGitRepo) {
|
||||||
|
// Initialize git repo and create first commit
|
||||||
|
await git.init({
|
||||||
|
fs: fs,
|
||||||
|
dir: destPath,
|
||||||
|
defaultBranch: "main",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stage all files
|
||||||
|
await git.add({
|
||||||
|
fs: fs,
|
||||||
|
dir: destPath,
|
||||||
|
filepath: ".",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
await git.commit({
|
||||||
|
fs: fs,
|
||||||
|
dir: destPath,
|
||||||
|
message: "Init Dyad app",
|
||||||
|
author: await getGitAuthor(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new app
|
||||||
|
const [app] = await db
|
||||||
|
.insert(apps)
|
||||||
|
.values({
|
||||||
|
name: appName,
|
||||||
|
// Use the name as the path for now
|
||||||
|
path: appName,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Create an initial chat for this app
|
||||||
|
const [chat] = await db
|
||||||
|
.insert(chats)
|
||||||
|
.values({
|
||||||
|
appId: app.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
return { appId: app.id, chatId: chat.id };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug("Registered import IPC handlers");
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ import type {
|
|||||||
CreateCustomLanguageModelParams,
|
CreateCustomLanguageModelParams,
|
||||||
DoesReleaseNoteExistParams,
|
DoesReleaseNoteExistParams,
|
||||||
ApproveProposalResult,
|
ApproveProposalResult,
|
||||||
|
ImportAppResult,
|
||||||
|
ImportAppParams,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
import type { ProposalResult } from "@/lib/schemas";
|
import type { ProposalResult } from "@/lib/schemas";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
@@ -792,9 +794,26 @@ export class IpcClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- End window control methods ---
|
public async selectAppFolder(): Promise<{
|
||||||
|
path: string | null;
|
||||||
// --- Language Model Operations ---
|
name: string | null;
|
||||||
|
}> {
|
||||||
// --- App Operations ---
|
return this.ipcRenderer.invoke("select-app-folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkAiRules(params: {
|
||||||
|
path: string;
|
||||||
|
}): Promise<{ exists: boolean }> {
|
||||||
|
return this.ipcRenderer.invoke("check-ai-rules", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async importApp(params: ImportAppParams): Promise<ImportAppResult> {
|
||||||
|
return this.ipcRenderer.invoke("import-app", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAppName(params: {
|
||||||
|
appName: string;
|
||||||
|
}): Promise<{ exists: boolean }> {
|
||||||
|
return this.ipcRenderer.invoke("check-app-name", params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { registerUploadHandlers } from "./handlers/upload_handlers";
|
|||||||
import { registerVersionHandlers } from "./handlers/version_handlers";
|
import { registerVersionHandlers } from "./handlers/version_handlers";
|
||||||
import { registerLanguageModelHandlers } from "./handlers/language_model_handlers";
|
import { registerLanguageModelHandlers } from "./handlers/language_model_handlers";
|
||||||
import { registerReleaseNoteHandlers } from "./handlers/release_note_handlers";
|
import { registerReleaseNoteHandlers } from "./handlers/release_note_handlers";
|
||||||
|
import { registerImportHandlers } from "./handlers/import_handlers";
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
// Register all IPC handlers by category
|
// Register all IPC handlers by category
|
||||||
@@ -37,4 +38,5 @@ export function registerIpcHandlers() {
|
|||||||
registerVersionHandlers();
|
registerVersionHandlers();
|
||||||
registerLanguageModelHandlers();
|
registerLanguageModelHandlers();
|
||||||
registerReleaseNoteHandlers();
|
registerReleaseNoteHandlers();
|
||||||
|
registerImportHandlers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,3 +191,13 @@ export interface ApproveProposalResult {
|
|||||||
extraFiles?: string[];
|
extraFiles?: string[];
|
||||||
extraFilesError?: string;
|
extraFilesError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImportAppParams {
|
||||||
|
path: string;
|
||||||
|
appName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportAppResult {
|
||||||
|
appId: number;
|
||||||
|
chatId: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { ImportAppButton } from "@/components/ImportAppButton";
|
||||||
|
|
||||||
// Adding an export for attachments
|
// Adding an export for attachments
|
||||||
export interface HomeSubmitOptions {
|
export interface HomeSubmitOptions {
|
||||||
@@ -165,6 +166,7 @@ export default function HomePage() {
|
|||||||
<SetupBanner />
|
<SetupBanner />
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
<ImportAppButton />
|
||||||
<HomeChatInput onSubmit={handleSubmit} />
|
<HomeChatInput onSubmit={handleSubmit} />
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ const validInvokeChannels = [
|
|||||||
"delete-messages",
|
"delete-messages",
|
||||||
"start-chat-stream",
|
"start-chat-stream",
|
||||||
"does-release-note-exist",
|
"does-release-note-exist",
|
||||||
|
"import-app",
|
||||||
|
"check-ai-rules",
|
||||||
|
"select-app-folder",
|
||||||
|
"check-app-name",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Add valid receive channels
|
// Add valid receive channels
|
||||||
|
|||||||
Reference in New Issue
Block a user