通知

AIプロダクトの設定にある通知セクションです。行、列、すべてを切り替えられるチャンネルとイベントのグリッド、次の静音時間をリアルタイムで示す静音時間、メールダイジェスト、デスクトップ、メール、プッシュ、Slackの実際のテスト送信、ブラウザー権限の処理、Slackの接続フローを備えています。どの設定セクションにも組み込めます。

どのAIプロダクトも、結局は同じ通知ページに行き着きます。起こり得ることのリスト、それが届く場所のリスト、夜間に止める方法です。NotificationSettingsは、そのページを1つのセクションにしたものです。SettingsSectionに組み込め、他のSettingsと同じ下書き、保存バー、未保存の変更に対するガードを使います。

中核は、イベントとチャンネルのグリッドです。最上段の行はすべてのイベントでチャンネルをオンまたはオフにし、最初の列はすべての場所でイベントをオンまたはオフにし、角のチェックボックスはそのすべてを操作します。一部だけがオンのときは、ダッシュが表示されます。キーボードでは、グリッドは1つのタブストップです。矢印キーでチェックボックス間を移動し、HomeとEndで行に沿ってジャンプし、Spaceで切り替えます。スクリーンリーダーは各項目を「Email for Agent finished」と読み上げます。スマートフォンでは、グリッドがイベントごとのカードになり、大きなチャンネルチップが付きます。

各チャンネルには、実際に送信するSend testがあります。デスクトップとプッシュはページ上に通知バナーを表示し、デスクトップはブラウザーが許可していれば実際のシステム通知も発行します。メールは受信トレイ内のメッセージのプレビューを開き、メールがまとめられている場合はダイジェスト版を開きます。Slackは、チームがチャンネルで見るとおりのメッセージを表示します。

Desktopはブラウザーの権限を読み取ります。まだ確認されていない場合はAllowで確認し、ブロックされている場合は変更できる場所を案内し、ブラウザーが通知を表示できない場合はその旨を伝えます。Slackは未接続の状態で始まり、その列はオフで、Connectはワークスペースとチャンネルを選ぶダイアログを開きます。接続に失敗した場合は、理由がダイアログに表示され、何も失われません。

静音時間は、選んだタイムゾーンで、選んだ曜日の2つの時刻の間、デスクトップとプッシュの通知を保留し、次の静音期間がいつ始まるか、または現在静音中であることを示す行が付きます。承認などの緊急のイベントは、引き続き通知されます。

  1. Proレジストリをcomponents.jsonに追加する

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. トークンを追加する

    アカウントページでトークンを作成し、.env.local に HEXTAUI_PRO_TOKEN として設定してください。

  3. ブロックを追加する

    pnpm dlx shadcn@latest add @hextaui-pro/notifications

APIに接続する

保存済みの設定と、非同期のonSaveを渡します。編集は、保存バーまたは⌘Sで保存するまで下書きに留まります。フィールドエラーを返すとフィールドの下に表示され、エラーを投げるとメッセージが保存バーに表示されます。onSendTestは、プレビューが表示される前に実際のテストを送信します。

"use client"

import { IconBell } from "@tabler/icons-react"

import { SettingsSection, SettingsShell } from "../settings/settings"

import { NotificationSettings, type NotificationPreferences } from "@/components/blocks/notifications/notification-settings"

export function NotificationsPage({ preferences }: { preferences: NotificationPreferences }) {
  return (
    <SettingsShell
      sections={[{ id: "notifications", label: "Notifications", icon: <IconBell /> }]}
      className="h-svh"
    >
      <SettingsSection id="notifications">
        <NotificationSettings
          preferences={preferences}
          email="[email protected]"
          device="Mia’s iPhone"
          onSave={async (values) => {
            const response = await fetch("/api/notifications", {
              method: "PUT",
              body: JSON.stringify(values),
            })
            if (response.status === 422) return { quietDays: "Pick at least one day." }
            if (!response.ok) throw new Error("Check your connection and try again.")
          }}
          onSendTest={async ({ channel, event }) => {
            const response = await fetch("/api/notifications/test", {
              method: "POST",
              body: JSON.stringify({ channel, event: event.id }),
            })
            if (!response.ok) throw new Error("Couldn’t send the test.")
          }}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

独自のイベントとチャンネル

行を追加・削除するにはevents、列を選ぶにはchannelsを渡します。各イベントには、テストプレビュー用のsampleと、静音時間を通過させるurgentを指定できます。onSlackConnectは選択したワークスペースとチャンネルを受け取ります。失敗理由を表示するにはエラーを投げると、ダイアログは開いたままになります。

"use client"

import * as React from "react"

import {
  NotificationSettings,
  notificationEvents,
  type NotificationEvent,
  type NotificationPreferences,
  type SlackConnection,
  type SlackWorkspace,
} from "@/components/blocks/notifications/notification-settings"

const events: NotificationEvent[] = [
  ...notificationEvents.filter((event) => event.id !== "billing"),
  {
    id: "deploys",
    label: "Deploys",
    description: "An agent ships to production.",
    sample: {
      title: "Deployed to production",
      body: "“Fix flaky checkout test” is live. 2 checks passed.",
      subject: "Deployed: Fix flaky checkout test",
      action: "Open deploy",
    },
  },
]

export function TeamNotifications({
  preferences,
  workspaces,
  connection,
}: {
  preferences: NotificationPreferences
  workspaces: SlackWorkspace[]
  connection: SlackConnection | null
}) {
  const [slack, setSlack] = React.useState(connection)

  return (
    <NotificationSettings
      preferences={preferences}
      events={events}
      channels={["email", "slack"]}
      email="[email protected]"
      slack={slack}
      slackWorkspaces={workspaces}
      onSlackConnect={async ({ workspace, channel }) => {
        const response = await fetch("/api/slack/connect", {
          method: "POST",
          body: JSON.stringify({ workspace: workspace.id, channel: channel.id }),
        })
        if (response.status === 403) {
          throw new Error(`Hexta isn’t in #${channel.name}. Run /invite @Hexta there first.`)
        }
        if (!response.ok) throw new Error("Slack didn’t answer. Try again.")
        setSlack({ workspace: workspace.name, channel: channel.name })
      }}
      onSlackDisconnect={async () => {
        await fetch("/api/slack/connect", { method: "DELETE" })
        setSlack(null)
      }}
      onSave={(values) =>
        fetch("/api/notifications", { method: "PUT", body: JSON.stringify(values) }).then(() => {})
      }
    />
  )
}

構造

外側から内側へ組み合わせるパーツ。

パーツ説明
NotificationSettingsセクション全体。イベントのグリッド、テストつきのチャンネル、静音時間、メールダイジェスト。
notificationEventsデフォルトのイベント: Agent finished、Needs approval、Run failed、Mentions、Weekly summary、Billing。
notificationChannelsデフォルトのチャンネル: desktop、email、push、slack。

NotificationSettings

SettingsSectionの内側に置く必要があります。

プロパティ型デフォルト
preferences現在保存されている内容: { matrix, quietHours, quietFrom, quietTo, quietDays, timeZone, quietUrgent, digest }。matrixは各イベントidを、オンになっているチャンネルに対応付けます。変更があり、編集がない場合、下書きはそれに追従します。
NotificationPreferences–
onSave下書きを保存します。フィールドエラーを表示するには { quietDays: "…" } などを返し、保存バーにメッセージを表示するにはエラーを投げます。
(preferences) => void | errors | Promise<void | errors>–
eventsグリッドの行: { id, label, description?, urgent?, sample? }。sampleはテストプレビュー用の { title, body, subject, action } です。urgentのイベントは静音時間を通過できます。
NotificationEvent[]notificationEvents
channelsグリッドの列と、Channelsの下の行を、順番に。"desktop"、"email"、"push"、"slack" のいずれか。
NotificationChannel[]notificationChannels
emailメールの送信先。Emailの下とテストメールに表示されます。
string"[email protected]"
deviceプッシュ通知を受け取るスマートフォン。
string"your phone"
slack接続済みのSlackチャンネル。nullの間は、Slackの列はオフです。
{ workspace, channel } | nullnull
slackWorkspaces接続時に選べるワークスペース: { id, name, channels: { id, name, private? }[] }。
SlackWorkspace[][]
onSlackConnect選択したチャンネルを接続します。エラーを投げると、そのメッセージがダイアログに表示されます。
({ workspace, channel }) => void | Promise<void>–
onSlackDisconnectユーザーが確認した後、Slackを切断します。
() => void | Promise<void>–
onSendTest実際のテストを送信します。resolveされるとプレビューが表示され、エラーを投げるとボタンに再試行が表示されます。eventは、そのチャンネルでオンになっている最初のイベントです。
({ channel, event }) => void | Promise<void>–
キーアクション
Tab最後に使ったチェックボックスでグリッドに一度入り、その後チャンネルに移動します。
↑↓←→グリッドで、チェックボックス間を移動します。左右は読み方向に従います。
HomeEndグリッドで、行の先頭または末尾にジャンプします。CtrlまたはCommandキーを併用すると、最初または最後のチェックボックスにジャンプします。
Spaceフォーカスのあるチェックボックスをオンまたはオフにします。行、列、角のチェックボックスでは、すべてをオンにし、すべてがオンの場合はオフにします。
Escフォーカスがあるとき、テスト通知のバナーを閉じます。
  • グリッドは、列ヘッダーと行ヘッダーを持つARIAグリッドで、すべてのチェックボックスには「Mobile push for Run failed」のようにチャンネルとイベントの名前が付きます。
  • 行、列、角のチェックボックスは、一部だけがオンのときmixedになるため、スクリーンリーダーは「partially checked」と読み上げます。
  • テスト送信、権限の結果、Slackの変更はpoliteに読み上げられます。次の静音期間は、編集に合わせて更新されるステータス行です。
  • テストバナーは、ホバーまたはフォーカス中は一時停止し、閉じるボタンがあり、モーション軽減時はスライドではなくフェードします。
  • スマートフォンでは、すべてのチャンネルチップが44pxのターゲットになります。

使用技術

Notifications を構成する無料のHextaUIコンポーネントです。それぞれ単独でインストールできます。

コード

5 個のファイルを components/blocks/notifications に追加しました。