Initial open-source release
This commit is contained in:
6
src/hooks/use-mobile.ts
Normal file
6
src/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
export function useIsMobile() {
|
||||
// Always return false to force desktop behavior
|
||||
return false;
|
||||
}
|
||||
42
src/hooks/useChats.ts
Normal file
42
src/hooks/useChats.ts
Normal 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
61
src/hooks/useLoadApp.ts
Normal 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 };
|
||||
}
|
||||
62
src/hooks/useLoadAppFile.ts
Normal file
62
src/hooks/useLoadAppFile.ts
Normal 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
33
src/hooks/useLoadApps.ts
Normal 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 };
|
||||
}
|
||||
54
src/hooks/useLoadVersions.ts
Normal file
54
src/hooks/useLoadVersions.ts
Normal 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
99
src/hooks/useRunApp.ts
Normal 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
88
src/hooks/useSettings.ts
Normal 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
128
src/hooks/useStreamChat.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user