BluePeriod Docs
開発コンポーネント

共通コンポーネント・汎用UI

アプリ横断的に使用する共通コンポーネントと汎用UI

14. 共通コンポーネント・汎用UI (common / ui)

このドキュメントでは、src/components/common/ および src/components/ui/ に追加された、アプリ横断的に使用可能なコンポーネントを記録します。

ディレクトリ設計の思想

  • src/components/ui/ ... shadcn/ui ベースの純粋な「部品(原子)」。ロジックを持たない。
  • src/components/common/ ... Jotai Atom や外部ロジックと接続した「機能コンポーネント」。複数ページで共有、AppShell に常駐するものを置く。

ProgressWithLabel

ファイル: src/components/ui/progress-with-label.tsx
種別: 汎用UIコンポーネント(原子)
追加日: 2026-03-04

概要

shadcn/ui の Progress をラップし、ラベル・パーセンテージ・サブラベルを統合したプログレスバーコンポーネント。ロジックを一切持たず、propsによる制御のみ。

Props

Prop名デフォルト説明
labelstring-バーの上部に表示するラベルテキスト
valuenumber-0〜100の進捗率
showValuebooleantrueパーセンテージ数値の表示/非表示
subLabelstring-バーの下部に表示するサブラベル(ファイル名など)
containerClassNamestring-外側コンテナへの追加クラス
classNamestring-Progress 本体への追加クラス(高さ変更など)

使用例

<ProgressWithLabel
  label="モデルダウンロード中"
  value={72}
  showValue={true}
  subLabel="model.onnx"
/>

応用パターン

エラー状態でプログレスバー自体を赤くする場合は className を使用:

<ProgressWithLabel
  label="ダウンロード失敗"
  value={45}
  className="bg-destructive/20 [&>div]:bg-destructive"
/>

転用先

  • ModelDownloadStatus.tsx
  • 再ベクタライズ進捗の表示(reVectorizationProgressAtom と組み合わせ)
  • 一括インポート進捗の表示

ModelDownloadStatus

ファイル: src/components/common/ModelDownloadStatus.tsx
種別: 機能コンポーネント(Atom接続)
追加日: 2026-03-04
配置: AppShell.tsx 内に常駐

概要

Embeddingモデルのダウンロード状況を監視し、ダウンロード中または完了・エラー時に画面右下へフローティング表示するコンポーネント。modelDownloadStatusAtom を購読し、状態変化に応じた可視・不可視の制御を行う。

表示条件

状態表示内容
idle❌ 非表示ダウンロード実績なし
downloading✅ 表示プログレスバー + ファイル名
error✅ 表示エラーメッセージ(赤バー)
done✅ 表示(3秒)完了メッセージ → 自動退場

アーキテクチャ

transformers.js
  ↓ progress_callback
embedding.ts (globalForTransformers.onProgress)
  ↓ registerProgressCallback で登録
AppManager.tsx (store.set)
  ↓ Jotai Store API
modelDownloadProgressMapAtom (Record<filename, { loaded, total, status }>)
  ↓ 派生Atom
modelDownloadStatusAtom → { progress, fileName, status, isDownloading }
  ↓ useAtomValue
ModelDownloadStatus.tsx (UI表示)

配置方法

AppShell.tsx に一度配置するだけで、全ページから有効になる:

// AppShell.tsx
import { ModelDownloadStatus } from '@/components/common/ModelDownloadStatus';

// JSX内
<ModelDownloadStatus />

類似パターンの実装時のヒント

同様の「グローバルに常駐する進捗通知」を作る場合は以下を参考にしてください:

  1. Atom設計: ファイル/タスクごとの粒度でMapに管理 → 派生Atomで集計(集積ロジックを分離)
  2. コールバック橋渡し: 非Reactロジックとのつなぎは「シングルトン・コールバック変数」パターンを使用
  3. 表示位置: fixed bottom-20 right-4 z-[100] をベースにモバイルのボトムナビ(bottom-16)との重なりに注意
  4. 余韻: 完了・エラー後も setTimeout で数秒表示を維持し、フィードバックの確認機会を確保
  5. 退場アニメーション: AnimatePresence + motion.div による y: 20 のスライドイン/アウト

バックグラウンド進捗通知パターン(設計ガイド)

長時間バックグラウンドタスクに進捗フィードバックを追加する際の実装テンプレートです。

Step 1: 状態を定義する(memory-atoms.ts または専用ファイル)

// タスクごとの進捗エントリ
interface TaskProgress {
  loaded: number;
  total: number;
  status: 'progress' | 'done' | 'init' | 'error';
}

// Mapで複数タスクを管理
export const myTaskProgressMapAtom = atom<Record<string, TaskProgress>>({})

// 集計した表示用Atom(派生Atom)
export const myTaskStatusAtom = atom((get) => {
  const map = get(myTaskProgressMapAtom)
  const items = Object.values(map)
  if (items.length === 0) return { isActive: false, progress: 0, status: 'idle' as const }
  
  const totalLoaded = items.reduce((s, i) => s + i.loaded, 0)
  const totalSize   = items.reduce((s, i) => s + i.total, 0)
  const isAllDone   = items.every(i => i.status === 'done')
  const hasError    = items.some(i => i.status === 'error')
  
  return {
    isActive: !isAllDone && !hasError,
    progress: totalSize > 0 ? (totalLoaded / totalSize) * 100 : 0,
    status: hasError ? 'error' : (isAllDone ? 'done' : 'downloading') as const
  }
})

Step 2: ロジック層にコールバック口を開ける

// myLogic.ts
const globalScope = globalThis as { onMyProgress?: (p: MyProgressEvent) => void }

export function registerMyProgressCallback(cb: (p: MyProgressEvent) => void) {
  globalScope.onMyProgress = cb
}

// 内部の長時間処理で進捗を報告
function doHeavyWork() {
  // ...処理中...
  globalScope.onMyProgress?.({ loaded: 50, total: 100, status: 'progress', key: 'task-1' })
}

Step 3: AppManager で一度だけ登録

// AppManager.tsx の useEffect 内
registerMyProgressCallback((p) => {
  const current = store.get(myTaskProgressMapAtom)
  store.set(myTaskProgressMapAtom, { ...current, [p.key]: { loaded: p.loaded, total: p.total, status: p.status } })
})

Step 4: 表示コンポーネントを common/ に作成

// src/components/common/MyTaskStatus.tsx
export const MyTaskStatus = () => {
  const { isActive, progress, status } = useAtomValue(myTaskStatusAtom)
  const [isVisible, setIsVisible] = React.useState(false)
  
  React.useEffect(() => {
    if (isActive || status === 'error') { setIsVisible(true) }
    else if (status === 'done') {
      setIsVisible(true)
      const t = setTimeout(() => setIsVisible(false), 3000)
      return () => clearTimeout(t)
    } else { setIsVisible(false) }
  }, [isActive, status])
  
  return (
    <AnimatePresence>
      {isVisible && (
        <motion.div
          initial={{ opacity: 0, y: 20, scale: 0.95 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: 20, scale: 0.95 }}
          className="fixed bottom-20 right-4 z-[100] w-72"
        >
          <Card className="p-3 bg-background/95 backdrop-blur border-primary/20">
            <ProgressWithLabel label="処理中..." value={progress} />
          </Card>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

Step 5: AppShell に配置

// AppShell.tsx
<MyTaskStatus />

関連ドキュメント

On this page