Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
87 changes: 71 additions & 16 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { autoUpdater } = require("electron-updater");
const Store = require("electron-store");

let requiredModules: Record<string, any> = {};
const networkRequests: Record<string, any> = {};
let mainWindow: BrowserWindow | undefined = undefined;

app
Expand Down Expand Up @@ -48,18 +49,6 @@ app.on("quit", () => {
app.quit();
});

ipcMain.handle("setNativeThemeLight", () => {
nativeTheme.themeSource = "light";
});

ipcMain.handle("setNativeThemeDark", () => {
nativeTheme.themeSource = "dark";
});

ipcMain.handle("setNativeThemeSystem", () => {
nativeTheme.themeSource = "system";
});

function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1200,
Expand Down Expand Up @@ -138,6 +127,30 @@ function handleAppUpdates() {
* Handles various IPC messages from the renderer process.
*/
function handleIPCs() {
/**
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
* This will change the appearance of the app to the light theme.
*/
ipcMain.handle("setNativeThemeLight", () => {
nativeTheme.themeSource = "light";
});

/**
* Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
* This will change the appearance of the app to the dark theme.
*/
ipcMain.handle("setNativeThemeDark", () => {
nativeTheme.themeSource = "dark";
});

/**
* Handles the "setNativeThemeSystem" IPC message by setting the native theme source to "system".
* This will change the appearance of the app to match the system's current theme.
*/
ipcMain.handle("setNativeThemeSystem", () => {
nativeTheme.themeSource = "system";
});

/**
* Invokes a function from a plugin module in main node process.
* @param _event - The IPC event object.
Expand Down Expand Up @@ -319,8 +332,9 @@ function handleIPCs() {
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
const userDataPath = app.getPath("userData");
const destination = resolve(userDataPath, fileName);
const rq = request(url);

progress(request(url), {})
progress(rq, {})
.on("progress", function (state: any) {
mainWindow?.webContents.send("FILE_DOWNLOAD_UPDATE", {
...state,
Expand All @@ -332,13 +346,54 @@ function handleIPCs() {
fileName,
err,
});
networkRequests[fileName] = undefined;
})
.on("end", function () {
mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", {
fileName,
});
if (networkRequests[fileName]) {
mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", {
fileName,
});
networkRequests[fileName] = undefined;
} else {
mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
fileName,
err: "Download cancelled",
});
}
})
.pipe(createWriteStream(destination));

networkRequests[fileName] = rq;
});

/**
* Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle("pauseDownload", async (_event, fileName) => {
networkRequests[fileName]?.pause();
});

/**
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle("resumeDownload", async (_event, fileName) => {
networkRequests[fileName]?.resume();
});

/**
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
* The network request associated with the fileName is then removed from the networkRequests object.
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle("abortDownload", async (_event, fileName) => {
const rq = networkRequests[fileName];
networkRequests[fileName] = undefined;
rq?.abort();
});

/**
Expand Down
9 changes: 9 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
downloadFile: (url: string, path: string) =>
ipcRenderer.invoke("downloadFile", url, path),

pauseDownload: (fileName: string) =>
ipcRenderer.invoke("pauseDownload", fileName),

resumeDownload: (fileName: string) =>
ipcRenderer.invoke("resumeDownload", fileName),

abortDownload: (fileName: string) =>
ipcRenderer.invoke("abortDownload", fileName),

onFileDownloadUpdate: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),

Expand Down
87 changes: 87 additions & 0 deletions web/app/_components/ConfirmationModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import { PrimitiveAtom, useAtom } from 'jotai'

interface Props {
atom: PrimitiveAtom<boolean>
title: string
description: string
onConfirm: () => void
}

const ConfirmationModal: React.FC<Props> = ({ atom, title, description, onConfirm }) => {
const [show, setShow] = useAtom(atom)

return (
<Transition.Root show={show} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setShow}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<QuestionMarkCircleIcon
className="h-6 w-6 text-green-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-base font-semibold leading-6 text-gray-900"
>
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">{description}</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={onConfirm}
>
OK
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={() => setShow(false)}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

export default React.memo(ConfirmationModal)
27 changes: 23 additions & 4 deletions web/app/_components/ExploreModelItemHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
MainViewState,
setMainViewStateAtom,
} from '@helpers/atoms/MainView.atom'
import ConfirmationModal from '../ConfirmationModal'
import { showingCancelDownloadModalAtom } from '@helpers/atoms/Modal.atom'

type Props = {
suitableModel: ModelVersion
Expand All @@ -31,6 +33,9 @@ const ExploreModelItemHeader: React.FC<Props> = ({
)
const downloadState = useAtomValue(downloadAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom)
const setShowingCancelDownloadModal = useSetAtom(
showingCancelDownloadModalAtom
)

useEffect(() => {
getPerformanceForModel(suitableModel)
Expand Down Expand Up @@ -70,17 +75,30 @@ const ExploreModelItemHeader: React.FC<Props> = ({
// downloading
downloadButton = (
<Button
disabled
themes="accent"
themes="outline"
onClick={() => {
setMainViewState(MainViewState.MyModel)
setShowingCancelDownloadModal(true)
}}
>
Downloading {formatDownloadPercentage(downloadState.percent)}
Cancel ({formatDownloadPercentage(downloadState.percent)})
</Button>
)
}

let cancelDownloadModal =
downloadState != null ? (
<ConfirmationModal
atom={showingCancelDownloadModalAtom}
title="Cancel Download"
description={`Are you sure you want to cancel the download of ${downloadState?.fileName}?`}
onConfirm={() => {
window.coreAPI?.abortDownload(downloadState?.fileName)
}}
/>
) : (
<></>
)

return (
<div className="flex items-center justify-between rounded-t-md border-b border-border bg-background/50 px-4 py-2">
<div className="flex items-center gap-2">
Expand All @@ -90,6 +108,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({
)}
</div>
{downloadButton}
{cancelDownloadModal}
</div>
)
}
Expand Down
8 changes: 0 additions & 8 deletions web/containers/BottomBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ const BottomBar = () => {
{!stateModelStartStop.loading && (
<SystemItem name="Active model:" value={activeModel?.name || '-'} />
)}
{downloadStates.length > 0 && (
<SystemItem
name="Downloading:"
value={`${downloadStates[0].fileName} - ${formatDownloadPercentage(
downloadStates[0].percent
)}`}
/>
)}
</div>
<div className="flex gap-x-2">
<SystemItem name="CPU:" value={`${cpu}%`} />
Expand Down
1 change: 1 addition & 0 deletions web/helpers/EventListenerWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function EventListenerWrapper({ children }: Props) {
window.electronAPI.onFileDownloadError(
(_event: string, callback: any) => {
console.log('Download error', callback)
setDownloadStateSuccess(callback.fileName)
}
)

Expand Down
2 changes: 1 addition & 1 deletion web/helpers/atoms/DownloadState.atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ export const setDownloadStateSuccessAtom = atom(
delete currentState[fileName]
set(modelDownloadStateAtom, currentState)
}
)
)
1 change: 1 addition & 0 deletions web/helpers/atoms/Modal.atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const showingAdvancedPromptAtom = atom<boolean>(false)
export const showingProductDetailAtom = atom<boolean>(false)
export const showingMobilePaneAtom = atom<boolean>(false)
export const showingBotListModalAtom = atom<boolean>(false)
export const showingCancelDownloadModalAtom = atom<boolean>(false)

export const switchingModelConfirmationModalPropsAtom = atom<
SwitchingModelConfirmationModalProps | undefined
Expand Down
13 changes: 9 additions & 4 deletions web/screens/MyModels/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ const MyModelsScreen = () => {
return (
<div className="flex h-full items-center justify-center px-4">
<div className="text-center">
<LayoutGrid size={32} className="text-accent/50 mx-auto" />
<LayoutGrid size={32} className="mx-auto text-accent/50" />
<div className="mt-4">
{isDownloadingFirstModel ? (
<div className="relative">
<div className="mt-4">
<h1 className="text-2xl font-bold leading-snug">
Donwloading your first model
</h1>
<p className="text-muted-foreground mt-1">
<p className="mt-1 text-muted-foreground">
{downloadStates[0].fileName} -{' '}
{formatDownloadPercentage(downloadStates[0].percent)}
</p>
Expand All @@ -47,7 +47,7 @@ const MyModelsScreen = () => {
) : (
<Fragment>
<h1 className="text-2xl font-bold leading-snug">{`Ups, You don't have a model.`}</h1>
<p className="text-muted-foreground mt-1 text-base">{`let’s download your first model`}</p>
<p className="mt-1 text-base text-muted-foreground">{`let’s download your first model`}</p>
<Button
className="mt-4"
themes="accent"
Expand All @@ -65,7 +65,12 @@ const MyModelsScreen = () => {
return (
<div className="flex h-full w-full overflow-y-auto">
<div className="w-full p-5">
<h1 data-testid="testid-mymodels-header" className="text-lg font-semibold">My Models</h1>
<h1
data-testid="testid-mymodels-header"
className="text-lg font-semibold"
>
My Models
</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">
You have <span>{downloadedModels.length}</span> models downloaded
</p>
Expand Down