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",
],
}