Initial open-source release

This commit is contained in:
Will Chen
2025-04-11 09:37:05 -07:00
commit 43f67e0739
208 changed files with 45476 additions and 0 deletions

6
src/hooks/use-mobile.ts Normal file
View File

@@ -0,0 +1,6 @@
export function useIsMobile() {
// Always return false to force desktop behavior
return false;
}

42
src/hooks/useChats.ts Normal file
View File

@@ -0,0 +1,42 @@
import { useAtom } from "jotai";
import { useEffect } from "react";
import { chatsAtom, chatsLoadingAtom } from "@/atoms/chatAtoms";
import { getAllChats } from "@/lib/chat";
import type { ChatSummary } from "@/lib/schemas";
export function useChats(appId: number | null) {
const [chats, setChats] = useAtom(chatsAtom);
const [loading, setLoading] = useAtom(chatsLoadingAtom);
useEffect(() => {
const fetchChats = async () => {
try {
setLoading(true);
const chatList = await getAllChats(appId || undefined);
setChats(chatList);
} catch (error) {
console.error("Failed to load chats:", error);
} finally {
setLoading(false);
}
};
fetchChats();
}, [appId, setChats, setLoading]);
const refreshChats = async () => {
try {
setLoading(true);
const chatList = await getAllChats(appId || undefined);
setChats(chatList);
return chatList;
} catch (error) {
console.error("Failed to refresh chats:", error);
return [] as ChatSummary[];
} finally {
setLoading(false);
}
};
return { chats, loading, refreshChats };
}

61
src/hooks/useLoadApp.ts Normal file
View File

@@ -0,0 +1,61 @@
import { useState, useEffect } from "react";
import { IpcClient } from "@/ipc/ipc_client";
import type { App } from "@/ipc/ipc_types";
import { atom, useAtom } from "jotai";
import { currentAppAtom } from "@/atoms/appAtoms";
export function useLoadApp(appId: number | null) {
const [app, setApp] = useAtom(currentAppAtom);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const loadApp = async () => {
if (appId === null) {
setApp(null);
setLoading(false);
return;
}
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
const appData = await ipcClient.getApp(appId);
setApp(appData);
setError(null);
} catch (error) {
console.error(`Error loading app ${appId}:`, error);
setError(error instanceof Error ? error : new Error(String(error)));
setApp(null);
} finally {
setLoading(false);
}
};
loadApp();
}, [appId]);
const refreshApp = async () => {
if (appId === null) {
return;
}
setLoading(true);
try {
console.log("Refreshing app", appId);
const ipcClient = IpcClient.getInstance();
const appData = await ipcClient.getApp(appId);
console.log("App data", appData);
setApp(appData);
setError(null);
} catch (error) {
console.error(`Error refreshing app ${appId}:`, error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
};
return { app, loading, error, refreshApp };
}

View File

@@ -0,0 +1,62 @@
import { useState, useEffect } from "react";
import { IpcClient } from "@/ipc/ipc_client";
export function useLoadAppFile(appId: number | null, filePath: string | null) {
const [content, setContent] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const loadFile = async () => {
if (appId === null || filePath === null) {
setContent(null);
setError(null);
return;
}
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
const fileContent = await ipcClient.readAppFile(appId, filePath);
setContent(fileContent);
setError(null);
} catch (error) {
console.error(
`Error loading file ${filePath} for app ${appId}:`,
error
);
setError(error instanceof Error ? error : new Error(String(error)));
setContent(null);
} finally {
setLoading(false);
}
};
loadFile();
}, [appId, filePath]);
const refreshFile = async () => {
if (appId === null || filePath === null) {
return;
}
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
const fileContent = await ipcClient.readAppFile(appId, filePath);
setContent(fileContent);
setError(null);
} catch (error) {
console.error(
`Error refreshing file ${filePath} for app ${appId}:`,
error
);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
};
return { content, loading, error, refreshFile };
}

33
src/hooks/useLoadApps.ts Normal file
View File

@@ -0,0 +1,33 @@
import { useState, useEffect, useCallback } from "react";
import { useAtom } from "jotai";
import { appBasePathAtom, appsListAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
export function useLoadApps() {
const [apps, setApps] = useAtom(appsListAtom);
const [appBasePath, setAppBasePath] = useAtom(appBasePathAtom);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const refreshApps = useCallback(async () => {
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
const appListResponse = await ipcClient.listApps();
setApps(appListResponse.apps);
setAppBasePath(appListResponse.appBasePath);
setError(null);
} catch (error) {
console.error("Error refreshing apps:", error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
}, [setApps, setError, setLoading]);
useEffect(() => {
refreshApps();
}, [refreshApps]);
return { apps, loading, error, refreshApps };
}

View File

@@ -0,0 +1,54 @@
import { useState, useEffect } from "react";
import { useAtom } from "jotai";
import { versionsListAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
export function useLoadVersions(appId: number | null) {
const [versions, setVersions] = useAtom(versionsListAtom);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const loadVersions = async () => {
// If no app is selected, clear versions and return
if (appId === null) {
setVersions([]);
setLoading(false);
return;
}
try {
const ipcClient = IpcClient.getInstance();
const versionsList = await ipcClient.listVersions({ appId });
setVersions(versionsList);
setError(null);
} catch (error) {
console.error("Error loading versions:", error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
};
loadVersions();
}, [appId, setVersions]);
const refreshVersions = async () => {
if (appId === null) {
return;
}
try {
const ipcClient = IpcClient.getInstance();
const versionsList = await ipcClient.listVersions({ appId });
setVersions(versionsList);
setError(null);
} catch (error) {
console.error("Error refreshing versions:", error);
setError(error instanceof Error ? error : new Error(String(error)));
}
};
return { versions, loading, error, refreshVersions };
}

99
src/hooks/useRunApp.ts Normal file
View File

@@ -0,0 +1,99 @@
import { useState, useCallback } from "react";
import { IpcClient } from "@/ipc/ipc_client";
import { appOutputAtom, appUrlAtom, currentAppAtom } from "@/atoms/appAtoms";
import { useAtom, useSetAtom } from "jotai";
import { App } from "@/ipc/ipc_types";
export function useRunApp() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [app, setApp] = useAtom(currentAppAtom);
const setAppOutput = useSetAtom(appOutputAtom);
const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom);
const runApp = useCallback(async (appId: number) => {
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
console.debug("Running app", appId);
// Clear the URL and add restart message
if (appUrlObj?.appId !== appId) {
setAppUrlObj({ appUrl: null, appId: null });
}
setAppOutput((prev) => [
...prev,
{ message: "Trying to restart app...", type: "stdout", appId },
]);
const app = await ipcClient.getApp(appId);
setApp(app);
await ipcClient.runApp(appId, (output) => {
setAppOutput((prev) => [...prev, output]);
// Check if the output contains a localhost URL
const urlMatch = output.message.match(/(https?:\/\/localhost:\d+\/?)/);
if (urlMatch) {
setAppUrlObj({ appUrl: urlMatch[1], appId });
}
});
setError(null);
} catch (error) {
console.error(`Error running app ${appId}:`, error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
}, []);
const stopApp = useCallback(async (appId: number) => {
if (appId === null) {
return;
}
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
await ipcClient.stopApp(appId);
setError(null);
} catch (error) {
console.error(`Error stopping app ${appId}:`, error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
}, []);
const restartApp = useCallback(async (appId: number) => {
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
console.debug("Restarting app", appId);
// Clear the URL and add restart message
setAppUrlObj({ appUrl: null, appId: null });
setAppOutput((prev) => [
...prev,
{ message: "Restarting app...", type: "stdout", appId },
]);
const app = await ipcClient.getApp(appId);
setApp(app);
await ipcClient.restartApp(appId, (output) => {
setAppOutput((prev) => [...prev, output]);
// Check if the output contains a localhost URL
const urlMatch = output.message.match(/(https?:\/\/localhost:\d+\/?)/);
if (urlMatch) {
setAppUrlObj({ appUrl: urlMatch[1], appId });
}
});
setError(null);
} catch (error) {
console.error(`Error restarting app ${appId}:`, error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
}, []);
return { loading, error, runApp, stopApp, restartApp, app };
}

88
src/hooks/useSettings.ts Normal file
View File

@@ -0,0 +1,88 @@
import { useState, useEffect } from "react";
import { useAtom } from "jotai";
import { userSettingsAtom, envVarsAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import type { UserSettings } from "@/lib/schemas";
const PROVIDER_TO_ENV_VAR: Record<string, string> = {
openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
google: "GEMINI_API_KEY",
};
// Define a type for the environment variables we expect
type EnvVars = Record<string, string | undefined>;
export function useSettings() {
const [settings, setSettingsAtom] = useAtom(userSettingsAtom);
const [envVars, setEnvVarsAtom] = useAtom(envVarsAtom);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const loadInitialData = async () => {
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
// Fetch settings and env vars concurrently
const [userSettings, fetchedEnvVars] = await Promise.all([
ipcClient.getUserSettings(),
ipcClient.getEnvVars(),
]);
setSettingsAtom(userSettings);
setEnvVarsAtom(fetchedEnvVars);
setError(null);
} catch (error) {
console.error("Error loading initial data:", error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
};
loadInitialData();
// Only run once on mount, dependencies are stable getters/setters
}, [setSettingsAtom, setEnvVarsAtom]);
const updateSettings = async (newSettings: Partial<UserSettings>) => {
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
const updatedSettings = await ipcClient.setUserSettings(newSettings);
setSettingsAtom(updatedSettings);
setError(null);
return updatedSettings;
} catch (error) {
console.error("Error updating settings:", error);
setError(error instanceof Error ? error : new Error(String(error)));
throw error;
} finally {
setLoading(false);
}
};
const isProviderSetup = (provider: string) => {
const providerSettings = settings?.providerSettings[provider];
if (providerSettings) {
return true;
}
if (envVars[PROVIDER_TO_ENV_VAR[provider]]) {
return true;
}
return false;
};
return {
settings,
envVars,
loading,
error,
updateSettings,
isProviderSetup,
isAnyProviderSetup: () => {
return Object.keys(PROVIDER_TO_ENV_VAR).some((provider) =>
isProviderSetup(provider)
);
},
};
}

128
src/hooks/useStreamChat.ts Normal file
View File

@@ -0,0 +1,128 @@
import { useCallback, useState } from "react";
import type { Message } from "ai";
import { useAtom, useSetAtom } from "jotai";
import {
chatErrorAtom,
chatMessagesAtom,
chatStreamCountAtom,
isStreamingAtom,
} from "@/atoms/chatAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import type { ChatResponseEnd } from "@/ipc/ipc_types";
import { useChats } from "./useChats";
import { useLoadApp } from "./useLoadApp";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadVersions } from "./useLoadVersions";
import { showError } from "@/lib/toast";
export function getRandomString() {
return Math.random().toString(36).substring(2, 15);
}
export function useStreamChat() {
const [messages, setMessages] = useAtom(chatMessagesAtom);
const [isStreaming, setIsStreaming] = useAtom(isStreamingAtom);
const [error, setError] = useAtom(chatErrorAtom);
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
const [selectedAppId] = useAtom(selectedAppIdAtom);
const { refreshChats } = useChats(selectedAppId);
const { refreshApp } = useLoadApp(selectedAppId);
const setStreamCount = useSetAtom(chatStreamCountAtom);
const { refreshVersions } = useLoadVersions(selectedAppId);
const streamMessage = useCallback(
async ({
prompt,
chatId,
redo,
}: {
prompt: string;
chatId: number;
redo?: boolean;
}) => {
if (!prompt.trim() || !chatId) {
return;
}
setError(null);
console.log("streaming message - set messages", prompt);
setMessages((currentMessages: Message[]) => {
if (redo) {
let remainingMessages = currentMessages.slice();
if (
currentMessages[currentMessages.length - 1].role === "assistant"
) {
remainingMessages = currentMessages.slice(0, -1);
}
return [
...remainingMessages,
{
id: getRandomString(),
role: "assistant",
content: "",
},
];
}
return [
...currentMessages,
{
id: getRandomString(),
role: "user",
content: prompt,
},
{
id: getRandomString(),
role: "assistant",
content: "",
},
];
});
setIsStreaming(true);
setStreamCount((streamCount) => streamCount + 1);
try {
IpcClient.getInstance().streamMessage(prompt, {
chatId,
redo,
onUpdate: (updatedMessages: Message[]) => {
setMessages(updatedMessages);
},
onEnd: (response: ChatResponseEnd) => {
if (response.updatedFiles) {
setIsPreviewOpen(true);
}
// Keep the same as below
setIsStreaming(false);
refreshChats();
refreshApp();
refreshVersions();
},
onError: (errorMessage: string) => {
console.error(`[CHAT] Stream error for ${chatId}:`, errorMessage);
setError(errorMessage);
// Keep the same as above
setIsStreaming(false);
refreshChats();
refreshApp();
refreshVersions();
},
});
} catch (error) {
console.error("[CHAT] Exception during streaming setup:", error);
setIsStreaming(false);
setError(error instanceof Error ? error.message : String(error));
}
},
[setMessages, setIsStreaming, setIsPreviewOpen]
);
return {
streamMessage,
isStreaming,
error,
setError,
setIsStreaming,
};
}