diff --git a/README.md b/README.md index eb41d1260..305dec99d 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,14 @@ Using python3.10 (3.10.13) Notice the environment prefix `(openadapt-py3.10)`. +### Tray +Run the following command to start the system tray icon and launch the web dashboard: + +``` +python -m openadapt.entrypoint +``` +This command will print the config, update the database to the latest migration, start the system tray icon and launch the web dashboard. + ### Record Create a new recording by running the following command: diff --git a/openadapt/app/dashboard/api/settings.py b/openadapt/app/dashboard/api/settings.py index c791dbe70..716203102 100644 --- a/openadapt/app/dashboard/api/settings.py +++ b/openadapt/app/dashboard/api/settings.py @@ -20,7 +20,9 @@ def attach_routes(self) -> APIRouter: self.app.add_api_route("", self.set_settings, methods=["POST"]) return self.app - Category = Literal["api_keys", "scrubbing", "record_and_replay", "general"] + Category = Literal[ + "api_keys", "scrubbing", "record_and_replay", "general", "onboarding" + ] @staticmethod def get_settings(category: Category) -> dict[str, Any]: diff --git a/openadapt/app/dashboard/app/layout.tsx b/openadapt/app/dashboard/app/layout.tsx index 52d4515c1..3cd56c206 100644 --- a/openadapt/app/dashboard/app/layout.tsx +++ b/openadapt/app/dashboard/app/layout.tsx @@ -4,7 +4,6 @@ import { ColorSchemeScript, MantineProvider } from '@mantine/core' import { Notifications } from '@mantine/notifications'; import { Shell } from '@/components/Shell' import { CSPostHogProvider } from './providers'; -import { ModalsProvider } from '@mantine/modals'; export const metadata = { title: 'OpenAdapt.AI', @@ -24,9 +23,9 @@ export default function RootLayout({ - - {children} - + + {children} + diff --git a/openadapt/app/dashboard/app/onboarding/page.tsx b/openadapt/app/dashboard/app/onboarding/page.tsx new file mode 100644 index 000000000..d4a3ecdde --- /dev/null +++ b/openadapt/app/dashboard/app/onboarding/page.tsx @@ -0,0 +1,16 @@ +import { BookACall } from "@/components/Onboarding/steps/BookACall"; +import { RegisterForUpdates } from "@/components/Onboarding/steps/RegisterForUpdates"; +import { Tutorial } from "@/components/Onboarding/steps/Tutorial"; +import { Box, Divider } from "@mantine/core"; + +export default function Onboarding() { + return ( + + + + + + + + ); +} diff --git a/openadapt/app/dashboard/app/recordings/page.tsx b/openadapt/app/dashboard/app/recordings/page.tsx index 901444f76..46e5f8361 100644 --- a/openadapt/app/dashboard/app/recordings/page.tsx +++ b/openadapt/app/dashboard/app/recordings/page.tsx @@ -50,7 +50,7 @@ export default function Recordings() { }, []); return ( - {recordingStatus === RecordingStatus.RECORDING && ( + {/* {recordingStatus === RecordingStatus.RECORDING && ( @@ -64,7 +64,7 @@ export default function Recordings() { - )} + )} */} diff --git a/openadapt/app/dashboard/app/routes.ts b/openadapt/app/dashboard/app/routes.ts index a8ef8f96d..b0ef5f0d0 100644 --- a/openadapt/app/dashboard/app/routes.ts +++ b/openadapt/app/dashboard/app/routes.ts @@ -15,5 +15,9 @@ export const routes: Route[] = [ { name: 'Scrubbing', path: '/scrubbing', + }, + { + name: 'Onboarding', + path: '/onboarding', } ] diff --git a/openadapt/app/dashboard/components/Onboarding/steps/BookACall.tsx b/openadapt/app/dashboard/components/Onboarding/steps/BookACall.tsx new file mode 100644 index 000000000..05179d818 --- /dev/null +++ b/openadapt/app/dashboard/components/Onboarding/steps/BookACall.tsx @@ -0,0 +1,16 @@ +import { Box, Text } from '@mantine/core' +import Link from 'next/link' +import React from 'react' + +export const BookACall = () => { + return ( + + + + Book a call with us + + to discuss how OpenAdapt can help your team + + + ) +} diff --git a/openadapt/app/dashboard/components/Onboarding/steps/RegisterForUpdates.tsx b/openadapt/app/dashboard/components/Onboarding/steps/RegisterForUpdates.tsx new file mode 100644 index 000000000..37470ecf5 --- /dev/null +++ b/openadapt/app/dashboard/components/Onboarding/steps/RegisterForUpdates.tsx @@ -0,0 +1,67 @@ +'use client'; + + +import { Box, Button, Stack, Text, TextInput } from '@mantine/core' +import { isNotEmpty, useForm } from '@mantine/form' +import { notifications } from '@mantine/notifications' +import React, { useEffect } from 'react' + +export const RegisterForUpdates = () => { + const onboardingForm = useForm({ + initialValues: { + email: '', + }, + validate: { + email: isNotEmpty('Email is required'), + } + }) + useEffect(() => { + fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + REDIRECT_TO_ONBOARDING: false + }), + }) + }, []) + function onSubmit({ email }: { email: string }) { + fetch('https://openadapt.ai/form.html', { + method: 'POST', + mode: 'no-cors', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + email, + 'form-name': 'email', + 'bot-field': '', + }).toString(), + }).then(() => { + notifications.show({ + title: 'Thank you!', + message: 'You have been registered for updates', + color: 'green', + }) + }) + } + return ( + + +
+ + + +
+
+ ) +} diff --git a/openadapt/app/dashboard/components/Onboarding/steps/Tutorial.tsx b/openadapt/app/dashboard/components/Onboarding/steps/Tutorial.tsx new file mode 100644 index 000000000..eca662c92 --- /dev/null +++ b/openadapt/app/dashboard/components/Onboarding/steps/Tutorial.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { Box, Text } from '@mantine/core' +import { algora, type AlgoraOutput } from '@algora/sdk'; +import React, { useEffect } from 'react' +import Link from 'next/link'; + +type Bounty = AlgoraOutput['bounty']['list']['items'][number]; + +function BountyCard(props: { bounty: Bounty }) { + return ( + +
+
+ {props.bounty.reward_formatted} +
+
+ {props.bounty.task.repo_name}#{props.bounty.task.number} +
+
+ {props.bounty.task.title} +
+
+ + ); +} + +const featuredBountyId = 'clxi7tk210002l20aqlz58ram'; + +async function getFeaturedBounty() { + const bounty: Bounty = await algora.bounty.get.query({ id: featuredBountyId }); + return bounty; +} + +export const Tutorial = () => { + const [featuredBounty, setFeaturedBounty] = React.useState(null); + useEffect(() => { + getFeaturedBounty().then(setFeaturedBounty); + }, []); + return ( + + + Welcome to OpenAdapt! Thank you for joining us on our mission to build open source desktop AI. Your feedback is extremely valuable! + + + To start, please watch the demonstration below. Then try it yourself! If you have any issues, please submit a Github Issue. + + + + If you'd like to contribute directly to our development, please consider the following open Bounties (no development experience required): + {featuredBounty && } + + + ) +} diff --git a/openadapt/app/dashboard/index.js b/openadapt/app/dashboard/index.js index b16703cc9..161d03c6d 100644 --- a/openadapt/app/dashboard/index.js +++ b/openadapt/app/dashboard/index.js @@ -17,7 +17,7 @@ const checkPort = (port) => { } // check if both ports are not being used -const { DASHBOARD_CLIENT_PORT, DASHBOARD_SERVER_PORT } = process.env +const { DASHBOARD_CLIENT_PORT, DASHBOARD_SERVER_PORT, REDIRECT_TO_ONBOARDING } = process.env Promise.all([checkPort(DASHBOARD_CLIENT_PORT), checkPort(DASHBOARD_SERVER_PORT)]) .then(([clientPort, serverPort]) => { if (clientPort !== DASHBOARD_CLIENT_PORT) { @@ -45,7 +45,11 @@ function spawnChildProcess() { // wait for 3 seconds before opening the browser setTimeout(() => { import('open').then(({ default: open }) => { - open(`http://localhost:${DASHBOARD_CLIENT_PORT}`) + let url = `http://localhost:${DASHBOARD_CLIENT_PORT}` + if (REDIRECT_TO_ONBOARDING === 'true') { + url += '/onboarding' + } + open(url) }) }, 3000) }) diff --git a/openadapt/app/dashboard/package-lock.json b/openadapt/app/dashboard/package-lock.json index 07ac7c00c..c939b78fc 100644 --- a/openadapt/app/dashboard/package-lock.json +++ b/openadapt/app/dashboard/package-lock.json @@ -8,6 +8,7 @@ "name": "nextjs-fastapi", "version": "0.1.0", "dependencies": { + "@algora/sdk": "^0.2.0", "@mantine/carousel": "7.7.1", "@mantine/core": "7.7.1", "@mantine/form": "7.7.1", @@ -35,6 +36,16 @@ "prettier": "^3.2.5" } }, + "node_modules/@algora/sdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@algora/sdk/-/sdk-0.2.0.tgz", + "integrity": "sha512-J8r9OqNGnnRGjX29w9jyRAIih40PIoY+18q7zI5RQHZgne0gSmhUZrvAXqejMSpi8WbYpYetA4mViuvoTT1AxQ==", + "dependencies": { + "@trpc/client": "^10.0.0", + "@trpc/server": "^10.0.0", + "superjson": "^1.9.1" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -702,6 +713,25 @@ "react": ">= 16" } }, + "node_modules/@trpc/client": { + "version": "10.45.2", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.45.2.tgz", + "integrity": "sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==", + "funding": [ + "https://trpc.io/sponsor" + ], + "peerDependencies": { + "@trpc/server": "10.45.2" + } + }, + "node_modules/@trpc/server": { + "version": "10.45.2", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.45.2.tgz", + "integrity": "sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg==", + "funding": [ + "https://trpc.io/sponsor" + ] + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1456,6 +1486,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3346,6 +3390,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -5097,6 +5152,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/openadapt/app/dashboard/package.json b/openadapt/app/dashboard/package.json index 92398a2f9..787ee08b8 100644 --- a/openadapt/app/dashboard/package.json +++ b/openadapt/app/dashboard/package.json @@ -15,6 +15,7 @@ "format": "prettier --write ." }, "dependencies": { + "@algora/sdk": "^0.2.0", "@mantine/carousel": "7.7.1", "@mantine/core": "7.7.1", "@mantine/form": "7.7.1", diff --git a/openadapt/app/dashboard/run.py b/openadapt/app/dashboard/run.py index e6fd43186..87839b1f1 100644 --- a/openadapt/app/dashboard/run.py +++ b/openadapt/app/dashboard/run.py @@ -25,9 +25,11 @@ def run() -> Thread: def run_client() -> subprocess.Popen: """The entry point for the thread that runs the dashboard client.""" if is_running_from_executable(): - webbrowser.open( - f"http://localhost:{config.DASHBOARD_SERVER_PORT}/recordings" - ) + if config.REDIRECT_TO_ONBOARDING: + url = f"http://localhost:{config.DASHBOARD_SERVER_PORT}/onboarding" + else: + url = f"http://localhost:{config.DASHBOARD_SERVER_PORT}" + webbrowser.open(url) run_app() return @@ -41,6 +43,9 @@ def run_client() -> subprocess.Popen: "DASHBOARD_SERVER_PORT": str(config.DASHBOARD_SERVER_PORT), "NEXT_PUBLIC_POSTHOG_HOST": POSTHOG_HOST, "NEXT_PUBLIC_POSTHOG_PUBLIC_KEY": POSTHOG_PUBLIC_KEY, + "REDIRECT_TO_ONBOARDING": ( + "true" if config.REDIRECT_TO_ONBOARDING else "false" + ), "NEXT_PUBLIC_MODE": ( "production" if is_running_from_executable() else "development" ), diff --git a/openadapt/app/tray.py b/openadapt/app/tray.py index 29d28b52a..3aa03ab06 100644 --- a/openadapt/app/tray.py +++ b/openadapt/app/tray.py @@ -133,10 +133,6 @@ def __init__(self) -> None: # self.app_action.triggered.connect(self.show_app) # self.menu.addAction(self.app_action) - self.dashboard_action = TrackedQAction("Launch Dashboard") - self.dashboard_action.triggered.connect(self.launch_dashboard) - self.menu.addAction(self.dashboard_action) - self.quit = TrackedQAction("Quit") def _quit() -> None: @@ -168,6 +164,8 @@ def _quit() -> None: # for storing toasts that should be manually removed self.sticky_toasts = {} + self.launch_dashboard() + def handle_recording_signal( self, notifier: QSocketNotifier, @@ -481,7 +479,6 @@ def launch_dashboard(self) -> None: self.dashboard_thread.join() self.dashboard_thread = run_dashboard() self.dashboard_thread.start() - self.dashboard_action.setText("Reload dashboard") def run(self) -> None: """Run the system tray icon.""" diff --git a/openadapt/config.defaults.json b/openadapt/config.defaults.json index 15c9840ac..a74f15eb9 100644 --- a/openadapt/config.defaults.json +++ b/openadapt/config.defaults.json @@ -83,5 +83,6 @@ "SPACY_MODEL_NAME": "en_core_web_trf", "DASHBOARD_CLIENT_PORT": 5173, "DASHBOARD_SERVER_PORT": 8080, - "UNIQUE_USER_ID": "" + "UNIQUE_USER_ID": "", + "REDIRECT_TO_ONBOARDING": true } diff --git a/openadapt/config.py b/openadapt/config.py index d18465e79..742aeaa88 100644 --- a/openadapt/config.py +++ b/openadapt/config.py @@ -227,6 +227,7 @@ def validate_scrub_fill_color(cls, v: Union[str, int]) -> int: # noqa: ANN102 SOM_SERVER_URL: str = "" UNIQUE_USER_ID: str = "" + REDIRECT_TO_ONBOARDING: bool = True class Adapter(str, Enum): """Adapter for the completions API.""" @@ -289,6 +290,7 @@ def __setattr__(self, key: str, value: Any) -> None: ], "general": [ "UNIQUE_USER_ID", + "REDIRECT_TO_ONBOARDING", ], }