diff --git a/.semgrepignore b/.semgrepignore index 7f5d2839b..dc078c0c5 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -1 +1,2 @@ -.devcontainer/docker-compose.yml \ No newline at end of file +.devcontainer/docker-compose.yml +frontend/ \ No newline at end of file diff --git a/README.md b/README.md index e2b334d73..70919118b 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,11 @@ Since Raven is a Frappe app, it can be installed via [frappe-bench](https://frap Once you have [setup your bench](https://frappeframework.com/docs/v14/user/en/installation) and your [site](https://frappeframework.com/docs/v14/user/en/tutorial/install-and-setup-bench), you can install the app via the following commands: ```bash -bench get-app https://github.com/The-Commit-Company/Raven.git -bench --site yoursite.name install-app raven +bench get-app https://github.com/The-Commit-Company/raven.git +``` + +```bash +bench --site install-app raven ``` Post this, you can access Raven on your Frappe site at the `/raven` endpoint (e.g. https://yoursite.com/raven). diff --git a/frontend/package.json b/frontend/package.json index df464669b..f8d407adc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "raven-ui", "private": true, "license": "AGPL-3.0-only", - "version": "1.7.1", + "version": "2.0.0", "type": "module", "scripts": { "dev": "vite", @@ -47,6 +47,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", + "react-error-boundary": "^4.0.13", "react-hook-form": "^7.52.2", "react-icons": "^5.3.0", "react-idle-timer": "^5.7.2", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fd37bb9bc..a78a28945 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,7 +9,15 @@ import { ThemeProvider } from './ThemeProvider' import { Toaster } from 'sonner' import { useStickyState } from './hooks/useStickyState' import MobileTabsPage from './pages/MobileTabsPage' -import { UserProfile } from './components/feature/userSettings/UserProfile/UserProfile' +import Cookies from 'js-cookie' + +/** Following keys will not be cached in app cache */ +const NO_CACHE_KEYS = [ + "frappe.desk.form.load.getdoctype", + "frappe.desk.search.search_link", + "frappe.model.workflow.get_transitions", + "frappe.desk.reportview.get_count" +] const router = createBrowserRouter( @@ -28,11 +36,36 @@ const router = createBrowserRouter( import('./components/feature/saved-messages/SavedMessages')} /> import('./pages/settings/Settings')}> - } /> - } /> + import('./components/feature/userSettings/UserProfile/UserProfile')} /> + import('./components/feature/userSettings/UserProfile/UserProfile')} /> import('./components/feature/userSettings/Users/AddUsers')} /> - import('./pages/settings/Integrations/FrappeHR')} /> - {/* import('./components/feature/userSettings/Bots')} /> */} + import('./pages/settings/Integrations/FrappeHR')} /> + + import('./pages/settings/AI/BotList')} /> + import('./pages/settings/AI/CreateBot')} /> + import('./pages/settings/AI/ViewBot')} /> + + + + import('./pages/settings/AI/FunctionList')} /> + import('./pages/settings/AI/CreateFunction')} /> + import('./pages/settings/AI/ViewFunction')} /> + + + + + import('./pages/settings/AI/InstructionTemplateList')} /> + import('./pages/settings/AI/CreateInstructionTemplate')} /> + import('./pages/settings/AI/ViewInstructionTemplate')} /> + + + + import('./pages/settings/AI/SavedPromptsList')} /> + import('./pages/settings/AI/CreateSavedPrompt')} /> + import('./pages/settings/AI/ViewSavedPrompt')} /> + + + import('./pages/settings/AI/OpenAISettings')} /> import('@/pages/ChatSpace')}> import('./components/feature/threads/ThreadDrawer/ThreadDrawer')} /> @@ -102,12 +135,52 @@ function App() { function localStorageProvider() { // When initializing, we restore the data from `localStorage` into a map. - const map = new Map(JSON.parse(localStorage.getItem('app-cache') || '[]')) + // Check if local storage is recent (less than a week). Else start with a fresh cache. + const timestamp = localStorage.getItem('app-cache-timestamp') + let cache = '[]' + if (timestamp && Date.now() - parseInt(timestamp) < 7 * 24 * 60 * 60 * 1000) { + const localCache = localStorage.getItem('app-cache') + if (localCache) { + cache = localCache + } + } + const map = new Map(JSON.parse(cache)) // Before unloading the app, we write back all the data into `localStorage`. window.addEventListener('beforeunload', () => { - const appCache = JSON.stringify(Array.from(map.entries())) - localStorage.setItem('app-cache', appCache) + + + // Check if the user is logged in + const user_id = Cookies.get('user_id') + if (!user_id || user_id === 'Guest') { + localStorage.removeItem('app-cache') + localStorage.removeItem('app-cache-timestamp') + } else { + const entries = map.entries() + + const cacheEntries = [] + + for (const [key, value] of entries) { + + let hasCacheKey = false + for (const cacheKey of NO_CACHE_KEYS) { + if (key.includes(cacheKey)) { + hasCacheKey = true + break + } + } + + //Do not cache doctype meta and search link + if (hasCacheKey) { + continue + } + cacheEntries.push([key, value]) + } + const appCache = JSON.stringify(cacheEntries) + localStorage.setItem('app-cache', appCache) + localStorage.setItem('app-cache-timestamp', Date.now().toString()) + } + }) // We still use the map for write & read for performance. diff --git a/frontend/src/components/common/Form.tsx b/frontend/src/components/common/Form.tsx index 37f56ef3a..466c2dae3 100644 --- a/frontend/src/components/common/Form.tsx +++ b/frontend/src/components/common/Form.tsx @@ -18,12 +18,12 @@ export const Label = ({ children, isRequired, ...props }: LabelProps) => { export const HelperText = (props: TextProps) => { return ( - + ) } export const ErrorText = (props: TextProps) => { return ( - + ) } \ No newline at end of file diff --git a/frontend/src/components/common/LinkField/LinkField.tsx b/frontend/src/components/common/LinkField/LinkField.tsx index 34d3837ee..480fafdab 100644 --- a/frontend/src/components/common/LinkField/LinkField.tsx +++ b/frontend/src/components/common/LinkField/LinkField.tsx @@ -24,7 +24,7 @@ export interface LinkFieldProps { const LinkField = ({ doctype, filters, label, placeholder, value, required, setValue, disabled, autofocus, dropdownClass }: LinkFieldProps) => { - const [searchText, setSearchText] = useState('') + const [searchText, setSearchText] = useState(value ?? '') const isDesktop = useIsDesktop() @@ -49,14 +49,15 @@ const LinkField = ({ doctype, filters, label, placeholder, value, required, setV itemToString(item) { return item ? item.value : '' }, - selectedItem: items.find(item => item.value === value), onSelectedItemChange({ selectedItem }) { + setValue(selectedItem?.value ?? '') }, + defaultInputValue: value, + defaultIsOpen: isDesktop && autofocus, + defaultSelectedItem: items.find(item => item.value === value), }) - console.log(isOpen) - return