发布于 2026-01-06 9 阅读
0

🤯 使用 React 构建你的第一个神经应用 DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

🤯 使用 React 构建你的第一个神经科学应用

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

如今大多数应用程序都会根据用户意图改变状态。更具体地说,是根据用户的手部动作(例如点击、轻触、按压等)来改变状态。然而,每一个意图都始于我们的大脑。

今天,我们将开发一款不同类型的应用程序。我们将开发一款能够根据你的认知状态改变自身状态的应用程序。

听我说完。

如果我们的应用能根据你的平静程度改变WebGL海洋的运动轨迹,会怎么样?一种由你的情绪驱动的“视觉冥想”体验。

第一步是测量和获取这些数据。为此,我们将使用 Notion 头戴式设备。

推文介绍 Notion

推文介绍 Notion

入门

我们首先使用 Create React App (CRA) 来启动我们的应用程序。我们在 VS Code 中打开项目,并在本地运行应用程序。

  • npx create-react-app mind-controlled-ocean
  • code mind-controlled-ocean
  • npm start

如果一切顺利,你应该会看到类似这样的画面:

创建 React 应用默认视图


创建 React 应用默认视图

🔑 身份验证

我们重视隐私。因此,Notion 是首款配备身份验证功能的脑机接口。为应用添加身份验证功能非常简单。为此,我们需要一个登录表单和三个副作用来同步身份验证状态。

您只需一个Neurosity 账户和一个设备 ID 即可连接到您的 Notion 脑机接口。那么,让我们首先创建一个新的登录表单组件来收集这些信息。

// src/components/LoginForm.js
import React, { useState } from "react";

export function LoginForm({ onLogin, loading, error }) {
  const [deviceId, setDeviceId] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  function onSubmit(event) {
    event.preventDefault();
    onLogin({ deviceId, email, password });
  }

  return (
    <form className="card login-form" onSubmit={onSubmit}>
      <h3 className="card-heading">Login</h3>
      {!!error ? <h4 className="card-error">{error}</h4> : null}
      <div className="row">
        <label>Notion Device ID</label>
        <input
          type="text"
          value={deviceId}
          disabled={loading}
          onChange={e => setDeviceId(e.target.value)}
        />
      </div>
      <div className="row">
        <label>Email</label>
        <input
          type="email"
          value={email}
          disabled={loading}
          onChange={e => setEmail(e.target.value)}
        />
      </div>
      <div className="row">
        <label>Password</label>
        <input
          type="password"
          value={password}
          disabled={loading}
          onChange={e => setPassword(e.target.value)}
        />
      </div>
      <div className="row">
        <button type="submit" className="card-btn" disabled={loading}>
          {loading ? "Logging in..." : "Login"}
        </button>
      </div>
    </form>
  );
}

Enter fullscreen mode Exit fullscreen mode

🖍 为表单添加样式 -以下是我使用的 CSS

deviceId此组件将保存 `<form> `、` <form> email` 和 ` <form>` 的状态password。此外,我们的表单组件将接收一个 ` onLoginprop`,该属性会在用户点击“登录”按钮时执行。我们还会接收一个loading`prop`,用于指示表单提交正在进行中,以及一个error`prop`,用于在发生错误时显示消息。

现在我们已经创建了登录组件,让我们添加一个登录页面来使用我们的新组件。

// src/pages/Login.js
import React, { useState, useEffect } from "react";
import { LoginForm } from "../components/LoginForm";

export function Login({ notion, user, setUser, setDeviceId }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [isLoggingIn, setIsLoggingIn] = useState(false);

  function onLogin({ email, password, deviceId }) {
    if (email && password && deviceId) {
      setError("");
      setEmail(email);
      setPassword(password);
      setDeviceId(deviceId);
    } else {
      setError("Please fill the form");
    }
  }

  return (
    <LoginForm
      onLogin={onLogin}
      loading={isLoggingIn}
      error={error}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

登录表单


登录表单组件

本页面的目标是显示登录表单,通过setError函数添加基本的表单验证,并执行登录功能。对于后者,我们添加一个副作用,该副作用将与email页面password接收到的 props 同步。

useEffect(() => {
  if (!user && notion && email && password) {
    login();
  }

  async function login() {
    setIsLoggingIn(true);
    const auth = await notion
      .login({ email, password })
      .catch(error => {
        setError(error.message);
      });

    if (auth) {
      setUser(auth.user);
    }

    setIsLoggingIn(false);
  }
}, [email, password, notion, user, setUser, setError]);
Enter fullscreen mode Exit fullscreen mode

你可以把它理解user为保存 Notion API 设置的身份验证用户会话的对象。因此,只有login()当不存在身份验证会话、状态中存在 Notion 实例,且用户已提交电子邮件和密码时,我们才会调用我们的函数。

很快你们就会知道我们将如何接收道具了:notion, user, setUser, setDeviceId。但在那之前,让我们回到我们的工作区App.js,开始把所有东西组装起来。

⚙️ 应用状态

为了简化应用,我们将只使用 React 的Hook、Reach Router 以及react-useuseState提供的本地存储 Hook 👍 。这意味着我们的应用状态策略是将全局状态保存在 App 组件级别,并将必要的 props 传递给子组件。

  • npm install @reach/router react-use

我们将从一条路由开始,但随着我们继续构建应用程序,我们将再添加 2 条路由。

// src/App.js
import React, { useState, useEffect } from "react";
import { Router, navigate } from "@reach/router";
import useLocalStorage from "react-use/lib/useLocalStorage";
import { Login } from "./pages/Login";

export function App() {
  const [notion, setNotion] = useState(null);
  const [user, setUser] = useState(null);
  const [deviceId, setDeviceId] = useLocalStorage("deviceId");
  const [loading, setLoading] = useState(true);

  return (
    <Router>
      <Login
        path="/"
        notion={notion}
        user={user}
        setUser={setUser}
        setDeviceId={setDeviceId}
      />
    </Router>
  );
}
Enter fullscreen mode Exit fullscreen mode

🏆 我发现Ryan Florence(及其团队)开发的Reach Router在简洁性和可预测性之间取得了完美的平衡。他们的文档非常清晰,让我几分钟内就实现了基本的路由配置。

如果您想知道为什么我们决定将地址保存deviceId在本地存储中,那是因为我们需要在用户登录前后访问它。此外,避免用户多次输入地址也能带来更佳的用户体验。

🧠 Notion

现在我们已经实现了基本的状态管理,接下来让我们通过安装 API 并将其导入到 Notion 中,将我们的应用程序与NotionApp.js集成。

  • npm install @neurosity/notion

🤯 Notion API 实现了设备和应用之间的全面通信。我们将使用它来获取基于用户认知状态的实时反馈。在这个应用中,我们将专门使用Calm API。

import { Notion } from "@neurosity/notion";
Enter fullscreen mode Exit fullscreen mode

连接到 Notion 设备很简单。我们实例化一个新的Notion 对象并传递设备 ID。我们可以添加一个副作用,通过与 App 组件同步,将实例的状态设置为 App 组件的状态deviceId

📖 您可以在docs.neurosity.co找到完整的 Notion 文档

useEffect(() => {
  if (deviceId) {
    const notion = new Notion({ deviceId }); // 😲
    setNotion(notion);
  } else {
    setLoading(false);
  }
}, [deviceId]);
Enter fullscreen mode Exit fullscreen mode

我们需要同步的另一个状态是user状态。

在以下示例中,我们将添加一个与实例值同步的副作用notion。如果实例notion值尚未设置,我们将跳过订阅Calm事件,直到notion实例创建完成。

useEffect(() => {
  if (!notion) {
    return;
  }

  const subscription = notion.onAuthStateChanged().subscribe(user => {
    if (user) {
      setUser(user);
    } else {
      navigate("/");
    }
    setLoading(false);
  });

  return () => {
    subscription.unsubscribe();
  };
}, [notion]);
Enter fullscreen mode Exit fullscreen mode

如果应用程序通过 Notion 身份验证持久化了活动用户会话,我们将需要获取当前登录用户,并将其设置为我们的 App 组件中的状态。

onAuthStateChanged方法返回一个包含用户身份验证事件的可观察对象。需要注意的是,在浏览器中使用 Notion API 时,会话将通过本地存储持久保存。因此,如果您关闭应用程序或重新加载页面,会话将保持不变,并onAuthStateChanged返回用户会话,而不是其他状态null。这正是我们所需要的。

如果未检测到会话,我们可以跳转到登录页面。否则,user在组件状态中进行设置。

我们可以通过添加注销页面来实现完整的身份验证。

// src/pages/Logout.js
import { useEffect } from "react";
import { navigate } from "@reach/router";

export function Logout({ notion, resetState }) {
  useEffect(() => {
    if (notion) {
      notion.logout().then(() => {
        resetState();
        navigate("/");
      });
    }
  }, [notion, resetState]);

  return null;
}
Enter fullscreen mode Exit fullscreen mode

登出页面只是一个简单的 React 组件,不包含任何 DOM 元素。我们只需要一个副作用,notion.logout()即如果notion实例存在,则调用相应的方法。最后,它会在用户登出后将其重定向到初始路由。

现在可以将此组件添加为路由App.js

// src/App.js
// ...
import { Logout } from "./pages/Logout";
// ...

return (
  <Router>
    {/* ... */}
    <Logout path="/logout" notion={notion} resetState={() => {
      setNotion(null);
      setUser(null);
      setDeviceId("");
    }} />
  </Router>
);
Enter fullscreen mode Exit fullscreen mode

身份验证完成后,让我们根据认知状态添加应用程序逻辑!

🌊 WebGL Ocean

我第一眼看到David用WebGL制作的海洋模型就爱上了它。所以,用Notion来控制天气,从而模拟海浪,感觉就像是一个很有趣的实验。

💡 有趣的事实:这个海浪模拟程序是 David Li 于 2013 年使用 JavaScript 和 WebGL 创建的。它曾作为 Google Experiments 的一部分进行展示。我很高兴看到它以 MIT 许可证开源。

接下来,我们要创建一个使用 WebGL Ocean 的新组件。因此,我们创建一个名为 Ocean 的目录./src/components/Ocean,并将以下文件添加到该目录中。

// src/components/Ocean/Ocean.js
import React, { useState, useEffect, useRef } from "react";
import useRafState from "react-use/lib/useRafState";

import { Simulator, Camera } from "./simulation.js"; // by David Li
import { mapCalmToWeather } from "./weather.js";

const camera = new Camera();

export function Ocean({ calm }) {
  const ref = useRef();
  const [simulator, setSimulator] = useState();
  const [lastTime, setLastTime] = useRafState(Date.now());

  useEffect(() => {
    const { innerWidth, innerHeight } = window;
    const simulator = new Simulator(ref.current, innerWidth, innerHeight);
    setSimulator(simulator);
  }, [ref, setSimulator]);

  useEffect(() => {
    if (simulator) {
      const currentTime = Date.now();
      const deltaTime = (currentTime - lastTime) / 1000 || 0.0;
      setLastTime(currentTime);
      simulator.render(deltaTime, camera);
    }
  }, [simulator, lastTime, setLastTime]);

  return <canvas className="simulation" ref={ref}></canvas>;
}
Enter fullscreen mode Exit fullscreen mode

如果一切顺利,我们应该能看到这一幕。

海洋波浪模拟


海洋波浪模拟

让我来解释一下这里发生了什么。

  • 1️⃣ React 组件返回一个用于 WebGL 3D 场景的 canvas 元素
  • 2️⃣ 我们使用 React 的方法useRef来访问 canvas HTML 元素
  • 3️⃣Simulator当引用发生变化时,我们会实例化一个新的对象。该类Simulator负责控制渲染以及天气属性,例如风力波浪大小和风场大小
  • 4️⃣ 我们使用useRaf(requestAnimationFrame) hook 创建一个循环,其中回调函数在每个动画帧上执行。

目前,我们的海浪是根据静态天气参数(波浪的汹涌程度风力大小)来变化的。那么,我们如何根据calm评分来映射这些天气参数呢?

为此,我创建了一个实用函数,用于将平静度weather.js评分映射到相应的天气设置:波浪的湍急程度风力波浪大小。然后,我们可以创建一个副作用,在每次评分变化时同步更新。calm

useEffect(() => {
  if (simulator) {
    setWeatherBasedOnCalm(animatedCalm, 0, 0);
  }

  function setWeatherBasedOnCalm(calm) {
    const { choppiness, wind, size } = mapCalmToWeather(calm);
    simulator.setChoppiness(choppiness);
    simulator.setWind(wind, wind);
    simulator.setSize(size);
  }
}, [calm, simulator]);
Enter fullscreen mode Exit fullscreen mode

认知状态

这才是最有趣的部分。在这里,我们可以获取大脑数据并将其映射到应用程序状态。

通过订阅,我们可以大约每秒notion.calm()获得一个新的分数。因此,让我们添加组件,将其作为 prop 添加,并创建一个副作用,使其与实例和同步。如果这两个状态都存在,那么我们就可以安全地订阅calm 了calm<Ocean calm={calm} />calmnotionuser

🧘🏿‍♀️平静度评分源自您的被动认知状态。该指标基于α波。平静度评分范围为0.00 到 100。1.0分数越高,检测到平静0.3感的可能性就越大。平静度评分超过100 分意义重大。有助于提高平静度评分的方法包括闭上眼睛、保持静止、深呼吸或冥想。

// src/pages/Calm.js
import React, { useState, useEffect } from "react";
import { Ocean } from "../components/Ocean/Ocean";

export function Calm({ user, notion }) {
  const [calm, setCalm] = useState(0);

  useEffect(() => {
    if (!user || !notion) {
      return;
    }

    const subscription = notion.calm().subscribe(calm => {
      const calmScore = Number(calm.probability.toFixed(2));
      setCalm(calmScore);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, [user, notion]);

  return (
    <Ocean calm={calm} />
  );
}
Enter fullscreen mode Exit fullscreen mode

💡 所有 notion 指标,包括notion.calm()返回一个 RxJS 订阅,我们可以使用该订阅在组件卸载时安全地取消订阅。

最后,我们将 Calm 页面添加到App.js.

// src/App.js
// ...
import { Calm } from "./pages/Calm";
// ...

// If already authenticated, redirect user to the Calm page
useEffect(() => {
  if (user) {
    navigate("/calm");
  }
}, [user]);

return (
  <Router>
    {/* ... */}
    <Calm path="/calm" notion={notion} user={user} />
  </Router>
);
Enter fullscreen mode Exit fullscreen mode

至此,我们的Neuro React应用程序就完成了。

GitHub 标志 神经质/概念海洋

🌊 使用脑机接口控制 WebGL 海洋的运动


我非常期待能根据用户个性特点打造个性化的应用体验。每个人的大脑都不同,但我们开发的应用却总是给所有用户呈现相同的体验。如果应用能为量身定制呢?

如果有些应用程序能帮助你在压力大的时候放松身心呢?

如果能用脑电波验证应用程序的身份呢?

如果电子游戏可以根据玩家的感受改变剧情走向呢?

如果什么...

文章来源:https://dev.to/neurosity/building-your-first-neuro-app-with-react-49pj