diff --git a/app/main/main.ts b/app/main/main.ts index 6313407..32cd985 100644 --- a/app/main/main.ts +++ b/app/main/main.ts @@ -250,6 +250,7 @@ app.whenReady().then(() => { win.setBounds({ height: arg.height, }); + win.center(); }); createSplashScreen(); diff --git a/app/package-lock.json b/app/package-lock.json index b0fcc76..ade90fe 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,11 @@ "name": "electron-app", "version": "0.1.0", "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.1", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", @@ -32,6 +37,7 @@ "react-dom": "18.2.0", "react-redux": "^9.1.0", "react-resizable-panels": "^2.0.11", + "rxjs": "^7.8.1", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.3.3", "tailwindcss-animate": "^1.0.7", @@ -524,6 +530,72 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz", + "integrity": "sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", + "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", + "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -9024,6 +9096,48 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==" + }, + "@fortawesome/fontawesome-free": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz", + "integrity": "sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.5.1" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", + "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.5.1" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.5.1" + } + }, + "@fortawesome/react-fontawesome": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", + "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "requires": { + "prop-types": "^15.8.1" + } + }, "@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", diff --git a/app/package.json b/app/package.json index a452df7..4cb2c19 100644 --- a/app/package.json +++ b/app/package.json @@ -23,6 +23,11 @@ "lint": "npx eslint --max-warnings 0 --ext=.ts ." }, "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.1", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", @@ -47,6 +52,7 @@ "react-dom": "18.2.0", "react-redux": "^9.1.0", "react-resizable-panels": "^2.0.11", + "rxjs": "^7.8.1", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.3.3", "tailwindcss-animate": "^1.0.7", diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index 15d0086..6a52445 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -1,5 +1,6 @@ 'use client'; +import StoreProvider from '../AppProvider'; import './globals.css'; export default function RootLayout({ @@ -15,7 +16,9 @@ export default function RootLayout({ userSelect: 'none', }} > - {children} + + {children} + ); diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 7554a84..b3a87b3 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -7,11 +7,20 @@ import React, { import Chat from '../components/chat/Chat'; import SelectDirectory from '../components/options/SelectDirectory'; import SelectModel from '../components/options/SelectModel'; +import { + useAppDispatch, +} from '../lib/hooks'; +import { + startDirectoryIndexing, + stopDirectoryIndexing, +} from '../lib/store'; export default function Home() { const [selectedDirectory, setSelectedDirectory] = useState(null); const [selectedModel, setSelectedModel] = useState(null); + const dispatch = useAppDispatch(); + function handleOpen() { if (typeof window !== 'undefined') { window.electronAPI.selectDirectory(); @@ -22,6 +31,7 @@ export default function Home() { window.electronAPI.onSelectDirectory(async (customData) => { setSelectedDirectory(customData[0]); try { + dispatch(startDirectoryIndexing()); await fetch('http://localhost:8080/api/index', { method: 'POST', headers: { @@ -31,10 +41,12 @@ export default function Home() { directory: customData[0], }), }); + dispatch(stopDirectoryIndexing()); // TODO: spinner while indexing } catch (error) { // eslint-disable-next-line no-console console.error('Error sending message: ', error); + dispatch(stopDirectoryIndexing()); } }); }, []); diff --git a/app/src/components/chat/Chat.tsx b/app/src/components/chat/Chat.tsx index b1062c0..fac59fa 100644 --- a/app/src/components/chat/Chat.tsx +++ b/app/src/components/chat/Chat.tsx @@ -56,9 +56,9 @@ const Chat = () => {
0, + 'h-[400px]': chatHistory.length > 0, }, )} > diff --git a/app/src/components/chat/ChatInput.tsx b/app/src/components/chat/ChatInput.tsx index 7525864..0d86dc1 100644 --- a/app/src/components/chat/ChatInput.tsx +++ b/app/src/components/chat/ChatInput.tsx @@ -1,7 +1,11 @@ import React, { + useEffect, useRef, useState, } from 'react'; +import { + useAppSelector, +} from '../../lib/hooks'; import { Input, } from '../ui/input'; @@ -23,6 +27,22 @@ const ChatInput = ({ sendMessage(message); }; + // detect website focus and focus the input + const handleFocus = () => { + if (document.activeElement !== inputRef.current) { + inputRef.current?.focus(); + } + }; + + useEffect(() => { + window.addEventListener('focus', handleFocus); + return () => { + window.removeEventListener('focus', handleFocus); + }; + }, []); + + const isDirectoryIndexing = useAppSelector((state) => state.isDirectoryIndexing); + return (
setMessage(e.target.value)} - placeholder='Enter prompt here' + placeholder={isDirectoryIndexing ? 'Indexing your files..' : 'Enter prompt here'} onKeyDown={handleSend} ref={inputRef} + disabled={isDirectoryIndexing} className={'text-xl border-0 focus-visible:outline-transparent focus-visible:ring-0 focus-visible:shadow-0 w-full shadow-0'} style={{ // @ts-expect-error -- WebkitAppRegion is a valid property diff --git a/app/src/components/options/SelectDirectory.tsx b/app/src/components/options/SelectDirectory.tsx index 69219d4..42b9231 100644 --- a/app/src/components/options/SelectDirectory.tsx +++ b/app/src/components/options/SelectDirectory.tsx @@ -1,4 +1,20 @@ -import React from 'react'; +import { + faCheckCircle, + faCircleNotch, +} from '@fortawesome/free-solid-svg-icons'; +import { + FontAwesomeIcon, +} from '@fortawesome/react-fontawesome'; +import React, { + useEffect, +} from 'react'; +import { + useAppSelector, + usePrevious, +} from '../../lib/hooks'; +import { + cn, +} from '../../lib/utils'; import { Button, } from '../ui/button'; @@ -13,14 +29,39 @@ const SelectDirectory = ({ const shortenedDirectory = selectedDirectory ? `/${selectedDirectory.split('/')[1]}/../${selectedDirectory.split('/').pop()}` : 'Select Directory'; + const [isCheckShowing, setIsCheckShowing] = React.useState(false); + + const isDirectoryIndexing = useAppSelector((state) => state.isDirectoryIndexing); + + const oldLoadingState = usePrevious(isDirectoryIndexing); + + useEffect(() => { + if (oldLoadingState && !isDirectoryIndexing) { + setIsCheckShowing(true); + setTimeout(() => { + setIsCheckShowing(false); + }, 3000); + } + }, [isDirectoryIndexing]); return ( - +
+ +
); }; diff --git a/app/src/components/ui/input.tsx b/app/src/components/ui/input.tsx index 8becd60..9b29750 100644 --- a/app/src/components/ui/input.tsx +++ b/app/src/components/ui/input.tsx @@ -10,7 +10,7 @@ const Input = React.forwardRef( AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; export const useAppStore: () => AppStore = useStore; + +export function usePrevious(value: T): T | undefined { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} diff --git a/app/src/lib/store.ts b/app/src/lib/store.ts index 4346d87..5d2ab56 100644 --- a/app/src/lib/store.ts +++ b/app/src/lib/store.ts @@ -1,10 +1,33 @@ import { configureStore, + createSlice, } from '@reduxjs/toolkit'; +const globalSlice = createSlice({ + name: 'global', + initialState: { + isDirectoryIndexing: false, + }, + reducers: { + startDirectoryIndexing: (state) => { + // eslint-disable-next-line no-param-reassign + state.isDirectoryIndexing = true; + }, + stopDirectoryIndexing: (state) => { + // eslint-disable-next-line no-param-reassign + state.isDirectoryIndexing = false; + }, + }, +}); + +export const { + startDirectoryIndexing, + stopDirectoryIndexing, +} = globalSlice.actions; + export const makeStore = () => configureStore({ - reducer: {}, + reducer: globalSlice.reducer, }); // Infer the type of makeStore