開発コンポーネント
共通コンポーネント・汎用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名 | 型 | デフォルト | 説明 |
|---|---|---|---|
label | string | - | バーの上部に表示するラベルテキスト |
value | number | - | 0〜100の進捗率 |
showValue | boolean | true | パーセンテージ数値の表示/非表示 |
subLabel | string | - | バーの下部に表示するサブラベル(ファイル名など) |
containerClassName | string | - | 外側コンテナへの追加クラス |
className | string | - | 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 />類似パターンの実装時のヒント
同様の「グローバルに常駐する進捗通知」を作る場合は以下を参考にしてください:
- Atom設計: ファイル/タスクごとの粒度でMapに管理 → 派生Atomで集計(集積ロジックを分離)
- コールバック橋渡し: 非Reactロジックとのつなぎは「シングルトン・コールバック変数」パターンを使用
- 表示位置:
fixed bottom-20 right-4 z-[100]をベースにモバイルのボトムナビ(bottom-16)との重なりに注意 - 余韻: 完了・エラー後も
setTimeoutで数秒表示を維持し、フィードバックの確認機会を確保 - 退場アニメーション:
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 />関連ドキュメント
- 09_design_system - §6.8「バックグラウンド進捗通知」
- 11_long_term_memory_architecture - §2.3「モデルダウンロードの可視化」
- 2026-03-04_1646_report_embedding-model-download-progress - 初回実装レポート